最近在用 Three.js 写一个物理碰撞仿真的项目,为了让小球看起来更有质感,我给它们加上了不同的材质贴图(比如台球、网球、黏土)。
本来一切都很顺利,直到我把视角拉近,差点没被屏幕上的画面丑哭------小球的表面竟然出现了一块块诡异的黑色斑块!


折腾了大半天,翻了不少文档,终于把这两个隐藏极深的渲染底层机制给挖出来了。今天就来复盘一下,希望能帮到遇到同样问题的同学。
坑位一:极点的放射状黑斑
一开始,我发现小球的顶部和底部(也就是南北极)有一团放射状的死黑色斑块。
怎么产生的?
这其实是 3D 图形学里非常经典的**"极点奇点(Pole Singularity)"**问题。
想象一下我们是怎么给地球仪贴地图的:把一张长方形的世界地图卷成圆筒,然后把顶部边缘用力"捏"成一个点(北极)。
在 3D 模型里也是一样的。小球顶部的所有三角形,它们的顶点物理坐标全都挤在同一个位置(比如 X=0, Y=10, Z=0),但是在 2D 贴图上,它们的 UV 坐标(横向的 U)却是从 0 到 1 散开的。
我的代码里手贱加了这么一行:
scss
geo.computeTangents(); // 计算切线
这行代码的作用是计算表面纹理的走向。它的底层数学逻辑大概是:3D物理移动距离 / UV贴图移动距离。
但在极点上,3D物理移动距离是 0 !这就导致了数学上的"除以零"错误,算出来一堆 NaN(无效数字)。当显卡拿到这堆乱码去计算光照时,直接罢工,渲染出来的就是一团死黑。
怎么解决?
其实如果你的材质只是用了普通的凹凸贴图(bumpMap),Three.js 底层的着色器完全可以自己通过导数算出来凹凸感,根本不需要预先计算切线 。
解决办法极其简单:直接删掉 geo.computeTangents(); 这行代码。
坑位二:越拉远越明显的方形黑斑
删掉切线计算后,极点的黑斑确实没了。但我一缩放画布,又发现了新问题:当鼠标把画面缩小(镜头拉远)时,小球表面又出现了随机的方形黑斑,而且越缩小黑斑越大!
怎么产生的?
这个现象非常关键,它直接指向了 3D 渲染的另一个底层机制:Mipmap(多级渐远贴图) (Mi ni P icture Map)。
为了优化性能,当我们把一张 512x512 的贴图贴在模型上,且镜头拉得很远时,显卡不会去傻傻地读原图,而是会自动生成一系列缩略图(256x256、128x128...直到 1x1)。
我的贴图是用 Canvas 动态生成的,为了模拟"黏土"的粗糙感,我在 Canvas 里用 Math.random() 画了几万个 1.5 像素的微小噪点。
问题就出在这里:这种极其密集的、高频的随机噪点,在被显卡强行压缩成缩略图时,算法溢出了! 压缩算法崩溃导致缩略图里生成了纯黑色的"死像素"。
当你拉远镜头时,引擎正好调用了那张坏掉的缩略图,那个微小的黑点被贴在小球上,就放大了成你看到的巨大黑色方块。
怎么解决?
既然我们的场景不需要看几公里外的小球,最暴力的解决办法就是:禁用这几张贴图的 Mipmap 生成。
在生成 Texture 的代码最后,加上这两行:
ini
const texture = new THREE.CanvasTexture(canvas);
// 1. 告诉显卡:不要帮我生成缩略图了!
texture.generateMipmaps = false;
// 2. 告诉显卡:当物体缩小时,直接去原图上线性取样就行。
texture.minFilter = THREE.LinearFilter;
注意,minFilter 这一行必须加。因为 Three.js 默认的缩小滤镜是去寻找缩略图的,既然我们关了缩略图,如果不改滤镜,显卡找不到缩略图就会直接让整个模型变黑。

总结
搞 3D 前端开发,很多时候遇到视觉 Bug,往往不是业务逻辑写错了,而是触发了图形学底层的数学边界(比如除以零、高频噪点采样崩溃)。
总结一下今天的两个经验:
- 慎用
computeTangents():除非你用了法线贴图(Normal Map)且发现光照不对,否则不要随便调这个方法,极容易在球体极点引发黑斑。 - 警惕代码生成的噪点贴图 :如果用 Canvas 绘制了高频噪点作为贴图,记得关掉
generateMipmaps,否则缩放时大概率会糊掉或者出现黑块。
希望这篇文章能帮大家少走点弯路。如果你也遇到过 Three.js 里奇奇怪怪的渲染问题,欢迎在评论区一起吐槽交流~