【ThreeJS调试技巧】那些让 Bug 无所遁形的“脏套路”

前言

有些 Bug 不报错、不崩溃,就静静在那儿恶心你

做 Web 3D 开发最头疼的是什么?

不是编译报错,不是性能瓶颈,而是那种画面看起来不对劲,但代码没任何错误的视觉 Bug。

模型缺了一半、纹理糊成一团、颜色阴阳脸、设备半透明像鬼影......这些 Bug 不会让控制台飘红,不会让页面崩溃,就静静在那儿恶心你

更气人的是,很多时候你盯着看半天,死活找不到原因。改几行代码试试?Bug 还在。回退版本?Bug 还在。重装依赖?Bug 还在。

到最后你甚至开始怀疑:是不是显卡坏了?

今天就来聊聊,我这两年攒下来的调试视觉 Bug 的脏套路。不求优雅,只求让 Bug 现原形。


场景一:模型"缺胳膊少腿" ------ 面去哪儿了?

症状

模型加载完,转一圈看,有些面没了。机械臂少个夹爪、设备缺个盖子,像被切掉了一样。

排查思路

第一反应:模型导错了?用建模软件打开原文件,好好的。

第二反应:代码里隐藏了?搜 visible = false,没有。

脏套路一:强制双面渲染

javascript 复制代码
// 调试代码:暴力解决
scene.traverse((child) => {
  if (child.isMesh) {
    child.material.side = THREE.DoubleSide; // 两面都给我渲染
  }
});

消失的面出现了!

这说明什么问题?面法线反了 。正常应该是正面朝外,但模型里有些面是正面朝里。Three.js 默认只渲染正面(FrontSide),朝里的面直接忽略。

根治方案

javascript 复制代码
// 不能留 DoubleSide,性能扛不住
// 正确做法:加载时修正法线方向
loader.load('model.glb', (gltf) => {
  gltf.scene.traverse((child) => {
    if (child.isMesh) {
      child.geometry.computeVertexNormals(); // 重新计算法线方向
    }
  });
});

注意 :有些模型是艺术家手调的法线,computeVertexNormals 可能会破坏原有效果。如果修正后出现硬边或光影异常,还是得回源头改模型。


场景二:设备"鬼影重重" ------ 半透明叠加

症状

某个设备变得半透明,后面的物体清晰可见,像开了透视挂。而且一旦变透明,就再也恢复不了正常。

排查思路

检查代码,发现之前加了一个"高亮选中设备"的功能:

javascript 复制代码
function highlightDevice(device) {
  device.material.transparent = true;
  device.material.opacity = 0.8;
  device.material.emissive.setHex(0xffaa00);
}

高亮完恢复了吗?恢复了。但问题出在:transparent 一旦设为 true,即使改回 opacity = 1,混合模式还开着

脏套路二:重置材质

javascript 复制代码
// 暴力恢复:新建材质
function resetMaterial(mesh) {
  const oldMat = mesh.material;
  mesh.material = new THREE.MeshStandardMaterial({
    map: oldMat.map,
    color: oldMat.color,
    // ... 复制其他属性
  });
  oldMat.dispose(); // 记得释放
}

设备恢复正常。

根本原因:transparent状态 ,不是操作 。你打开它,GPU 就切换渲染管线;关掉 opacity 没用,得彻底关掉 transparent


场景三:物体"一闪一闪" ------ 深度冲突

症状

两个物体挨在一起,交接处出现闪烁的白线画面抖动,转视角时尤其明显。

排查思路

第一反应:性能问题?帧率稳定 60。

第二反应:光照问题?动画循环里有光源变化?没有。

第三反应:检查两个物体的位置。

脏套路三:微调位置

javascript 复制代码
// 检查两个物体的位置关系
console.log(objectA.position.z, objectB.position.z);
// 输出:0, 0

// 果然,两个面完全重合
// 微调其中一个的高度
objectB.position.z += 0.01;

不闪了。

原理:两个面完全重合,GPU 不知道谁前谁后,每帧随机决定谁在上面,看起来就是闪烁。稍微错开一点,深度测试就老实了。

进阶方案

如果必须完全重合(比如地面上铺的网格和地面本身):

javascript 复制代码
// 调整渲染顺序
ground.renderOrder = 0;
grid.renderOrder = 1;

// 或者关闭一个的深度写入
grid.material.depthWrite = false;

场景四:纹理"糊成一团" ------ mipmap 策略不当

症状

纹理明明分辨率很高,但在屏幕上就是模糊的色块,细节全无。

排查思路

纹理分辨率 2048x2048,不小。各向异性过滤开了 16,最大了。mipmap 也正常。问题在哪儿?

脏套路四:强制用最近过滤

