Three.js Shader实现点云顶点球内持续移动的问题排查
问题:Three.js Shader实现球内粒子限制移动失败
我需要实现点云粒子在半径为X的球体内持续移动的效果,已经用非Shader方式完成了——初始化时随机生成粒子位置和速度数组,每帧遍历粒子,将速度叠加到位置上,若位置到原点的长度超过球半径则反转对应粒子的速度,标记Points几何体的position属性需更新,最终粒子能随机移动但始终被限制在球内(附效果GIF)。
但换成Three.js Shader实现时,粒子会扩散至无穷远,达不到预期效果。当前Shader代码如下:
const uniforms = { u_time: { value: 0 }, u_radius: { value: 1500 }, }; const vShader = ` uniform float u_time; uniform int u_radius; attribute vec3 velocity; varying vec3 v_position; void main() { v_position = position; vec3 vel = velocity * u_time; if(length(position) > float(u_radius)) { vel = vel * -1.0; } gl_Position = projectionMatrix * modelViewMatrix * vec4(position + vel, 1.0); gl_PointSize = 10.0; }`; const fShader = ` varying vec3 v_position; void main() { vec3 color = vec3(1.0); gl_FragColor = vec4(color, 0.7); }`;
我的思路是对比每个顶点当前位置到原点的长度与球半径,超出则反转速度向量,但显然忽略了关键逻辑。
问题根源
- Shader无状态特性误解:CPU端是逐帧更新粒子位置(
position = position + velocity),但Shader里的u_time是累计时间,直接用velocity * u_time会让粒子做匀速直线运动,而非逐帧叠加位移,反弹逻辑只触发一次,无法持续限制粒子。 - 反弹逻辑错误:仅反转基于总时间计算的速度向量,没有修正已经超出球体的粒子位置,也无法模拟反弹后的持续运动。
- 类型不匹配:
u_radius用int类型和float运算,容易出现精度问题。
两种可行解决方案
方案1:纯Shader模拟反弹(数学反射)
通过反射公式计算粒子在球内的运动路径,修正超出边界的位置:
修改顶点着色器:
uniform float u_time; uniform float u_radius; // 改为float类型,避免精度问题 attribute vec3 velocity; attribute vec3 initialPosition; // 传入粒子初始位置,保留原始position属性 varying vec3 v_position; void main() { vec3 pos = initialPosition; vec3 velDir = normalize(velocity); // 归一化速度方向 float speed = length(velocity); // 保留原始速度大小 // 计算累计位移 vec3 displacement = velDir * u_time * speed; vec3 currentPos = pos + displacement; float dist = length(currentPos); if (dist > u_radius) { // 计算球面上的法向量(当前位置的单位向量) vec3 normal = normalize(currentPos); // 反射公式:R = V - 2*(V·N)*N vec3 reflectedDir = reflect(velDir, normal); // 将粒子拉回球内,修正超出部分 float overshoot = dist - u_radius; currentPos = currentPos - reflectedDir * overshoot; // 计算反弹后的剩余位移 float remainingTime = (u_time * speed - (dist - overshoot)) / speed; currentPos += reflectedDir * remainingTime * speed; } v_position = currentPos; gl_Position = projectionMatrix * modelViewMatrix * vec4(currentPos, 1.0); gl_PointSize = 10.0; }
JS端需要添加initialPosition属性:
// 假设geometry是你的Points几何体 geometry.setAttribute('initialPosition', new THREE.BufferAttribute(geometry.attributes.position.array, 3));
方案2:CPU端更新位置,Shader仅渲染
这种方式更接近你原本的非Shader逻辑,性能允许的情况下更直观:
JS端每帧更新粒子位置:
function updateParticles() { const positions = geometry.attributes.position.array; const velocities = geometry.attributes.velocity.array; const radius = uniforms.u_radius.value; const timeStep = 0.1; // 时间步长,控制粒子移动速度 for (let i = 0; i < positions.length; i += 3) { // 叠加速度到位置 positions[i] += velocities[i] * timeStep; positions[i+1] += velocities[i+1] * timeStep; positions[i+2] += velocities[i+2] * timeStep; // 计算到原点的距离 const dist = Math.sqrt(positions[i]**2 + positions[i+1]**2 + positions[i+2]**2); if (dist > radius) { // 反转速度 velocities[i] *= -1; velocities[i+1] *= -1; velocities[i+2] *= -1; // 将粒子拉回球内,避免卡在边界外 const scale = radius / dist; positions[i] *= scale; positions[i+1] *= scale; positions[i+2] *= scale; } } geometry.attributes.position.needsUpdate = true; geometry.attributes.velocity.needsUpdate = true; } // 动画循环中调用 function animate() { requestAnimationFrame(animate); updateParticles(); renderer.render(scene, camera); }
Shader简化为仅渲染:
// 顶点着色器 varying vec3 v_position; void main() { v_position = position; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); gl_PointSize = 10.0; }
内容的提问来源于stack exchange,提问作者Dimitris Karagiannis
相关产品推荐
相关产品推荐

