首页 / Three.js 入门教程 / 调试辅助工具

Three.js 入门教程

调试辅助工具

本教程共 40 篇 · 第 24 篇 · 更新于 2026-08-14 · 约 9 分钟阅读

Three.jsStats.jslil-gui调试Helper性能监控

本节目标:学会用 Stats.js 监控性能、用 lil-gui 实时调参、用各类 Helper 可视化坐标轴、包围盒和视锥体。学完你能快速定位 3D 场景里”看不见”的问题。

调试三板斧

3D 开发里,很多问题肉眼看不到:帧率够不够、某个参数调多少合适、物体到底在哪。靠 console.log 猜,效率太低。three.js 生态有三个顺手工具:

  • Stats.js:帧率与内存监控
  • lil-gui:可视化参数面板(dat.GUI 的现代替代)
  • Helper 系列:把坐标轴、包围盒、视锥体直接画出来

Stats.js:性能监控

Stats.js 是 mrdoob 写的小工具,显示 FPS(每秒帧数)、渲染耗时、内存占用。官方把它的模块版本放在了 addons 里:

import Stats from 'three/addons/libs/stats.module.js';

const stats = new Stats();
document.body.appendChild(stats.dom);

动画循环里每帧调用 update():

function animate() {
  requestAnimationFrame(animate);
  stats.update();
  renderer.render(scene, camera);
}
animate();

左上角出现一个面板,点击可切换三种显示:FPS 帧率、MS 每帧耗时、MB 内存占用。帧率持续低于 60,说明场景性能有问题,就该用第 37 章的手段优化了。

Note

stats.dom 是一个 div,默认固定在左上角。它只是显示工具,不影响场景。发布前记得把它从页面移除。

lil-gui:可视化调参

参数反复试,改代码重刷新太慢。lil-gui 把变量变成面板上的滑块和开关,拖动实时生效。它是 dat.GUI 的继承者,API 几乎一样,three.js 官方示例已经全面改用 lil-gui。

引入方式同样走 addons:

import { GUI } from 'three/addons/libs/lil-gui.module.min.js';

const gui = new GUI();

gui.add(对象, 属性名) 把任意对象的属性挂到面板。布尔值自动变成开关,数字自动变成滑块:

const params = {
  wireframe: false,
  rotationSpeed: 1
};

gui.add(params, 'wireframe');                 // 开关
gui.add(params, 'rotationSpeed', 0, 5, 0.1);  // 滑块:0~5,步进 0.1

add 的第三、四、五个参数分别是 min、max、step,等价于链式写法:

gui.add(params, 'rotationSpeed')
  .min(0).max(5).step(0.1)
  .name('旋转速度'); // 面板上显示的名字

除了滑块和开关,下拉选择也很常用:给 add 传一个数组,面板自动变成下拉框:

const params = { theme: '默认' };
gui.add(params, 'theme', ['默认', '暖色', '冷色']);

onChange 在值变化时回调,最适合联动场景对象。颜色用 addColor:

const material = new THREE.MeshStandardMaterial({ color: 0x00d4ff });
const params = { color: 0x00d4ff };

gui.addColor(params, 'color').onChange((value) => {
  material.color.set(value);
});

滑块松手时才触发的回调用 onFinishChange,适合做”拖完再执行”的重操作。面板不用了调用 gui.destroy() 清理。

Note

lil-gui 回调里拿到的是新值。赋给材质颜色前要过一遍 material.color.set(value),不要直接 material.color = value,那是类型不匹配的常见错误。

变量多了用文件夹分组,面板不混乱:

const folder = gui.addFolder('相机');
folder.add(camera.position, 'x', -10, 10, 0.1);
folder.add(camera.position, 'y', -10, 10, 0.1);
folder.open(); // 默认展开

坐标轴:AxesHelper

新场景最容易犯的错:分不清哪条轴是 X。AxesHelper 直接画三条轴,一眼看穿:

const axes = new THREE.AxesHelper(5); // 半轴长度 5
scene.add(axes);

