如何让尘埃跟随遥控车
英文原作者:meded90
原始链接:https://discourse.threejs.org/t/allow-dust-follow-rccar/93684
标签:shaders
本文是对 three.js 论坛技术问题《Allow dust follow rcCar》的中文深度解读,而非逐句直译。文中保留核心实现代码,并结合 WebGL / Three.js 粒子渲染机制给出优化建议。
一、原帖核心内容(中文编译)
作者表示已为粒子系统添加了着色器,并创建 THREE.Points 粒子对象:
const dust = new THREE.Points(geometry, material);
scene.add(dust);
在游戏循环中更新粒子位置:
const positionAttribute = dustGeometry.getAttribute("position");
for (let i = 0; i < count; i++) {
const i3 = i * 3;
// ...
if (positionAttribute.array[i3 + 1] > 2.5) {
positionAttribute.array[i3] = (Math.random() - 0.5) * 8;
positionAttribute.array[i3 + 1] = -2.5;
positionAttribute.array[i3 + 2] = (Math.random() - 0.5) * 4;
}
}
之后作者尝试使用以下代码让尘埃跟随遥控车:
let throughDiffDustAndRCCar = new THREE.Vector3().subVectors(
new THREE.Vector3(dust.position.x, 0, dust.position.z),
new THREE.Vector3(samplePickRCCar.position.x, 0, samplePickRCCar.position.z),
);
dust.position.copy({
x: samplePickRCCar.position.x,
y: camera.position.y,
z: samplePickRCCar.position.z + throughDiffDustAndRCCar.z,
});
作者的问题在于:samplePickRCCar.position.z 与 dust.position.z 应当不同,才能让尘埃在相机中可见。
按作者预期,尘埃应位于车尾,并且这种差异取决于 camera.y;rcCar.position.z 与 dust.position.z 之间需要有一个差值,尘埃才能在相机中正确显示。因此作者问:应该使用什么样的向量差值?
二、问题诊断
上述代码实际上存在几个相互耦合的问题:
-
差值计算逻辑失效
throughDiffDustAndRCCar计算的是尘埃与遥控车在 XZ 平面的世界坐标差值,但随后dust.position.z被设置为:samplePickRCCar.position.z + throughDiffDustAndRCCar.z;其中
throughDiffDustAndRCCar.z = dust.position.z - samplePickRCCar.position.z,代入后得到:samplePickRCCar.position.z + (dust.position.z - samplePickRCCar.position.z) = dust.position.z也就是说 尘埃的 z 坐标其实没有发生任何变化,只是被写回了原来的值。
同时dust.position.x被强行设为遥控车的 x 坐标,导致整个粒子系统在 X 轴上被锁定到车上,但在 Z 轴上并未真正跟随车体运动。 -
世界坐标差值与局部偏移混用
尘埃粒子系统既要作为独立对象在场景中运动,又被期望“位于车尾”。这本质上是需要计算一个基于车体朝向的 局部偏移量,而不是简单地对世界坐标做差。 -
每帧创建多个临时
Vector3对象
在游戏循环中反复new THREE.Vector3()会造成不必要的内存分配,影响帧率稳定性(GC 抖动)。 -
未提及缓冲区更新标记
代码修改了positionAttribute.array,但原文片段中未显示positionAttribute.needsUpdate = true,若未设置则 GPU 端缓冲区不会更新,粒子位置修改不会生效。
三、推荐修正方案
方案 A:将尘埃系统作为遥控车的子对象
这是最稳健的方式。将粒子系统挂到车体节点下,只需设置局部偏移即可:
const dust = new THREE.Points(geometry, material);
samplePickRCCar.add(dust);
// 局部坐标:车尾后方偏移,假设车辆朝向 -Z 时车尾位于负 Z
dust.position.set(0, 0, -1.5);
这样粒子系统会随车辆移动、旋转。粒子自身的运动仍可通过更新 positionAttribute 的局部坐标完成。
方案 B:世界空间下使用车辆四元数旋转偏移
如果必须保持 dust 独立于车体层级,应在世界空间中计算带朝向的偏移:
const _offset = new THREE.Vector3(0, 0, -1.5); // 车尾局部偏移
const _carPos = new THREE.Vector3();
samplePickRCCar.getWorldPosition(_carPos);
_offset.applyQuaternion(samplePickRCCar.quaternion);
dust.position.copy(_carPos).add(_offset);
dust.position.y = camera.position.y; // 若有需要,保持相机高度
这样尘埃粒子就会被放置在车辆后方,且与车辆朝向一致。
四、3 个核心优化要点
1. 复用临时向量,避免每帧 new Vector3
原文中每帧都会创建多个 THREE.Vector3,在游戏循环中频繁分配内存会触发垃圾回收,造成帧率抖动。
建议在循环外声明可复用变量:
const _vA = new THREE.Vector3();
const _vB = new THREE.Vector3();
const _diff = new THREE.Vector3();
// 每帧复用
_vA.set(dust.position.x, 0, dust.position.z);
_vB.set(samplePickRCCar.position.x, 0, samplePickRCCar.position.z);
_diff.subVectors(_vA, _vB);
2. 使用局部偏移或父子层级,取代世界坐标差值计算
“跟随”逻辑应基于车辆位置 + 局部偏移,而不是基于尘埃当前位置与车位置的世界坐标差。
将 dust 设为 rcCar 的子对象,或者使用车辆四元数旋转局部偏移,可以避免因世界坐标差导致 z 轴无法真正跟随的问题。
3. 更新 GPU 缓冲区时设置 needsUpdate 并使用更新范围
修改 positionAttribute.array 后,必须显式告诉 three.js 数据已变更:
positionAttribute.needsUpdate = true;
如果只更新部分粒子,建议使用:
positionAttribute.setUpdateRange(startIndex, count);
positionAttribute.needsUpdate = true;
同时将代码中的硬编码数值(如 2.5、-2.5、8、4)提取为可调参数,提升可维护性。
五、GLSL / WebGL 术语检查
| 术语 | 说明 |
|---|---|
THREE.Points | three.js 中的粒子系统,底层对应 WebGL 的 gl.POINTS 渲染,可译为“点精灵 / 粒子系统”。 |
position attribute | 此处指 THREE.BufferAttribute,属于 WebGL 中的顶点属性(vertex attribute),对应 GLSL 中的 attribute vec3 position。 |
needsUpdate | three.js 中用于标记 BufferAttribute 数据需要重新上传到 GPU 的机制。 |
updateRange | three.js 中用于指定缓冲区部分更新范围的 API,可减少 GPU 上传量。 |
shader | 着色器,WebGL 渲染管线中的可编程阶段,通常包含顶点着色器(vertex shader)和片元着色器(fragment shader)。 |
Vector3 | 三维向量数学工具,用于表示位置、方向、偏移等。 |
以上术语在本文语境下均与 WebGL / three.js 渲染机制密切相关,翻译时需要保持一致性,避免与普通几何数学概念混淆。
