调试辅助工具
本教程共 40 篇 · 第 24 篇 · 更新于 2026-08-14 · 约 9 分钟阅读
本节目标:学会用 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 章的手段优化了。
Notestats.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() 清理。
Notelil-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();
NoteBoxHelper 的包围盒是”世界轴对齐”的,物体旋转时盒子会变大包住旋转后的范围,这是正常现象,不是 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 里)等,用到时查官方文档即可。
TipHelper 的 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 主线程瓶颈,定位是脚本慢还是绘制慢
TipHelper 对象会真实参与渲染,发布前记得移除,或只在开发环境(比如用环境变量判断)下添加。lil-gui 和 Stats 同理,它们属于开发期工具,不属于最终产品。