引言
我们在做项目的时候,有时候会遇到物体或者相机需要做复杂轨迹运动的情况,往往没法简单的通过修改位置来达成我们想要的运动效果。
这时候可以通过引入多段曲线去拟合我们想要的运动轨迹,再获取曲线的参数去控制物体做相应轨迹的运动。
目录
- 1、创建关键空间点数组
- 2、根据点数组绘制曲线
- 3、获取曲线上特定位置的点,修改物体位置
- 4、获取曲线上特定位置的切线,修改物体朝向
- 5、随时间实时改变物体位置和朝向
- 6、添加修改曲线功能
- 7、引入模型模拟应用场景
1、创建关键空间点数组
首先我们可以先找出运动轨迹上几个特定的点。
假设给定的点是(1,1,-1),(1,0,1),(-1,0,1),(-1,0,-1)
这里在每个点放了一个实体方块用于示意点的位置,同时为后面的调整功能做准备
const initialPoints = [
{ x: 1, y: 1, z: -1 },
{ x: 1, y: 0, z: 1 },
{ x: -1, y: 0, z: 1 },
{ x: -1, y: 0, z: -1 }
];
const addCube = (pos) => {
const geometry = new THREE.BoxBufferGeometry(0.1, 0.1, 0.1);
const material = new THREE.MeshBasicMaterial(0xffffff);
const cube = new THREE.Mesh(geometry, material);
cube.position.copy(pos);
scene.add(cube);
}
const cubeList = initialPoints.map(pos => {
return this.addCube(pos);
});
2、根据点数组绘制曲线
three.js 提供了好几种方法绘制曲线,这里采用的是 CatmullRom 插值的方法绘制曲线。
CatmullRom 插值的曲线一定会经过所有给定的点,所以这种方法会更适合用作轨迹曲线的绘制。
const curve = new THREE.CatmullRomCurve3(
cubeList.map((cube) => cube.position) // 直接绑定方块的position以便后续用方块调整曲线
);
curve.curveType = 'chordal'; // 曲线类型
curve.closed = true; // 曲线是否闭合
const points = curve.getPoints(50); // 50等分获取曲线点数组
const line = new THREE.LineLoop(
new THREE.BufferGeometry().setFromPoints(points),
new THREE.LineBasicMaterial({ color: 0x00ff00 })
); // 绘制实体线条,仅用于示意曲线,后面的向量线条同理,相关代码就省略了
scene.add(line);
3、获取曲线上特定位置的点,修改物体位置
有了曲线之后,可以通过 getPointAt 函数获取曲线上特定位置的点向量,然后复制给物体的 position
function changePosition (t) {
const position = curve.getPointAt(t); // t: 当前点在线条上的位置百分比,后面计算
mesh.position.copy(position);
}
为了直观表现下图采用 30 等分取点把位置向量绘制出来了,后面的图片也采用一样的方式展现向量
4、获取曲线上特定位置的切线,修改物体朝向
现在物体的位置对上了,但是朝向却是固定的,不符合生活经验。一般来说物体在运动的时候,正面总是朝向轨迹的切线方向的。
现在我们通过 getTangentAt 函数获取曲线上特定位置的切线向量,根据该切线向量和点的位置向量计算物体朝向的点向量,传入物体的 lookAt 函数
function changeLookAt (t) {
const tangent = curve.getTangentAt(t);
const lookAtVec = tangent.add(position); // 位置向量和切线向量相加即为所需朝向的点向量
mesh.lookAt(lookAtVec);
}
注意上图示的切线(黄线)实际起点为原点(0,0,0),这里为了示意切线在曲线上的位置,平移到了点所在位置上
因为 lookAt 实际上是指向某个点向量,如果直接传切线向量会导致物体朝向下图 A 点,需要和位置向量相加后才能得到所需的点向量(蓝线)即 C 点
5、随时间实时改变物体位置和朝向
现在轨迹上单一点的位置和朝向都可以获取到了,剩下的就是在渲染函数中实时修改了。
根据时间计算当前点在曲线上的位置百分比,传入第 3、4 步中
const loopTime = 10 * 1000; // loopTime: 循环一圈的时间
// 在渲染函数中获取当前时间
const render = () => {
let time = Date.now();
let t = (time % loopTime) / loopTime; // 计算当前时间进度百分比
changePosition(t);
changeLookAt(t);
requestAnimationFrame(render);
renderer.render(scene, camera);
}
requestAnimationFrame(render);
6、添加修改曲线功能
到这里曲线运动的效果是做出来了,但是如果我们想调整曲线,就得修改最初的点数组,既不直观也很繁琐。
参考 three.js 官网的 demo 发现可以通过 TransformControls 控制方块位置,实时修改曲线。同时因为前面的 curve 是通过方块的 position 生成的,所以方块位置的修改可以直接反映到 curve 上
import { TransformControls } from 'TransformControls.js'; // 引入模块
const control = new TransformControls(camera, renderer.domElement);
// 获取点击位置
const mouse = new THREE.Vector2();
renderer.domElement.addEventListener(
'click',
(event) => {
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
},
false
);
// 方块点击检测
const rayCaster = new THREE.Raycaster();
rayCaster.setFromCamera(mouse, camera);
const intersects = rayCaster.intersectObjects(cubeList);
if (intersects.length) {
const target = intersects[0].object;
control.attach(target); // 绑定controls和方块
scene.add(control);
}
// 修改曲线后同步修改实体线条
control.addEventListener('dragging-changed', (event) => {
if (!event.value) {
const points = curve.getPoints(50);
line.geometry.setFromPoints(points);
}
});
7、引入模型模拟应用场景
经过前面的步骤现在有了一个比较抽象的场景,现在可以考虑通过模型让应用场景更具象化。这里采用和场景契合度较高的过山车模型。
车模型的处理方式和方块基本没区别这里就不放相关代码了,轨道是通过一小段的轨道模型不断重复的方式去模拟。
// 轨道分段数
let railNum = 50;
// 导入模型
const loader = new GLTFLoader().setPath('model/');
loader.load('scene.gltf', (gltf) => {
// 轨道容器
const railway = new THREE.Object3D();
let position = new THREE.Vector3();
let tangent = new THREE.Vector3();
for (let i = 0; i < railNum; i++) {
// 复制多段轨道模型
let model = gltf.scene.clone();
railway.add(model);
// 这里和前面一样通过获取位置和切线向量去计算每段轨道的朝向
position = curve.getPointAt(i / railNum);
tangent = curve.getTangentAt(i / railNum);
model.position.copy(position);
model.lookAt(tangent.add(position));
}
scene.add(railway);
});
不过这样的方式相当于用多段直线拼出来的曲线,整体会比较生硬。如果把曲线调整的过长也会出现轨道接不上的问题。
three.js 官网的 examples 里有一个过山车 demo,轨道不是使用模型而是通过代码建模去模拟轨道,效果会自然很多。详见:https://threejs.org/examples/?q=roller#webxr_vr_rollercoaster
Demo 地址
http://demo.treedom.cn/threejs.curve_animation.wyl/
参考
https://threejs.org/examples/?q=curve#webgl_modifier_curve
https://threejs.org/examples/?q=spli#webgl_geometry_extrude_splines
了解更多
原文来源: 基于three.js的三维空间曲线轨迹运动