25 · GPU 变形与 CPU 场景数据
onBeforeCompile 只改变 GPU 渲染程序。Three.js 的 BufferGeometry position 数组仍保留旧坐标,因此 computeBoundingBox()、computeBoundingSphere()、普通 Mesh.raycast 的三角形求交不会自动看到自定义波浪。把材质设置 needsUpdate 也不会回写 GPU 位置。
1. 三种常见症状
模型靠近屏幕边缘时突然消失,通常是视锥剔除使用的包围体太小;阴影突然消失,也可能是光源视锥剔除或阴影相机范围不够。鼠标“点不中可见波峰”,则是 Raycaster 在和旧网格求交。
这三类问题需要分别解决。mesh.frustumCulled = false 能排查剔除,却不会让拾取或碰撞变准确。
2. 已知位移上界时扩大包围体
设变形
geometry.computeBoundingBox();
const originalBounds = geometry.boundingBox.clone();
function updateWaveBounds(maxAmplitude) {
geometry.boundingBox.copy(originalBounds);
geometry.boundingBox.min.y -= Math.abs(maxAmplitude);
geometry.boundingBox.max.y += Math.abs(maxAmplitude);
geometry.boundingSphere = geometry.boundingBox.getBoundingSphere(new THREE.Sphere());
}
updateWaveBounds(0.5);
每次都从 originalBounds 开始,避免每次更新继续膨胀。多个波相加时可以用振幅绝对值之和作上界。风动画若位移为
包围体是局部空间数据,普通对象变换由 Three.js 后续处理。实例化对象还存在整体实例范围,应根据 InstancedMesh 的聚合包围体机制重新评估。位移幅度交互增加到原上界之外时,需要同步更新边界。
3. 拾取方案按需求选择
| 需求 | 方案 | 成本与限制 |
|---|---|---|
| 点击整片水域 | 与静态 XZ 平面求交 | 最便宜,忽略波高 |
| 放置漂浮标记 | 平面求出 x/z,再用相同高度函数计算 y | 不是射线与波面的严格交点,但常足够 |
| 鼠标准确命中波面 | 解析函数与射线求根 | 可精确到误差阈值,需要区间、迭代和多交点策略 |
| 任意变形网格精确交互 | CPU 维护变形后的碰撞代理或 GPU 拾取 | 增加更新或读回成本,需要工程支持 |
“CPU 维护一份几何体”并不一定要求把可见物体也搬到 CPU 变形。可见物体保留 GPU 动画,用更低分辨率、不可见但专用于计算的碰撞网格近似即可。
4. 射线与波面求交的数学条件
局部射线为
应先把世界射线转换到物体局部空间,并限制搜索在几何有效区域内。对已知有符号变化的区间 [a,b],二分法可稳定逼近一个根:
function bisectRoot(evaluate, a, b, iterations = 32) {
let fa = evaluate(a);
const fb = evaluate(b);
if (fa === 0) return a;
if (fb === 0) return b;
if (fa * fb > 0) return null;
for (let i = 0; i < iterations; i++) {
const mid = (a + b) * 0.5;
const fm = evaluate(mid);
if (Math.abs(fm) < 1e-5) return mid;
if (fa * fm <= 0) b = mid;
else { a = mid; fa = fm; }
}
return (a + b) * 0.5;
}
该函数只解决已括住根的区间,不能保证自动找到最近交点,也可能漏掉不改变符号的切触点。实际应用要先沿有效射线区间分段搜索,选择最靠近相机的合法根,并明确近似精度。非均匀缩放后局部射线参数不能随意当成世界距离;将命中点变回世界空间后重新测距更清楚。
5. CPU/GPU 共享参数而非互相猜测
JavaScript 的 Math.sin 和 GLSL 的 sin 可实现同一公式,但二者不保证位级一致。让时间、振幅、频率来自同一状态对象,避免两套动画计时漂移。高频复杂噪声和大坐标场景需容许误差。
验证时把 CPU 计算的标记放到 GPU 水面上,看它是否随时间贴合;再在屏幕边缘、缩放后、变形最大幅度时检查可见性。不要只在初始时间验证。
练习
- 为两组叠加正弦波推导保守包围体。
- 用静态平面拾取放置一个标记,再通过高度函数让它跟随波面。
- 构造一条与波面有多个交点的射线,说明为什么一次二分不保证最近命中。
- 比较关闭剔除与扩大包围体的适用条件,并说明它们为何不修复 Raycaster。