threeJS 让物体延指定路线运动 & 生长的线

效果图

(图片大小限制,有一点跳帧,实际上是连贯的)
请添加图片描述

实现

本功能分为两个文件来实现(myPathAnimation.js和floor.vue),计算和路径绘制等功能封装到MyPath自定义类中。值得一提的是粗线段生长的原理是逐渐把新的点塞到路径中去,要使用(因为粗线条Line2官方没有文档,相关API我查了很久才找到):
this.geometry.attributes.instanceEnd.setXYZ(this.lineIndex, posi.x, posi.y, posi.z)
this.geometry.attributes.instanceEnd.needsUpdate = true

myPathAnimation.js代码如下:

/*
 * @Author: WJT
 * @Date: 2022-10-21 10:14:47
 * @Description: file content
 */
import * as THREE from 'three'
import { LineGeometry } from 'three/examples/jsm/lines/LineGeometry.js'
import { LineMaterial } from 'three/examples/jsm/lines/LineMaterial.js'
import { Line2 } from 'three/examples/jsm/lines/Line2.js'

// 自定义路径类
class MyPath {
  constructor (array) {
    // 将传进来的数组转换为Vec3集合
    const pointsArr = []
    if (array.length % 3 !== 0) {
      console.error('错误,数据的个数非3的整数倍!', array)
      return null
    }
    for (let index = 0; index < array.length; index += 3) {
      pointsArr.push(new THREE.Vector3(array[index], array[index + 1], array[index + 2]))
    }

    // 顶点位置三维向量数组
    this.pointsArr = pointsArr

    // 折线几何体 (细线)
    // this.line = null
    // {
    //   const lineMaterial = new THREE.LineBasicMaterial({
    //     color: 0xff00ff
    //   })
    //   const lineGeometry = new THREE.BufferGeometry().setFromPoints(pointsArr)
    //   this.line = new THREE.Line(lineGeometry, lineMaterial)
    // }

    // 折线几何体 (粗线)
    this.line = null
    this.geometry = new LineGeometry()
    // this.geometry.setPositions(array)
    this.geometry.setPositions(new Float32Array(500 * 3))
    this.matLine = new LineMaterial({
      color: 0xEE0000, // 0xffffff
      linewidth: 10, // in world units with size attenuation, pixels otherwise
      // vertexColors: true, // 默认为false,为false时颜色仅由LineMaterial的color决定;为true时颜色由LineMaterial的color和LineGeometry的color共同决定
      // resolution:  // to be set by renderer, eventually
      dashed: false
      // alphaToCoverage: true // 通道印射:该属性继承于基类Material,默认为false;如果为true的话曲线的每一段边缘会有白的的线条,曲线会看起来一节一节的(不晓得原理)
    })
    this.line = new Line2(this.geometry, this.matLine)
    // 锚点几何体(每个转角的小正方体)
    // this.points = null
    // {
    //   const pointsBufferGeometry = new THREE.BufferGeometry()
    //   pointsBufferGeometry.setAttribute('position', new THREE.Float32BufferAttribute(array, 3))
    //   const pointsMaterial = new THREE.PointsMaterial({ color: 0xFF7F00, size: 3 })
    //   this.points = new THREE.Points(pointsBufferGeometry, pointsMaterial)
    // }

    // 计算每个锚点在整条折线上所占的百分比
    this.pointPercentArr = []
    {
      const distanceArr = [] // 每段距离
      let sumDistance = 0 // 总距离
      for (let index = 0; index < pointsArr.length - 1; index++) {
        distanceArr.push(pointsArr[index].distanceTo(pointsArr[index + 1]))
      }
      sumDistance = distanceArr.reduce(function (tmp, item) {
        return tmp + item
      })

      const disPerSumArr = [0]
      disPerSumArr.push(distanceArr[0])
      distanceArr.reduce(function (tmp, item) {
        disPerSumArr.push(tmp + item)
        return tmp + item
      })

      disPerSumArr.forEach((value, index) => {
        disPerSumArr[index] = value / sumDistance
      })
      this.pointPercentArr = disPerSumArr
    }

    // 上一次的朝向
    this.preUp = new THREE.Vector3(0, 0, 0)

    // run函数需要的数据
    this.perce = 0 // 控制当前位置占整条线百分比
    this.speed = 0.0050 // 控制是否运动
    this.turnFactor = 0 // 暂停时间因子
    this.turnSpeedFactor = 0.005 // 转向速度因子
    this.obj = null

    this.preTime = new Date().getTime()
    this.firstTurn = false
    this.lineIndex = 0
  }

