(图片大小限制,有一点跳帧,实际上是连贯的)
本功能分为两个文件来实现(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