javascript 复制代码
// 调试代码:临时替换过滤方式
texture.magFilter = THREE.NearestFilter; // 最近点采样,清晰但锯齿严重

纹理瞬间清晰了!但锯齿出来了。

问题找到了:纹理在屏幕上的实际显示尺寸,比原始纹理小太多。GPU 用 mipmap 选了低精度层,导致模糊。

根治方案

javascript 复制代码
// 方案一:限制最小 mipmap 层级
texture.minFilter = THREE.LinearMipmapLinearFilter;
texture.anisotropy = 16; // 已经开了

// 方案二:如果摄像机永远不会靠近,关掉 mipmap
texture.generateMipmaps = false;
texture.minFilter = THREE.LinearFilter;

方案二适合固定视角的监控画面,方案一适合需要远近切换的场景。


场景五:颜色"阴阳脸" ------ 法线方向混乱

症状

同样的模型、同样的材质、同样的光照,左右两边颜色不一样。一边偏暖,一边偏冷,像阴阳脸。

排查思路

光照一样,材质一样,位置对称。邪门了。

脏套路五:检查法线

javascript 复制代码
// 临时显示法线方向
scene.overrideMaterial = new THREE.MeshNormalMaterial();

画面变成五彩斑斓的"法线可视化"。左右对比,发现问题:左边模型的法线方向乱了,有些面朝左,有些面朝右,光照计算自然就歪了。

根治

重新导入模型,检查建模软件里的法线方向。如果是复制过程中产生的错误,可以:

javascript 复制代码
// 尝试重新计算法线
mesh.geometry.computeVertexNormals();

但同样要注意:如果模型是手调法线,重新计算可能会破坏原有光影效果。


场景六:文字"模糊不清" ------ CanvasTexture 更新失败

症状

用 Canvas 生成动态纹理(比如仪表盘数字),第一次显示正常,后面数字变了,但纹理还是旧的。

排查思路

检查代码,Canvas 确实重绘了,纹理也调用了 needsUpdate。为什么没变?

脏套路六:强制清空再更新

javascript 复制代码
// 错误示范
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillText(newValue, 100, 100);
texture.needsUpdate = true; // 有时候不灵

// 脏套路:重新设置整个 canvas
function updateCanvasTexture(texture, newValue) {
  const canvas = texture.image;
  const ctx = canvas.getContext('2d');
  
  // 1. 清空
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  
  // 2. 画新内容
  ctx.fillStyle = '#ffffff';
  ctx.font = 'bold 48px Arial';
  ctx.fillText(newValue, 100, 100);
  
  // 3. 脏套路:重新设置 image 并强制更新
  texture.image = canvas; // 重新赋值触发更新
  texture.needsUpdate = true;
  
  // 4. 如果还不行,重建纹理
  // const newTexture = new THREE.CanvasTexture(canvas);
  // mesh.material.map = newTexture;
  // oldTexture.dispose();
}

原理 :CanvasTexture 底层缓存机制有时候会"偷懒",重新赋值 image 能强制它刷新。


场景七:模型"位置错乱" ------ 矩阵没更新

症状

InstancedMesh 或者手动修改了 matrix,但模型位置没变,或者位置乱飞。

排查思路

检查代码,确实调用了 setMatrixAt,也调用了 instanceMatrix.needsUpdate = true。为什么不动?

脏套路七:检查矩阵标志位

javascript 复制代码
// 常见错误
instancedMesh.setMatrixAt(index, matrix);
// 忘了设置 needsUpdate

// 正确写法
instancedMesh.setMatrixAt(index, matrix);
instancedMesh.instanceMatrix.needsUpdate = true;

// 脏套路:如果还不动,试试强制重新上传
instancedMesh.instanceMatrix.array.set(matrix.elements, index * 16);
instancedMesh.instanceMatrix.needsUpdate = true;
instancedMesh.computeBoundingSphere(); // 有时候需要这个

进阶排查

javascript 复制代码
// 打印矩阵看是否真的写进去了
const testMatrix = new THREE.Matrix4();
instancedMesh.getMatrixAt(0, testMatrix);
console.log(testMatrix); // 跟预期的一样吗?

// 检查 usage
console.log(instancedMesh.instanceMatrix.usage); // 应该是 DynamicDrawUsage 或 StaticDrawUsage

调试工具包:三板斧让 Bug 现形

遇到视觉 Bug,别急着改代码,先用这三板斧让问题显形

1. 材质覆盖大法

javascript 复制代码
// 强制所有模型用一种材质,排除材质差异
scene.overrideMaterial = new THREE.MeshBasicMaterial({ color: 0xff00ff });

// 查看法线方向
scene.overrideMaterial = new THREE.MeshNormalMaterial();

