【ThreeJS急诊室】一个生产事故:我把客户的工厂渲染“透明”了

前言

事情是这样的。

上周五下午,我美滋滋地喝着咖啡,心想智慧工厂项目一期终于交付了,周末能躺平打游戏。

结果快下班时,企业微信狂震 ------ 客户发来一段视频:

巨大的厂房里,所有设备都变成了半透明,管线像幽灵一样飘在空中,工人都懵了:"这啥情况?闹鬼了?"

我盯着视频看了三秒,脑子嗡的一下 ------ 我把混合模式(Blending)搞砸了

今天不说优化,不说加载,就聊聊那些年我们踩过的 Three.js 渲染"坑"。有些 Bug 不会报错,不会崩溃,只会让你的画面变得诡异无比,然后客户快下班时给你发鬼片....


事故一:所有模型都变"透明"了

现场还原

客户打开页面,正常加载,正常显示。然后他点了一下某个设备 ------ 突然,整个厂房的设备都变成了半透明,像 X 光片一样。

更诡异的是,透明之后再也变不回来了

追凶过程

我远程过去,第一反应是:材质透明度被改了?检查代码:

javascript 复制代码
// 设备点击高亮
function highlightDevice(deviceMesh) {
  deviceMesh.material.transparent = true;
  deviceMesh.material.opacity = 0.5; // 半透明高亮?
}

等等,这逻辑不对啊 ------ 高亮应该是变亮 或者变色,怎么会是变透明?

再往下看,发现了罪魁祸首:

javascript 复制代码
// 之前为了做"呼吸效果",全局改过材质
scene.traverse((child) => {
  if (child.isMesh) {
    child.material.transparent = true; // 😱 这里埋雷了
  }
});

问题出在哪?transparent 属性一旦设为 true,Three.js 就会启用混合模式,但混合模式的默认行为是"半透明叠加" 。当我把所有材质的 transparent 都打开,又没设置正确的混合参数,GPU 就按照"所有物体都是玻璃"的逻辑去渲染,结果就是整个世界都透了。

抢救方案

javascript 复制代码
// ✅ 正确做法:只有需要透明的材质才开 transparent
function highlightDevice(deviceMesh) {
  // 保存原始状态
  const originalTransparent = deviceMesh.material.transparent;
  const originalOpacity = deviceMesh.material.opacity;
  
  // 临时改成高亮色 + 正常不透明
  deviceMesh.material.emissive.setHex(0xff0000);
  
  // 如果要改透明度,一定要配套设置 blending
  // deviceMesh.material.transparent = true;
  // deviceMesh.material.opacity = 0.8;
  // deviceMesh.material.blending = THREE.NormalBlending; // 明确指定混合模式
}

教训transparent 不是开关,是模式切换。乱开的后果就是 ------ 客户半夜找你驱鬼。


事故二:设备"消失"了一半

现场还原

另一个项目,模型加载完,一切正常。但摄像机一转,设备的背面全不见了,像被切掉了一样。

客户:"你们这是 2.5D 模型?省了一半面数?"

我:???

追凶过程

检查模型,没问题。检查材质,没问题。最后在代码里发现了这个:

javascript 复制代码
// 为了性能优化,我加了一行"神代码"
material.side = THREE.FrontSide; // 只渲染正面

这行本身没错,问题是:这个模型有些面片是单面的,摄像机转到背面,自然就看不到了。

但为什么之前没发现?因为之前的场景里,所有模型都是"双面"的,或者摄像机从来没转到背面。

抢救方案

javascript 复制代码
// ✅ 稳妥做法:不确定的情况下用双面
material.side = THREE.DoubleSide;

// 如果担心性能,可以:
// 1. 确定不会看到背面的模型,用 FrontSide
// 2. 可能看到背面的,用 DoubleSide
// 3. 或者用 below 技巧:把"看不到的面"用简单颜色渲染
material.side = THREE.BackSide; // 只渲染背面,配合正面做轮廓描边

更骚的操作:用 material.wireframe 临时看一下,到底哪些面缺失了:

javascript 复制代码
// 调试模式:显示线框,一眼看出是背面没了还是面片本身缺失
material.wireframe = true;

教训side 属性不是性能优化的首选。省这点性能,换来模型"缺胳膊少腿",划不来。


事故三:模型突然"黑化"

现场还原

这是我最懵的一次。

模型加载完,亮堂堂的,一切正常。然后我加了一个点光源,想照亮某个局部 ------ 结果整个模型变黑了,像被泼了墨。

追凶过程

查了两小时,最后发现问题出在 法线(Normal) 上。

javascript 复制代码
// 我加载模型后,顺手做了一件事
geometry.computeVertexNormals(); // 重新计算法线

问题是:这个模型原本的法线是"艺术家 "手调的,有些面故意平滑,有些面故意硬边。我这一 computeVertexNormals,把所有法线都"标准化"了,结果光照计算全错,模型就黑了。

抢救方案

