深入分析Three.js场景中添加光照后模型全黑或阴影不显示的根本原因,涵盖材质受光机制、物理光度单位、法线计算、阴影相机视锥体匹配及模型层级遍历等排查方法与完整解决方案。
Three.js 添加光照后模型全黑或阴影不显示的原因
时间:2026-11-05
浏览:461
文章目录
一、光照与阴影渲染链路总览
初学者在遇到“黑模”与“无阴影”时,往往习惯盲目调节光源位置或反复修改强度。实际上,Three.js 的着色过程与阴影生成属于 WebGL 管线中两个相互配合却机制迥异的阶段:光照计算负责决定每个顶片元的受光色彩与明暗过渡,而阴影则依赖深度缓冲贴图(Depth Shadow Map)进行二次遮挡检测。
排查时必须首先将两者剥离:如果模型本身能看清明暗关系但地面没有影子,问题仅存在于阴影链路;如果连模型自身都是一团死黑,则必须先解决几何体与光照的受光计算,再进入阴影调试。
二、模型添加光照后全黑的核心原因与解决方法
在默认没有光源时,使用 MeshBasicMaterial 的模型是能看见纯色的;但一旦换用物理着色材质并引入光源,模型可能立刻变黑。这种现象常见于以下四个具体环节:
1. 材质不支持光照或 PBR 材质高金属度缺少环境贴图
原因解释:不同材质类对光的响应策略完全不同。MeshBasicMaterial 完全不参与任何漫反射与高光运算,如果材质设置了纯黑或未正确绑定贴图,它永远是纯黑的。反之,若换成了 MeshStandardMaterial 或 MeshPhysicalMaterial(基于物理渲染的 PBR 材质),在设置了 metalness = 1.0(完全金属)且 roughness = 0.0(极光滑镜面)时,物体表面只会反射周围环境。如果场景没有配置 scene.environment 或材质缺少 envMap,反射计算只能抓取到黑色的虚空背景,即便旁边放了一盏很亮的平行光,模型依然呈现死黑镜面。
具体做法:对于普通物体,将 metalness 下调至 0.1 ~ 0.2,roughness 保持在 0.5 左右以观察漫反射响应;若确实需要高金属质感,必须引入 HDR 环境贴图:
import { RGBELoader } from 'three/examples/jsm/loaders/RGBELoader.js';
// 加载环境反射贴图以提供 PBR 漫反射与镜面反射源
new RGBELoader().load('textures/environment.hdr', (texture) => {
texture.mapping = THREE.EquirectangularReflectionMapping;
scene.environment = texture; // 全局提供给所有 PBR 材质
});
2. Three.js r155+ 启用真实物理光度单位导致光强不足
原因解释:Three.js 在 r155 版本后默认开启了基于物理的光照度系统(Physically Correct Lighting)。在早期的老版本教程中,点光源或平行光的 intensity 通常设置为 1 或 2。但在现代版本中,光强单位换算成了真实的流明(Lumen)与坎德拉(Candela),一个标准的点光源可能需要 800 ~ 2000 流明才能照亮周围几米的物体;平行光也往往需要 3 ~ 5 甚至更高才能达到预期的日光效果。沿用旧教程数值会导致模型处在极暗的弱光中,肉眼观测就像全黑。
具体做法:大幅增加光源强度,同时在场景中添加一个基础环境光(AmbientLight)作为底色支撑,排除方向光死角:
// 基础漫反射环境光,防止暗部出现死黑
const ambientLight = new THREE.AmbientLight(0xffffff, 0.8);
scene.add(ambientLight);
// 主方向光,根据场景尺寸调整合适强度
const dirLight = new THREE.DirectionalLight(0xffffff, 3.5);
dirLight.position.set(10, 20, 10);
scene.add(dirLight);
3. 几何体法线丢失或法线方向反转(Inverted Normals)
原因解释:现代着色器计算漫反射时,依赖光照向量 \(L\) 与顶点法线向量 \(N\) 的点积结果(即 \(\max(N \cdot L, 0)\))。如果从 Blender、Maya 或通过 OBJ/GLTF 导出的模型在导出时未勾选“导出法线”,或者在三维建模时法线翻转(朝向几何体内部),着色器会判定当前视线所见的外表面全部为背光面,计算结果直接输出 0(黑色)。
验证与修复:通过将材质临时换成 new THREE.MeshNormalMaterial() 验证。如果替换后模型依然全黑或颜色斑驳翻转,证明法线存在致命错误。可通过代码命令强制重新计算几何体顶点法线,或者临时开启双面着色:
// 重新生成平滑法线向量
geometry.computeVertexNormals();
// 或者为材质开启双面渲染进行对比验证
material.side = THREE.DoubleSide;
4. 光源位置、朝向与衰减距离配置偏差
原因解释:对于 PointLight 或 SpotLight,其构造函数包含 distance(最大照射距离)和 decay(衰减因子,默认值为 2,符合真实物理平方反比衰减)。如果模型的距离超过了 distance,光强在到达模型前就衰减为 0。另外,聚光灯与平行光的 target 属性决定了光线的射向,若 target 未加入场景或默认停留在原点 (0, 0, 0),而模型被放置在别处,光束就会完全打空。
具体做法:若无需距离衰减,将 distance 设为 0;使用聚光灯时,务必将 light.target 显式指定到模型的坐标上,并将 target 添加至场景:
const spotLight = new THREE.SpotLight(0xffffff, 5);
spotLight.position.set(0, 15, 10);
spotLight.distance = 0; // 0 表示光线不会根据距离衰减到无
spotLight.target = myModel; // 让聚光灯追踪模型
scene.add(spotLight.target);
scene.add(spotLight);
三、阴影完全不显示的排查链条与关键节点
阴影在 WebGL 中是通过“渲染器从光源视角先渲染一张深度图,再与主相机片元进行深度比对”来实现的。这是一条脆弱的链路,任何一个开关缺失、视锥体尺寸不符或模型层级属性未继承,都会导致阴影整体消失。
1. 渲染器阴影映射总开关未开启(Step 01)
为了保证移动端性能,Three.js 的 WebGLRenderer 默认关闭了阴影贴图生成。必须显式设置:
renderer.shadowMap.enabled = true;
// 推荐启用柔和阴影类型以获得更高质量边缘
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
2. 光源类型错误或未开启投射(Step 02)
只有具有确定方向或发散源的点状/面状光源才具备投射阴影的几何条件。AmbientLight(环境光)与 HemisphereLight(半球光)在数学上是没有投影概念的,它们没有 shadow 属性。支持阴影的仅有:
DirectionalLight(平行光,模拟太阳光,生成正交投影)SpotLight(聚光灯,模拟手电筒/射灯,生成透视投影)PointLight(点光源,模拟灯泡,生成全向六面体立方体投影)
选对光源后,必须声明:light.castShadow = true;。
3. 光源阴影相机的视锥体(Shadow Frustum)未包围物体(Step 03,最隐蔽的坑点)
以最常用的平行光 DirectionalLight 为例,它内部挂载了一个正交相机 light.shadow.camera。WebGL 生成的阴影贴图只覆盖该相机视锥体所定义的立方体空间。
代码全都写了,但地面上空空如也,或者只有物体移动到特定小片区域时阴影才突然闪现出来。这是因为默认的正交相机包围盒通常只有 [-5, 5] 的极小范围,如果模型跨度为 20 米,模型的大部分甚至全部都在阴影相机的裁剪面(Clipping Plane)之外,从而在阴影贴图阶段被剔除!
排查利器:使用 CameraHelper。在开发阶段,强烈建议将光源阴影相机的辅助线添加进场景,直接在屏幕上观察金字塔或方框是否把你的模型和地面都罩住了:
// 调整平行光正交相机范围以包裹场景
dirLight.shadow.camera.left = -20;
dirLight.shadow.camera.right = 20;
dirLight.shadow.camera.top = 20;
dirLight.shadow.camera.bottom = -20;
dirLight.shadow.camera.near = 0.5;
dirLight.shadow.camera.far = 100;
dirLight.shadow.mapSize.width = 2048; // 提升阴影贴图分辨率
dirLight.shadow.mapSize.height = 2048;
// 添加辅助观察线
const shadowHelper = new THREE.CameraHelper(dirLight.shadow.camera);
scene.add(shadowHelper);
4. 导入的模型为 Group 容器,未递归遍历 Mesh 节点开启属性(Step 04)
通过 GLTFLoader 或 FBXLoader 加载进来的外部资产,其顶级对象通常是一个 THREE.Group 或 THREE.Object3D。在 Three.js 中,castShadow 和 receiveShadow 属性不会自动向子节点继承!如果直接写 gltf.scene.castShadow = true;,渲染器在遍历网格进行阴影计算时根本识别不到,导致投影完全丢失。
标准做法:使用 traverse 方法遍历模型树下的每一个叶子节点:
loader.load('model.glb', (gltf) => {
const model = gltf.scene;
// 必须遍历每个子网格
model.traverse((child) => {
if (child.isMesh) {
child.castShadow = true; // 允许该几何体投射阴影
child.receiveShadow = true; // 允许该几何体表面接收自遮挡阴影
}
});
scene.add(model);
});
5. 地面承接物材质不支持阴影或未开 receiveShadow(Step 05)
阴影最终需要“绘制”在承接物表面。如果地面误用了 MeshBasicMaterial,由于该材质在着色器阶段直接输出了底色,忽略了光照与阴影测试,投影便无法显现。地面必须采用受光材质,例如 MeshStandardMaterial、MeshLambertMaterial,或者专门用于透明投影合成的 ShadowMaterial:
const planeGeometry = new THREE.PlaneGeometry(100, 100);
// 地面材质必须响应光照
const planeMaterial = new THREE.MeshStandardMaterial({ color: 0xeeeeee });
const plane = new THREE.Mesh(planeGeometry, planeMaterial);
plane.rotation.x = -Math.PI / 2;
plane.receiveShadow = true; // 开启接收阴影
scene.add(plane);
四、完整可运行代码示例(阴影与光照最小闭环)
下面是一个排除了上述所有隐患的完整标准场景模板,包含渲染器设置、光源调校、阴影相机范围、模型递归与地面承接:
import * as THREE from 'three';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
// 1. 初始化场景与渲染器
const scene = new THREE.Scene();
scene.background = new THREE.Color(0xf0f0f0);
const camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(10, 15, 20);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
// 【关键步骤 1】:开启阴影映射
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
document.body.appendChild(renderer.domElement);
const controls = new OrbitControls(camera, renderer.domElement);
// 2. 光源配置
// 基础补光:防止阴影面和暗面死黑
const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
scene.add(ambientLight);
// 主投影光源:平行光
const dirLight = new THREE.DirectionalLight(0xffffff, 2.5);
dirLight.position.set(15, 25, 10);
// 【关键步骤 2】:光源开启投影
dirLight.castShadow = true;
// 【关键步骤 3】:调校阴影相机范围,防止阴影被裁切
dirLight.shadow.camera.near = 0.5;
dirLight.shadow.camera.far = 60;
dirLight.shadow.camera.left = -15;
dirLight.shadow.camera.right = 15;
dirLight.shadow.camera.top = 15;
dirLight.shadow.camera.bottom = -15;
dirLight.shadow.mapSize.width = 2048;
dirLight.shadow.mapSize.height = 2048;
dirLight.shadow.bias = -0.0005; // 避免阴影摩尔纹(Shadow Acne)
scene.add(dirLight);
// 3. 创建受光物体并投射阴影
const boxGeometry = new THREE.BoxGeometry(3, 3, 3);
// 保证法线正确
boxGeometry.computeVertexNormals();
const boxMaterial = new THREE.MeshStandardMaterial({
color: 0x30363C,
roughness: 0.4,
metalness: 0.1 // 避免高金属度无环境反射变黑
});
const cube = new THREE.Mesh(boxGeometry, boxMaterial);
cube.position.set(0, 3, 0);
// 【关键步骤 4】:网格开启投射
cube.castShadow = true;
cube.receiveShadow = true;
scene.add(cube);
// 4. 创建接收阴影的地面
const floorGeo = new THREE.PlaneGeometry(50, 50);
const floorMat = new THREE.MeshStandardMaterial({ color: 0xe0ded8 });
const floor = new THREE.Mesh(floorGeo, floorMat);
floor.rotation.x = -Math.PI / 2;
// 【关键步骤 5】:地面开启接收
floor.receiveShadow = true;
scene.add(floor);
// 5. 渲染循环
function animate() {
requestAnimationFrame(animate);
cube.rotation.y += 0.01;
controls.update();
renderer.render(scene, camera);
}
animate();
五、快速自查对照与决策树
遇到渲染异常时,无需通篇复查代码,可直接依照下图决策树分步阻断定位:
| 故障表象 | 根本原因 | 排查与校验手段 | 最终修复方案 |
|---|---|---|---|
| 模型全黑(有轮廓无明暗) | 几何体法线缺失或反转 | 替换为 MeshNormalMaterial 观察是否有彩度 |
调用 computeVertexNormals() 或调整导出设置 |
| 模型全黑(金属材质) | 高金属度缺少环境贴图反射源 | 观察 material.metalness 是否接近 1.0 |
挂载 scene.environment HDR 贴图或调低金属度 |
| 场景整体发黑昏暗 | Three.js 现代版本物理光度单位增强 | 确认 Three.js 版本是否大于等于 r155 | 大幅调高 intensity,添加 AmbientLight |
| 模型受光正常,但无任何阴影 | 渲染器阴影总开关未开启 | 打印检查 renderer.shadowMap.enabled |
显式声明 renderer.shadowMap.enabled = true |
| 有光照,但局部阴影缺失或闪烁 | 阴影相机(Shadow Frustum)未包围物体 | 添加 CameraHelper(light.shadow.camera) 观察视锥体框 |
扩大正交相机的 left/right/top/bottom 边界 |
| 外部加载模型无阴影 | 导入容器为 Group,子 Mesh 未配置属性 | 打印模型树类型 console.log(gltf.scene) |
使用 model.traverse() 遍历所有 isMesh 设置 |
当成功调出阴影后,有时会在模型表面看到一层像条纹一样的噪点暗斑。这是深度贴图分辨率有限造成的自遮挡伪影。只需为光源设置微小的阴影偏置即可消除:light.shadow.bias = -0.0001; 或配合 light.shadow.normalBias = 0.02;,切勿将数值设得过大,否则会导致阴影脱离模型底部(悬浮现象,Peter Panning)。
实用工具推荐
绿化、压缩、噪点和虚拟水印,自由调节做旧程度。
选择画质和输出格式,预览压缩前后的大小。