// 查看 UV 分布
scene.overrideMaterial = new THREE.MeshBasicMaterial({
  map: new THREE.CanvasTexture(uvGridCanvas) // 自定义 UV 网格
});

// 查看深度
scene.overrideMaterial = new THREE.MeshDepthMaterial();

2. 线框透视

javascript 复制代码
// 显示线框,看结构是否完整
scene.traverse((child) => {
  if (child.isMesh) {
    child.material.wireframe = true;
  }
});

3. 包围盒可视化

javascript 复制代码
// 查看模型的包围盒是否准确
import { BoxHelper } from 'three/examples/jsm/helpers/BoxHelper.js';

const boxHelper = new BoxHelper(scene, 0xff0000);
scene.add(boxHelper);

// 查看摄像机视锥
import { CameraHelper } from 'three';
const cameraHelper = new CameraHelper(camera);
scene.add(cameraHelper);

4. 光源可视化

javascript 复制代码
// 查看光源位置和方向
import { PointLightHelper, DirectionalLightHelper, SpotLightHelper } from 'three';

scene.traverse((child) => {
  if (child.isLight) {
    let helper;
    if (child.isPointLight) helper = new PointLightHelper(child, 1);
    if (child.isDirectionalLight) helper = new DirectionalLightHelper(child, 1);
    if (child.isSpotLight) helper = new SpotLightHelper(child);
    if (helper) scene.add(helper);
  }
});

调试心法:让 Bug 现形的五个问题

遇到 Bug 时,按顺序问自己这五个问题,90% 的情况都能找到线索:

1. 是所有的模型都这样,还是只有特定的?

  • 全都这样 → 可能是全局设置(光照、渲染器、后期)
  • 只有某个 → 查那个模型的材质、几何体、矩阵

2. 是固定出现,还是转视角才出现?

  • 固定出现 → 纹理、材质、模型本身的问题
  • 转视角出现 → 法线、视锥裁剪、深度测试的问题

3. 是加载完就这样,还是运行后才出现?

  • 加载完就这样 → 模型导出、加载解析的问题
  • 运行后才出现 → 动画、交互、内存泄漏的问题

4. 是只有这个设备这样,还是所有设备?

  • 只有某台电脑 → 显卡驱动、浏览器版本、硬件兼容性
  • 所有设备都这样 → 代码逻辑问题

5. 去掉这个模型/材质/纹理,Bug 还在吗?

  • 不在了 → 问题就出在去掉的东西上
  • 还在 → 继续二分法排查

终极脏套路:二分法注释

如果实在找不到原因,上最原始的二分法

  1. 注释掉一半场景
  2. Bug 还在吗?
    • 在 → 问题在前一半
    • 不在 → 问题在后一半
  3. 继续二分,直到锁定到具体某个模型或某行代码

这方法虽然笨,但绝对有效。有时候 Bug 找不到,是因为你太相信自己的直觉,而不是相信数据。


总结

视觉 Bug 调试的核心思路就一条:剥离所有"美化",看最原始的数据

  • 颜色不对?用法线材质看方向
  • 位置不对?用线框看结构
  • 纹理糊了?用 NearestFilter 看原始分辨率
  • 闪烁?检查深度冲突
  • 半透明?重置材质

把这些"脏套路"走一遍,90% 的 Bug 都能现原形。

剩下的 10% 怎么办?那就得动真正的"脏套路"了 ------ 比如把同事叫过来一起盯着看,两个人一起怀疑人生,Bug 往往就自己跑了。。。😆


互动

你在项目里遇到过什么"诡异的视觉 Bug"?最后怎么解决的?评论区分享出来,咱们一起"驱鬼" 😏

下篇预告:【Three.js 性能分析】从 Draw Call 到显存占用,一张表看懂瓶颈在哪

相关推荐
叶智辽1 天前
【ThreeJS急诊室】一个生产事故:我把客户的工厂渲染“透明”了
webgl·three.js
AI能见度1 天前
硬核:如何用大疆 SRT 数据实现高精度 AR 视频投射?
ar·无人机·webgl
EQ-雪梨蛋花汤2 天前
【踩坑记录】使用 Layui 框架时解决 Unity WebGL 渲染在 Tab 切换时黑屏问题
unity·layui·webgl
ct9782 天前
ThreeJs材质、模型加载、核心API
webgl·材质·threejs
爱看书的小沐6 天前
【小沐杂货铺】基于Three.js渲染三维无人机Drone(WebGL / vue / react )
javascript·vue.js·react.js·无人机·webgl·three.js·drone
小猫咪yi9 天前
1、绘制点
webgl
小猫咪yi12 天前
7、三角形旋转
webgl
小猫咪yi13 天前
3、绘制线
webgl
小猫咪yi13 天前
6、drawElements绘制多个三角形
webgl