  // 获取点,是否转弯,朝向等
  getPoint (percent) {
    let indexP = 0
    let indexN = 0
    let turn = false

    for (let i = 0; i < this.pointPercentArr.length; i++) {
      if (percent >= this.pointPercentArr[i] && percent < this.pointPercentArr[i + 1]) {
        indexN = i + 1
        indexP = i
        if (percent === this.pointPercentArr[i]) {
          turn = true
        }
      }
    }

    const factor = (percent - this.pointPercentArr[indexP]) / (this.pointPercentArr[indexN] - this.pointPercentArr[indexP])
    const position = new THREE.Vector3()
    position.lerpVectors(this.pointsArr[indexP], this.pointsArr[indexN], factor) // position的计算完全正确

    // 计算朝向
    const up = new THREE.Vector3().subVectors(this.pointsArr[indexN], this.pointsArr[indexP]) // subVectors(a:Vector3,b:Vector3) 将该向量设置为a-b
    const preUp = this.preUp
    if (this.preUp.x != up.x || this.preUp.y != up.y || this.preUp.z != up.z) {
      // console.info('当前朝向与上次朝向不等,将turn置为true!')
      turn = true
    }

    this.preUp = up

    return {
      position,
      direction: up,

      turn, // 是否需要转向
      preUp // 当需要转向时的上次的方向

    }
  }

  // 参数:是否运动,运动的对象,是否运动到结尾
  run (animata, cube, camera = null, end) {
    if (end) {
      this.perce = 0.99999
      this.obj = this.getPoint(this.perce)

      // 修改位置
      const posi = this.obj.position

      // cone.position.set(posi.x, posi.y, posi.z);
      cube.position.set(posi.x, posi.y, posi.z) // 相机漫游2
    } else if (animata) {
      // 转弯时
      if (this.obj && this.obj.turn) {
        if (this.turnFactor == 0) {
          this.preTime = new Date().getTime()
          this.turnFactor += 0.000000001
        } else {
          const nowTime = new Date().getTime()
          const timePass = nowTime - this.preTime
          this.preTime = nowTime

          this.turnFactor += this.turnSpeedFactor * timePass
        }

        // console.log('--->>> 当前需要turn , turnFactor值为 :', this.turnFactor)
        if (this.turnFactor > 1) {
          this.turnFactor = 0
          this.perce += this.speed

          this.obj = this.getPoint(this.perce)
        } else {
          // 修改朝向 (向量线性插值方式)
          const interDirec = new THREE.Vector3()
          // lerpVectors( v1 : Vector3, v2 : Vector3, alpha : Float )v1 - 起始的Vector3。v2 - 朝着进行插值的Vector3。alpha - 插值因数,其范围通常在[0, 1]闭区间。
          // 将此向量设置为在v1和v2之间进行线性插值的向量, 其中alpha为两个向量之间连线的长度的百分比 —— alpha = 0 时表示的是v1,alpha = 1 时表示的是v2。
          interDirec.lerpVectors(this.obj.preUp, this.obj.direction, this.turnFactor)

          let look = new THREE.Vector3()
          // add ( v : Vector3 ) 将传入的向量v和这个向量相加。
          look = look.add(this.obj.position)
          look = look.add(interDirec)

          // cone.lookAt(look);
          cube.lookAt(look) // 相机漫游1
        }
      }

      // 非转弯时
      else {
        this.obj = this.getPoint(this.perce)
        // this.geometry.attributes.instanceStart.needsUpdate = true
        // 修改位置
        const posi = this.obj.position
        // cone.position.set(posi.x, posi.y, posi.z);
        cube.position.set(posi.x, posi.y, posi.z) // 相机漫游2

        // 线条增长
        this.lineIndex++
        this.geometry.attributes.instanceStart.setXYZ(this.lineIndex,
          this.geometry.attributes.instanceEnd.getX(this.lineIndex - 1),
          this.geometry.attributes.instanceEnd.getY(this.lineIndex - 1),
          this.geometry.attributes.instanceEnd.getZ(this.lineIndex - 1))
        this.geometry.attributes.instanceEnd.setXYZ(this.lineIndex, posi.x, posi.y, posi.z)
        this.geometry.attributes.instanceEnd.needsUpdate = true
        this.geometry.attributes.instanceStart.needsUpdate = true

        // camera 视角跟着运动,y+5是为了有更好的观感
        camera && camera.position.set(posi.x, posi.y + 5, posi.z)
        // 当不需要转向时进行
        if (!this.obj.turn) {
          const look = posi.add(this.obj.direction)
          // cone.lookAt(look);
          cube.lookAt(look) // 相机漫游3
          camera && camera.lookAt(look)
        }
        this.perce += this.speed
      }
    }
  }
}

export default MyPath

floor.vue文件如下:

<!--
 * @Author: WangJingTing
 * @Date: 2022-10-13 14:42:15
 * @Description: 楼层demo
-->
<template>
  <div class="webgl-container">
    <div id="webglDom"
         ref="webglDom"></div>
  </div>
</template>

<script>
import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
import { FBXLoader } from 'three/examples/jsm/loaders/FBXLoader.js'
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
import MyPath from './myPathAnimation.js'