X 轴红色、Y 轴绿色、Z 轴蓝色,与 three.js 文档配色一致。想自定义颜色,用 setColors(x, y, z)。

网格地面:GridHelper

GridHelper 画网格地面,判断物体位置和”地平线”非常直观:

const grid = new THREE.GridHelper(20, 20, 0x444444, 0x888888);
scene.add(grid);

四个参数:尺寸、格数、中心线颜色、网格线颜色。默认在 XZ 平面上,改 position 可以挪到任何高度。

所有 Helper 都是普通 3D 对象,不想看时不用从场景移除,直接关掉显示即可:

grid.visible = false; // 关掉网格,随时再打开

方向箭头:ArrowHelper

ArrowHelper 画带箭头的方向线,调试速度、力的方向、朝向这类向量:

const dir = new THREE.Vector3(1, 0, 0).normalize(); // 方向必须先归一化
const arrow = new THREE.ArrowHelper(
  dir,                          // 方向
  new THREE.Vector3(0, 0, 0),   // 起点
  2,                            // 长度
  0xff0000                      // 颜色
);
scene.add(arrow);

方向向量不归一化,箭头长度会失真。setDirection / setLength / setColor 可以后期改。

包围盒:BoxHelper

BoxHelper 画出物体的世界轴对齐包围盒,检查物体实际占多大、有没有超出边界:

const box = new THREE.BoxHelper(cube, 0xffff00);
scene.add(box);

物体移动或缩放后,需要更新。渲染循环里每帧调用 update() 最省心:

function animate() {
  requestAnimationFrame(animate);
  box.update();
  renderer.render(scene, camera);
}
animate();
Note

BoxHelper 的包围盒是”世界轴对齐”的,物体旋转时盒子会变大包住旋转后的范围,这是正常现象,不是 bug。

视锥体:CameraHelper

CameraHelper 画出相机的视锥体——相机实际能看到的空间范围:

const camHelper = new THREE.CameraHelper(camera);
scene.add(camHelper);

相机参数(fov、near、far)或位置变化后,调用 camHelper.update() 刷新。“物体为什么看不见”的排查,看视锥体一眼就明白:物体在锥体外,当然看不到。

灯光 Helper

灯光位置看不见,调试很头疼。对应的 Helper 把灯光画出来:

scene.add(new THREE.PointLightHelper(pointLight, 0.5));    // 小球
scene.add(new THREE.DirectionalLightHelper(dirLight, 2));  // 方向线
scene.add(new THREE.SpotLightHelper(spotLight));           // 锥体

灯光移动后同样要调 update()。类似的还有 PlaneHelper、HemisphereLightHelper、PolarGridHelper、VertexNormalsHelper(addons 里)等,用到时查官方文档即可。

Tip

Helper 的 update 规律很统一:被观察的对象会动,就每帧 update();静态的,创建一次就行。记不住就全部每帧 update,代价只是几次矩阵计算。

综合示例

把三件套组合起来:Stats 监控帧率,lil-gui 调立方体的颜色、旋转速度、线框开关,AxesHelper + GridHelper 打底,BoxHelper 显示包围盒:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8" />
  <title>调试三件套</title>
  <style>
    * { margin: 0; padding: 0; }
    html, body { height: 100%; }
    canvas { display: block; }
  </style>
  <script type="importmap">
  {
    "imports": {
      "three": "https://cdn.jsdelivr.net/npm/three@0.185.0/build/three.module.js",
      "three/addons/": "https://cdn.jsdelivr.net/npm/three@0.185.0/examples/jsm/"
    }
  }
  </script>
