Three.js 添加光照后模型全黑或阴影不显示的原因

作者:灵感回路时间:2026-11-05 23:45:06浏览:458

深入分析Three.js场景中添加光照后模型全黑或阴影不显示的根本原因,涵盖材质受光机制、物理光度单位、法线计算、阴影相机视锥体匹配及模型层级遍历等排查方法与完整解决方案。

Three.js 核心问题排查
分类:WebGL / Three.js 常见故障诊断 阅读时长:约 8 分钟 适用版本:Three.js r120 - r160+
Three.js 添加光照后模型全黑或阴影不显示的原因
在 Three.js 中,添加光源后模型变成纯黑或者地面不投射阴影,并不是渲染器崩溃,而是光照计算管线与阴影贴图流水线中的关键配置发生了断裂。模型全黑通常源于材质不受光、物理光度单位下光强过弱、高金属度材质缺少环境反射源(envMap)或顶点法线缺失/反转;而阴影不显示则必然是因为渲染器全局开关未开、光源不支持投影、阴影相机正交视锥体过小导致截断、未递归遍历模型为子 Mesh 启用投射,或接收面采用了非受光材质。理清这两个独立机制的运作链条,就能在几分钟内准确定位并彻底排除问题。

一、光照与阴影渲染链路总览

初学者在遇到“黑模”与“无阴影”时,往往习惯盲目调节光源位置或反复修改强度。实际上,Three.js 的着色过程与阴影生成属于 WebGL 管线中两个相互配合却机制迥异的阶段:光照计算负责决定每个顶片元的受光色彩与明暗过渡,而阴影则依赖深度缓冲贴图(Depth Shadow Map)进行二次遮挡检测。

排查时必须首先将两者剥离:如果模型本身能看清明暗关系但地面没有影子,问题仅存在于阴影链路;如果连模型自身都是一团死黑,则必须先解决几何体与光照的受光计算,再进入阴影调试。

Three.js 添加光照后模型全黑或阴影不显示的原因
图 1:Three.js 光照与阴影渲染排查总览架构。左侧对应模型黑化受光管线,右侧对应阴影生成与投射管线。

二、模型添加光照后全黑的核心原因与解决方法

在默认没有光源时,使用 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 材质
});
Three.js 添加光照后模型全黑或阴影不显示的原因
图 2:模型受光机制与典型全黑原理分析。展示法线反转导致点积为负、高金属度缺少环境贴图以及着色模型差异的数学与视觉逻辑。

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 中是通过“渲染器从光源视角先渲染一张深度图,再与主相机片元进行深度比对”来实现的。这是一条脆弱的链路,任何一个开关缺失、视锥体尺寸不符或模型层级属性未继承,都会导致阴影整体消失。

Three.js 添加光照后模型全黑或阴影不显示的原因
图 3:Three.js 阴影渲染全链路五步配置流程。必须严格确保 01 至 05 节点全部闭合,才能形成完整的阴影贴图与接收绘制。

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();

五、快速自查对照与决策树

遇到渲染异常时,无需通篇复查代码,可直接依照下图决策树分步阻断定位:

Three.js 添加光照后模型全黑或阴影不显示的原因
图 4:Three.js 光照全黑与阴影故障排查决策树。从现象分流到辅助工具介入,快速定位根因。
故障表象 根本原因 排查与校验手段 最终修复方案
模型全黑(有轮廓无明暗) 几何体法线缺失或反转 替换为 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 设置
额外提醒:避免阴影自遮挡条纹(Shadow Acne)

当成功调出阴影后,有时会在模型表面看到一层像条纹一样的噪点暗斑。这是深度贴图分辨率有限造成的自遮挡伪影。只需为光源设置微小的阴影偏置即可消除:light.shadow.bias = -0.0001; 或配合 light.shadow.normalBias = 0.02;,切勿将数值设得过大,否则会导致阴影脱离模型底部(悬浮现象,Peter Panning)。

作者最新文章
相关文章
更多