export default {
  name: 'threeJS',
  data () {
    return {
      scene: null,
      camera: null,
      renderer: null,
      controls: null,
      cube: null,
      lineArr: null,
      a: new MyPath([
        0, 0.5, 0,
        35, 0.5, 0,
        35, 0.5, 20,
        35, 15.5, 40,
        35, 15.5, 45,
        -20, 15.5, 45,
        -20, 15.5, -20
      ]), // MyPath实例
      startFlag: true, // 是否运动
      endFlag: false, // 是否运动到了结尾
      line: null, // 线
      drawCount: 2,
      MAX_POINTS: 7 // 一共7个点
    }
  },
  mounted () {
    this.init()
  },
  methods: {
    init () {
      // 场景
      this.scene = new THREE.Scene()
      // PerspectiveCamera透视摄像机(视野角度(FOV),长宽比(aspect ratio),近截面(near),远截面(far))
      this.camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000)
      this.camera.position.set(100, 20, 0)
      // 渲染器
      this.renderer = new THREE.WebGLRenderer()
      this.renderer.setSize(window.innerWidth, window.innerHeight)
      this.renderer.outputEncoding = THREE.sRGBEncoding // 关键!默认情况下threeJS会使用线性编码(LinearEncoding)的方式渲染材质,因此会丢失真实颜色,需要改用RGB模式编码(sRGBEncoding)进行对材质进行渲染。
      document.getElementById('webglDom').appendChild(this.renderer.domElement)

      // 辅助坐标系
      const axesHelper = new THREE.AxesHelper(50)
      this.scene.add(axesHelper)

      // 添加线条
      this.scene.add(this.a.line)
      // 添加灯光
      this.addLight()
      // 添加拖放控制器
      this.addControl()
      // 载入fbx模型
      this.addfbx()
      // 载入gltf模型
      // this.addGltf()

      // 添加物体
      this.addCube()

      this.render()
    },
    addCube () {
      const material = new THREE.MeshBasicMaterial({ color: 0xCD00CD })
      const geometry = new THREE.CylinderBufferGeometry(0, 1, 3, 4) // 圆柱缓冲几何体
      geometry.rotateX(Math.PI / 2)
      this.cube = new THREE.Mesh(geometry, material)
      this.cube.position.set(0, 1, 0)
      this.scene.add(this.cube)
    },
    addfbx () {
      const loader = new FBXLoader()
      loader.load('/models/floor/楼层简易demo1.fbx', mesh => {
        mesh.scale.multiplyScalar(0.01)
        this.scene.add(mesh)
      }, undefined, function (error) {
        console.error(error)
      })
    },
    addGltf () {
      const loader = new GLTFLoader()

      loader.load('/models/chibi_gear_solid/scene.gltf', gltf => {
        const root = gltf.scene
        // root.multiplyScalar(0.1) // 定义模型的缩放大小
        root.castShadow = true //  投影
        root.rotation.z = 0.25 * Math.PI
        root.rotation.x = 0.5 * Math.PI
        root.position.z = -20
        this.scene.add(root)
      }, undefined, function (error) {
        console.error(error)
      })
    },
    addLight () {
      // 环境光
      const light = new THREE.AmbientLight(0xffffff, 0.5) // soft white light
      this.scene.add(light)

      // 平行光源
      const directionalLight = new THREE.DirectionalLight(0xffffff, 1)
      directionalLight.position.set(50, -30, 50)
      this.scene.add(directionalLight)
    },
    addControl () {
      // 创建一个控制器对象  相机  dom对象
      this.controls = new OrbitControls(this.camera, this.renderer.domElement)
      // 阻尼
      // this.controls.enableDamping = true
      // this.controls.dampingFactor = 0.05

      // 定义当平移的时候摄像机的位置将如何移动。如果为true,摄像机将在屏幕空间内平移。 否则,摄像机将在与摄像机向上方向垂直的平面中平移。
      this.controls.screenSpacePanning = true

      this.controls.minDistance = 10
      this.controls.maxDistance = 500
      this.controls.maxPolarAngle = Math.PI / 2
    },
    render () {
      this.renderer.render(this.scene, this.camera)
      this.controls.update()
      requestAnimationFrame(this.render)
      this.a.matLine.resolution.set(window.innerWidth, window.innerHeight)
      // 路程播放一遍就停止
      if (this.a.perce < 1) {
        // this.a.run(this.startFlag, this.cube, this.camera, this.endFlag)
        this.a.run(this.startFlag, this.cube, this.endFlag)
      }
      // 路程循环
      // this.a.run(this.startFlag, this.cube, this.endFlag)
      // if (this.a.perce >= 1) {
      //   this.a.perce = 0
      //   this.a.lineIndex = 0
      // }
    }
  }
}
</script>

<style scoped>
#webglDom,
.webgl-container {
  width: 100%;
  height: 100%;
  overflow: hidden;
}
</style>

参考:https://blog.csdn.net/qq_20535249/article/details/107611882

  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值