javascript 复制代码
// ✅ 正确做法:除非确定模型法线坏了,否则别动!
// loader.load('model.glb', (gltf) => {
//   gltf.scene.traverse(child => {
//     if (child.isMesh) {
//       // 别动法线!
//       // child.geometry.computeVertexNormals();
//     }
//   });
// });

// 如果真要重新计算,先备份原版
// const originalPositions = geometry.attributes.position.array.slice();
// computeVertexNormals();
// 对比效果,不行就恢复

教训:模型文件里的数据,每一分都有它的道理。别手贱去"优化"你不知道的东西。


事故四:边缘出现诡异白线

现场还原

这个 Bug 特别恶心。

两个模型挨在一起,相接的地方出现了一条细细的白线,时有时无,转视角就变。截图发给客户,客户:"你们模型没拼好吧?"

追凶过程

搜了半天,最后在 StackOverflow 上找到答案:深度测试(depthTest)冲突

两个模型的面完全重合,GPU 不知道谁在前谁在后,就会产生"闪烁"或"白边"。

抢救方案

javascript 复制代码
// 🚫 错误原因:两个面完全重合
// plane1 和 plane2 在同一位置,深度测试打架

// ✅ 方案一:微调位置,避免完全重合
plane2.position.z += 0.01; // 稍微错开

// ✅ 方案二:调整渲染顺序
plane2.renderOrder = 1; // 确保后渲染

// ✅ 方案三:如果必须完全重合,关闭一个的深度写入
plane2.material.depthWrite = false; // 不参与深度测试

教训:GPU 很笨,你让它同时渲染两个一模一样位置的东西,它就给你"抖"给你看。


事故五:纹理突然"糊了"

现场还原

这是我最无语的一次。

模型加载完,纹理高清细腻。运行 10 秒后,纹理突然变糊,像打了马赛克。

追凶过程

排查到最后,发现是 mipmap 的锅。

javascript 复制代码
// 纹理加载
const texture = loader.load('highres.jpg');
texture.generateMipmaps = true; // 默认就是 true
texture.minFilter = THREE.LinearMipmapLinearFilter;

这配置本身没错。问题是:我的摄像机从来没离模型很近过,所以 Three.js 一直在用最小的 mipmap 层级渲染,看起来就是糊的。

抢救方案

javascript 复制代码
// ✅ 方案一:强制用原始纹理
texture.minFilter = THREE.LinearFilter; // 不用 mipmap
texture.generateMipmaps = false;

// ✅ 方案二:调整各向异性过滤,让 mipmap 切换更平滑
texture.anisotropy = 16; // 显卡支持的最大值

// ✅ 方案三:如果是 CanvasTexture,记得设置
texture.needsUpdate = true; // 通知 GPU 纹理变了

教训:mipmap 不是万能的。如果你的模型距离固定,或者不需要远近切换,关掉 mipmap 反而更清晰。


急诊总结

这次"急诊"遇到的 5 个病例,每一个都是真实生产事故:

症状 病因 处方
模型全透明 乱开 transparent 只有透明材质才开,明确 blending
模型缺一半 side 设错 不确定就用 DoubleSide
模型变黑 乱算法线 别动模型原始法线
白线闪烁 深度测试冲突 微调位置或 renderOrder
纹理变糊 mipmap 策略不当 根据场景选择 filter

最后说两句

Three.js 的 API 看起来简单,但每个属性背后都是 GPU 的复杂逻辑。有时候你以为开了一个"开关",实际上改了整个渲染流水线。

这些 Bug 的共同点是:代码没报错,画面出错了。调试起来最痛苦,因为不知道从哪查起。

所以,如果你的项目也出现了诡异画面,先别急着骂显卡,看看是不是动了这几个属性:

  • transparent
  • side
  • computeVertexNormals
  • depthWrite / depthTest
  • minFilter / magFilter

互动

你的 Three.js 项目遇到过什么"灵异事件"?欢迎在评论区分享,让大伙乐乐(顺便避坑) 😏

下篇预告:【ThreeJS调试技巧】那些让 Bug 无所遁形的"脏套路"

相关推荐
AI能见度8 小时前
硬核:如何用大疆 SRT 数据实现高精度 AR 视频投射?
ar·无人机·webgl
EQ-雪梨蛋花汤1 天前
【踩坑记录】使用 Layui 框架时解决 Unity WebGL 渲染在 Tab 切换时黑屏问题
unity·layui·webgl
ct9781 天前
ThreeJs材质、模型加载、核心API
webgl·材质·threejs
爱看书的小沐6 天前
【小沐杂货铺】基于Three.js渲染三维无人机Drone(WebGL / vue / react )
javascript·vue.js·react.js·无人机·webgl·three.js·drone
小猫咪yi8 天前
1、绘制点
webgl
小猫咪yi11 天前
7、三角形旋转
webgl
小猫咪yi12 天前
3、绘制线
webgl
小猫咪yi12 天前
6、drawElements绘制多个三角形
webgl
叶智辽13 天前
【ThreeJS实战】5个让我帧率翻倍的小技巧,不用改模型
性能优化·three.js