41 · 几何调试、性能与代码组织

返回目录 · 上一章

1. 先把抽象量画出来

位置错误时先画原点和目标点;方向错误时画箭头;光照错误时画法线。看见向量比不断改变负号更可靠。

import { createDemo } from './runtime.js';

const { THREE, scene, onFrame, dispose } = createDemo();
scene.add(new THREE.AxesHelper(3));
scene.add(new THREE.GridHelper(10, 10));
const origin = new THREE.Vector3(0, 1, 0);
const direction = new THREE.Vector3(1, 1, 0).normalize();
const arrow = new THREE.ArrowHelper(direction, origin, 2, 0xe58b72);
scene.add(arrow);
const target = new THREE.Vector3();
onFrame((dt, elapsed) => {
  target.set(Math.cos(elapsed) * 2, 1, Math.sin(elapsed) * 2);
  direction.subVectors(target, origin);
  const distance = direction.length();
  arrow.visible = distance > 1e-8;
  if (arrow.visible) {
    arrow.setDirection(direction.divideScalar(distance));
    arrow.setLength(distance);
  }
});
if (import.meta.hot) import.meta.hot.dispose(dispose);

ArrowHelper 的方向应是单位向量,长度由单独参数控制。箭头挂在 scene 下,输入位置与方向都是世界坐标;若把箭头放进变换过的父节点,就需要相应转换。

2. 用不变量定位问题

不要只判断“画面看起来差不多”,可以检查应当保持的性质:

操作 可检查的性质
单位方向 长度约为 1
正交基底 三个轴两两点积约为 0,长度约为 1
纯旋转 向量长度保持不变
矩阵逆向变换 先变换再逆变换,误差在容差内
法线变换 与变换后的表面切线仍垂直
无损镜面反射 速度大小保持,法向分量反号

如果纯旋转后长度改变,检查四元数是否为单位长度、矩阵是否还包含缩放。如果法线不再垂直,检查是否误用了普通方向矩阵。每条检查都有适用前提,非均匀缩放本来就不保长。

3. 最常见的排错顺序

第一步检查输入是否有限、单位是否一致;第二步标注每个变量的空间;第三步确认矩阵是否更新及对象是否被原地修改;第四步用简单轴向输入验证符号和顺序;最后再考虑浮点误差。

复杂场景先简化到相机在 (0,0,5) 看原点、父节点无旋转缩放。若简单情形正确,再逐个恢复父变换、窗口偏移和交互。不要同时改三处公式,否则无法判断哪一处是根因。

4. 临时对象复用与函数契约

const worldPosition = new THREE.Vector3();
const localTarget = new THREE.Vector3();
function moveToWorldTarget(object, targetWorld) {
  localTarget.copy(targetWorld);
  if (object.parent) object.parent.worldToLocal(localTarget);
  object.position.copy(localTarget);
  object.getWorldPosition(worldPosition);
  return worldPosition; // 借用结果;下一次调用会覆盖
}

复用能减少热点循环的垃圾回收,但上面返回的是共享临时对象。调用者如果要长期保存,必须 clone。公共工具函数更推荐接收 out 输出参数,明确所有权,并避免重入或嵌套调用覆盖共享临时值。

“不分配对象”不值得以隐蔽状态为代价。在不频繁执行的配置代码中,clone 往往更清晰;只对测量证明的热点优化。

5. 拆分渲染循环职责

常见顺序是:读取输入 → 更新模拟 → 更新对象及矩阵 → 更新需要当前矩阵的标签/uniform → 渲染。鼠标事件记录输入,渲染帧消费输入,可降低事件频率与显示频率不一致导致的重复工作。

每帧重复创建材质、几何体或纹理会产生更昂贵的资源问题。离开示例或替换资源时,应释放 geometry、material、texture、render target,并移除 DOM 监听和动画回调。共享资源须按所有权或引用计数管理,不能由任意一个网格单方面 dispose。

教程 runtime 只自动清理它的独立场景中常见资源;从场景移除且未另行注册清理的对象、复杂 uniform 容器、外部共享缓存和 render target 都需要显式管理。

6. 兼容性与验证范围

对 Shader 改动,至少检查控制台的编译与链接错误、资源路径、桌面及窄屏尺寸、窗口缩放、交互和销毁。精度限定会影响 GPU 运算,不同设备的性能差异必须实测;不要仅从桌面高端 GPU 的结果推断移动端表现。

yarn build 能验证打包,但通常不能验证所有 Shader 的运行时编译、几何正确性和交互效果。数学断言、构建和浏览器检查回答的是不同问题。

当前项目声明支持 Node 12/20,但 Three.js r185 的现代模块语法和 WebGL2 要求是另一层约束。不要因教学代码语法简单,就声称整个依赖树在所有旧运行时均已验证。升级或降级依赖应单独评估,本系列不修改依赖版本。

练习(不附答案)

  1. 为一个方向对齐功能加入基底长度与正交性检查,并用 ArrowHelper 显示结果。
  2. 故意遗漏一次矩阵更新,设计最小复现并解释为什么渲染后问题可能消失。
  3. 将一个返回共享 Vector3 的函数改为显式 out 参数,说明调用者保存结果的方法。
  4. 制定一个粒子性能对比实验,控制分辨率、数量和透明覆盖范围,记录结果而非只判断代码长短。