</head>
<body>
  <script type="module">
    import * as THREE from 'three';
    import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
    import Stats from 'three/addons/libs/stats.module.js';
    import { GUI } from 'three/addons/libs/lil-gui.module.min.js';

    const scene = new THREE.Scene();
    scene.background = new THREE.Color(0x1a1a2e);

    const camera = new THREE.PerspectiveCamera(
      60, window.innerWidth / window.innerHeight, 0.1, 100
    );
    camera.position.set(4, 3, 6);

    const renderer = new THREE.WebGLRenderer();
    renderer.setSize(window.innerWidth, window.innerHeight);
    renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
    document.body.appendChild(renderer.domElement);

    const controls = new OrbitControls(camera, renderer.domElement);
    controls.enableDamping = true;

    // 场景内容
    const cube = new THREE.Mesh(
      new THREE.BoxGeometry(1.5, 1.5, 1.5),
      new THREE.MeshStandardMaterial({ color: 0x00d4ff })
    );
    scene.add(cube);

    const light = new THREE.DirectionalLight(0xffffff, 3);
    light.position.set(5, 10, 5);
    scene.add(light);

    // 调试工具:Stats
    const stats = new Stats();
    document.body.appendChild(stats.dom);

    // 调试工具:Helper
    scene.add(new THREE.AxesHelper(3));
    scene.add(new THREE.GridHelper(10, 10));
    const box = new THREE.BoxHelper(cube, 0xffff00);
    scene.add(box);

    // 调试工具:lil-gui
    const params = {
      color: 0x00d4ff,
      rotationSpeed: 1,
      wireframe: false
    };

    const gui = new GUI();
    gui.addColor(params, 'color').onChange((value) => {
      cube.material.color.set(value);
    });
    gui.add(params, 'rotationSpeed', 0, 5, 0.1).name('旋转速度');
    gui.add(params, 'wireframe').onChange((value) => {
      cube.material.wireframe = value;
    });

    function animate() {
      requestAnimationFrame(animate);
      cube.rotation.y += 0.01 * params.rotationSpeed;
      controls.update();
      box.update(); // 包围盒跟随物体
      stats.update();
      renderer.render(scene, camera);
    }
    animate();
  </script>
</body>
</html>

运行后:左上角是帧率面板,右上角是参数面板。拖动滑块,立方体旋转速度实时变化;切换 wireframe,线框立即出现。所有调试信息都可见、可调,不用再改代码刷页面。

控制台调试技巧

除了面板,浏览器控制台也能高效调试 three.js。

对象直接 console.log 会输出一大坨,用 console.dir 看属性列表更清楚:

console.dir(cube); // 展开查看 position、rotation、material 等

更实用的一招:把关键对象挂到 window 上,控制台里随时操作:

window.cube = cube;
window.scene = scene;

然后控制台里输入 cube.position.y = 3,配合渲染循环,物体立刻移动。改属性比改代码快得多,适合临时实验。

renderer.info:渲染统计

渲染器自带统计信息,不用装任何库:

console.log(renderer.info);
// render: { calls, triangles, ... }
// memory: { geometries, textures }
// programs: n
  • render.calls:一帧的 draw call 数,越少性能越好
  • render.triangles:一帧绘制的三角形总数
  • memory.geometries / textures:几何体和纹理数量,排查内存泄漏时盯着它

性能排查时,把这几项和 Stats 的帧率对照着看,问题出在渲染还是脚本,一目了然。

DevTools 里看材质与几何体

想查画面上某个物体的材质参数,控制台操作最快。先把对象暴露出来:

window.cube = cube;

然后在控制台输入 cube.material,展开就能看到 color、roughness、metalness 等所有参数。直接赋值也能实时生效:

cube.material.roughness = 0.2;
cube.material.color.set('#ff0000');

配合渲染循环,改动立刻反映到画面。怀疑几何体有问题时,检查 cube.geometry.attributes.position 的 count,和预期顶点数对比。这些操作不需要任何库,纯浏览器能力。

其他调试手段

  • debugger 语句:代码里写 debugger 会触发断点,配合浏览器 DevTools 单步执行,比 console.log 更细
  • DevTools Performance 面板:分析 JS 主线程瓶颈,定位是脚本慢还是绘制慢
Tip

Helper 对象会真实参与渲染,发布前记得移除,或只在开发环境(比如用环境变量判断)下添加。lil-gui 和 Stats 同理,它们属于开发期工具,不属于最终产品。