如何让尘埃跟随遥控车

英文原作者: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 之间需要有一个差值,尘埃才能在相机中正确显示。因此作者问:应该使用什么样的向量差值?


二、问题诊断

上述代码实际上存在几个相互耦合的问题:

  1. 差值计算逻辑失效
    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 轴上并未真正跟随车体运动。

  2. 世界坐标差值与局部偏移混用
    尘埃粒子系统既要作为独立对象在场景中运动,又被期望“位于车尾”。这本质上是需要计算一个基于车体朝向的 局部偏移量,而不是简单地对世界坐标做差。

  3. 每帧创建多个临时 Vector3 对象
    在游戏循环中反复 new THREE.Vector3() 会造成不必要的内存分配,影响帧率稳定性(GC 抖动)。

  4. 未提及缓冲区更新标记
    代码修改了 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.Pointsthree.js 中的粒子系统,底层对应 WebGL 的 gl.POINTS 渲染,可译为“点精灵 / 粒子系统”。
position attribute此处指 THREE.BufferAttribute,属于 WebGL 中的顶点属性(vertex attribute),对应 GLSL 中的 attribute vec3 position。
needsUpdatethree.js 中用于标记 BufferAttribute 数据需要重新上传到 GPU 的机制。
updateRangethree.js 中用于指定缓冲区部分更新范围的 API,可减少 GPU 上传量。
shader着色器,WebGL 渲染管线中的可编程阶段,通常包含顶点着色器(vertex shader)和片元着色器(fragment shader)。
Vector3三维向量数学工具,用于表示位置、方向、偏移等。

以上术语在本文语境下均与 WebGL / three.js 渲染机制密切相关,翻译时需要保持一致性,避免与普通几何数学概念混淆。