第 10 篇:灯光与阴影
本文是"使用 Three.js 构建转炉数字孪生系统"系列的第 10 篇。
本篇将系统介绍环境光、半球光、方向光、点光源、聚光灯和阴影,并给出适合转炉工业场景的基础布光方案与模型发黑排查流程。
在前面的文章中,我们已经学习了:
text
Geometry
Material
Mesh
PBR 材质
纹理
环境贴图
当模型使用下列受光材质时:
text
MeshLambertMaterial
MeshPhongMaterial
MeshStandardMaterial
MeshPhysicalMaterial
灯光会直接决定模型的:
- 明暗;
- 体积感;
- 高光;
- 金属反射层次;
- 空间前后关系;
- 阴影;
- 高温区域局部照明。
如果灯光配置错误,模型可能出现:
text
模型全黑
模型整体过暗
模型过曝发白
金属看起来像塑料
背光面完全不可见
阴影缺失
阴影只显示一部分
阴影边缘锯齿
阴影漂浮
表面出现黑色斑点
帧率明显下降
因此,工业三维场景的灯光并不是"多加几盏灯就会更好",而是要在材质、方向、阴影范围和性能之间取得平衡。
一、本文要解决的问题
完成本文后,应当能够回答:
- 为什么
MeshBasicMaterial不需要灯光; - 环境光和半球光有什么区别;
- 为什么方向光通常适合作为工业场景主光;
DirectionalLight.position和target如何决定方向;- 点光源怎样进行距离衰减;
- 聚光灯的
angle、penumbra和target有什么作用; - 哪些灯光可以产生阴影;
castShadow和receiveShadow分别设置在哪里;- 阴影贴图怎样工作;
- 为什么点光源阴影成本较高;
shadow.mapSize越大是否越好;- 方向光阴影相机为什么需要限制范围;
bias与normalBias如何处理阴影黑斑和悬浮;- 如何给转炉车间建立基础布光;
- 模型发黑时应当按什么顺序排查。
二、灯光和材质的关系
1. 不受灯光影响的材质
下面这些材质通常不依赖场景灯光:
text
MeshBasicMaterial
MeshNormalMaterial
部分自定义 ShaderMaterial
例如:
javascript
const material =
new THREE.MeshBasicMaterial({
color: 0xff6600
});
即使场景中没有任何灯光,它仍然可以显示。
MeshBasicMaterial 适合:
- 调试;
- 状态标记;
- 标签底板;
- 测点;
- 线框;
- 不希望受环境明暗影响的提示对象。
2. 受灯光影响的材质
下列材质需要有效灯光或环境照明:
text
MeshLambertMaterial
MeshPhongMaterial
MeshStandardMaterial
MeshPhysicalMaterial
例如:
javascript
const material =
new THREE.MeshStandardMaterial({
color: 0x64748b,
metalness: 0.72,
roughness: 0.66
});
如果场景中没有灯光,也没有环境贴图,高金属度材质可能非常暗。
3. 灯光不只是提高亮度
灯光还负责让用户辨认:
text
设备正面与侧面
设备之间的前后关系
转炉炉口方向
氧枪与炉体距离
平台与地面高度
钢包与工位位置
如果场景只使用很强的环境光,虽然模型不会黑,但会失去方向性和体积感。
三、Light 基础类
Three.js 中的具体灯光都继承自:
javascript
THREE.Light
常见公共属性:
javascript
light.color
light.intensity
light.visible
light.position
light.parent
light.children
创建方向光:
javascript
const light =
new THREE.DirectionalLight(
0xffffff,
3
);
light.position.set(
8,
15,
10
);
scene.add(light);
不同灯光类型的强度尺度和物理含义不同,因此不能简单认为:
text
DirectionalLight intensity = 3
和
PointLight intensity = 3
会产生相同亮度。
四、环境光 AmbientLight
1. 环境光是什么
环境光均匀影响整个场景:
javascript
const ambientLight =
new THREE.AmbientLight(
0xffffff,
0.35
);
scene.add(ambientLight);
它没有:
- 位置;
- 明确方向;
- 距离衰减;
- 阴影。
2. 环境光不能产生阴影
环境光没有明确的入射方向,因此无法判断阴影应该投向哪里。
即使写:
javascript
ambientLight.castShadow = true;
也不会得到正常阴影。
3. 环境光的作用
环境光适合:
- 抬高整体亮度;
- 避免背光面完全黑暗;
- 调试材质;
- 低成本模式中的基础补光。
4. 环境光过强的问题
如果环境光强度过高:
javascript
const ambientLight =
new THREE.AmbientLight(
0xffffff,
5
);
可能导致:
- 正面与背面亮度相近;
- 设备失去体积感;
- 阴影对比度降低;
- 画面整体发灰发白;
- PBR 材质显得平淡。
所以环境光更适合做补光,而不是唯一主光。
五、半球光 HemisphereLight
1. 半球光是什么
半球光使用天空颜色和地面颜色模拟环境照明:
javascript
const hemisphereLight =
new THREE.HemisphereLight(
0xdbeafe,
0x332c27,
0.8
);
scene.add(hemisphereLight);
参数含义:
text
skyColor
→ 上方环境颜色
groundColor
→ 下方环境颜色
intensity
→ 光照强度
2. 半球光的特点
朝上的表面更多受到 skyColor 影响,朝下的表面更多受到 groundColor 影响。
因此,它比完全均匀的环境光更容易产生上下层次。
3. 半球光不能产生阴影
HemisphereLight 主要用于环境补光,不能投射阴影。
4. 为什么适合工业厂房
工业厂房通常具有:
- 顶部天窗或顶灯;
- 暗色地面;
- 顶部与底部不同的环境颜色;
- 钢结构之间复杂的间接反射。
半球光能够以较低成本近似这种上下方向差异。
5. 环境光和半球光的选择
text
AmbientLight
→ 所有方向完全一致
HemisphereLight
→ 上下颜色不同
转炉数字孪生中,通常可以优先使用低强度 HemisphereLight,再根据需要增加很弱的 AmbientLight。
六、方向光 DirectionalLight
1. 方向光是什么
方向光发出彼此平行的光线,常用于近似:
- 太阳光;
- 大范围主光;
- 厂房顶部统一照明;
- 大型展示主光。
创建:
javascript
const mainLight =
new THREE.DirectionalLight(
0xffffff,
3
);
mainLight.position.set(
12,
20,
10
);
scene.add(mainLight);
2. position 与 target
方向光的照射方向由:
text
light.position
light.target.position
共同决定。
javascript
mainLight.position.set(
12,
20,
10
);
mainLight.target.position.set(
0,
4,
0
);
scene.add(
mainLight,
mainLight.target
);
可以理解为:
text
光线从 mainLight.position
指向 mainLight.target.position
3. rotation 通常不能直接控制方向
错误思路:
javascript
mainLight.rotation.x =
Math.PI / 2;
方向光是 Target Direct Light,应调整:
javascript
mainLight.position
mainLight.target.position
4. target 需要加入场景树
如果要移动 target:
javascript
mainLight.target.position.set(
0,
5,
0
);
应确保:
javascript
scene.add(mainLight.target);
或者把 target 加入正确的父节点。
5. DirectionalLightHelper
javascript
const helper =
new THREE.DirectionalLightHelper(
mainLight,
3
);
scene.add(helper);
灯光或目标改变后:
javascript
mainLight.target
.updateMatrixWorld();
helper.update();
6. 为什么适合作为工业场景主光
方向光具有:
- 大范围覆盖;
- 统一光照方向;
- 阴影方向稳定;
- 阴影相机范围可控;
- 比大量局部阴影灯更容易优化。
推荐结构:
text
一盏 DirectionalLight
→ 负责主要照明和阴影
HemisphereLight
→ 环境补光
少量 PointLight / SpotLight
→ 高温和局部工位照明
七、点光源 PointLight
1. 点光源是什么
点光源从一个空间点向所有方向发光,可近似:
- 裸灯泡;
- 炉口高温光;
- 火焰中心;
- 小型警示灯;
- 局部设备灯。
javascript
const furnaceLight =
new THREE.PointLight(
0xff5a1f,
45,
14,
2
);
furnaceLight.position.set(
0,
7,
0
);
scene.add(furnaceLight);
构造参数:
text
color
intensity
distance
decay
2. distance
javascript
furnaceLight.distance = 14;
当 distance = 0 时,没有人为设置的最大距离。
当 distance > 0 时,灯光会在接近该范围时平滑衰减到无影响。
炉口局部光建议设置合理距离,避免整个厂房都被橙红色照亮。
3. decay
javascript
furnaceLight.decay = 2;
物理上,点光源照度与距离平方大致呈反比:
E ∝ 1 r 2 E\propto\frac{1}{r^2} E∝r21
因此,通常保持:
text
decay = 2
不要为了让灯照得更远而随意改变衰减规律,应优先检查:
- 场景单位;
- 灯光强度;
- 灯光距离;
- 曝光;
- 光源位置。
4. PointLightHelper
javascript
const helper =
new THREE.PointLightHelper(
furnaceLight,
0.5
);
scene.add(helper);
5. 点光源阴影成本较高
点光源向所有方向发光。为了获得完整阴影,阴影系统需要从多个方向生成阴影贴图。
常见实现相当于使用立方体贴图的六个方向,因此一个点光源阴影可能需要多次额外渲染场景。
工业场景中推荐:
text
主要空间阴影
→ 方向光负责
大多数点光源
→ 不投射阴影
关键局部点光
→ 少量开启,并严格限制范围
八、聚光灯 SpotLight
1. 聚光灯是什么
聚光灯从一个点向一个方向发光,形成锥形区域。
适合:
- 顶部射灯;
- 检修照明;
- 工位照明;
- 摄像头补光;
- 局部展示灯。
javascript
const spotLight =
new THREE.SpotLight(
0xffffff,
80,
30,
THREE.MathUtils.degToRad(30),
0.4,
2
);
参数:
text
color
intensity
distance
angle
penumbra
decay
2. angle
javascript
spotLight.angle =
THREE.MathUtils.degToRad(30);
angle 控制光锥角度,单位为弧度。
3. penumbra
javascript
spotLight.penumbra = 0.4;
通常范围:
text
0 到 1
text
0
→ 边缘较硬
接近 1
→ 边缘更柔和
4. target
javascript
spotLight.position.set(
-8,
16,
7
);
spotLight.target.position.set(
-2,
3,
0
);
scene.add(
spotLight,
spotLight.target
);
5. SpotLightHelper
javascript
const helper =
new THREE.SpotLightHelper(
spotLight
);
scene.add(helper);
参数改变后:
javascript
helper.update();
6. 聚光灯阴影
SpotLight 可以投射阴影。相比点光源,它只需要覆盖一个锥形方向,阴影成本通常更容易控制。
但如果厂房中有数十盏射灯,也不应全部开启动态阴影。
九、哪些灯光可以产生阴影
常用可投射阴影的灯光:
text
DirectionalLight
PointLight
SpotLight
不能投射传统阴影的灯光:
text
AmbientLight
HemisphereLight
另外,RectAreaLight 可以模拟矩形面光源,但常规 Three.js 实现中不直接支持阴影,并主要用于 PBR 材质。
十、阴影贴图原理
1. Three.js 常用 Shadow Map
阴影贴图的核心流程:
text
第一步
从光源视角渲染投射阴影的对象
第二步
保存每个位置到光源的深度
第三步
从正常相机渲染场景
第四步
比较当前表面深度与阴影贴图深度
第五步
判断当前位置是否被其他物体挡住光线
2. 阴影需要额外渲染
如果场景有一盏投射阴影的方向光,通常会多一次从光源视角的场景渲染。
如果有多盏阴影灯,额外渲染次数继续增加。
因此:
可以有多盏灯,但通常只让极少数灯产生实时阴影。
十一、启用阴影的三个层级
1. 渲染器开启阴影
javascript
renderer.shadowMap.enabled = true;
WebGLRenderer 的阴影默认通常没有启用。
2. 光源开启投射阴影
javascript
mainLight.castShadow = true;
灯光必须是支持阴影的类型。
3. Mesh 设置投射或接收
投射阴影:
javascript
mesh.castShadow = true;
接收阴影:
javascript
mesh.receiveShadow = true;
地面一般只接收:
javascript
ground.castShadow = false;
ground.receiveShadow = true;
4. 三个条件缺一不可
javascript
renderer.shadowMap.enabled = true;
light.castShadow = true;
caster.castShadow = true;
receiver.receiveShadow = true;
如果其中任意一项缺失,阴影可能不显示。
十二、GLTF 模型开启阴影
只设置:
javascript
gltf.scene.castShadow = true;
通常不能替代内部 Mesh 配置。
应遍历模型:
javascript
gltf.scene.traverse((object) => {
if (!object.isMesh) {
return;
}
object.castShadow = true;
object.receiveShadow = true;
});
但是,大型厂房模型不应对所有 Mesh 无条件开启阴影。
建议按类别筛选:
text
关键动态设备
→ castShadow = true
地面和大型平台
→ receiveShadow = true
远景小零件
→ 关闭阴影
透明辅助对象
→ 通常关闭阴影
十三、阴影贴图类型
javascript
renderer.shadowMap.type =
THREE.PCFSoftShadowMap;
常见类型:
text
BasicShadowMap
PCFShadowMap
PCFSoftShadowMap
VSMShadowMap
1. BasicShadowMap
- 速度较快;
- 阴影边缘较硬;
- 锯齿较明显。
2. PCFShadowMap
- 对阴影进行一定过滤;
- 质量与性能较均衡;
- 是常见基础选择。
3. PCFSoftShadowMap
- 阴影边缘通常更柔和;
- 适合普通工业可视化;
- 不等同于真实面积光源软阴影。
4. VSMShadowMap
- 能产生较柔和结果;
- 可能出现漏光或模糊;
- 需要针对具体场景测试。
十四、shadow.mapSize
1. 设置分辨率
javascript
mainLight.shadow.mapSize.set(
2048,
2048
);
常见尺寸:
text
512
1024
2048
4096
通常使用 2 的幂。
2. 分辨率越高不一定越合理
在相同覆盖范围内,mapSize 越高,阴影通常越清晰。
但代价包括:
- 更多显存;
- 更高渲染成本;
- 更高带宽;
- 移动设备压力。
阴影清晰度同时取决于:
text
阴影贴图分辨率
÷
阴影相机覆盖范围
缩小覆盖范围通常比盲目升到 4096 更有效。
十五、方向光阴影相机
方向光使用正交阴影相机:
javascript
mainLight.shadow.camera
1. 设置阴影范围
javascript
const shadowCamera =
mainLight.shadow.camera;
shadowCamera.left = -15;
shadowCamera.right = 15;
shadowCamera.top = 15;
shadowCamera.bottom = -15;
shadowCamera.near = 1;
shadowCamera.far = 50;
shadowCamera.updateProjectionMatrix();
只有处于阴影相机范围中的投射对象和接收区域,才能获得正确阴影。
2. 阴影被截断
如果阴影只显示一部分,常见原因是:
text
对象或地面超出 shadow.camera 范围
方向光可能照亮整个场景,但阴影只会在阴影相机范围中生成。
3. CameraHelper
javascript
const shadowCameraHelper =
new THREE.CameraHelper(
mainLight.shadow.camera
);
scene.add(shadowCameraHelper);
检查:
- 转炉是否在盒内;
- 地面接收区域是否在盒内;
- near 和 far 是否合理;
- 范围是否过大。
4. 不要覆盖整个厂区
如果设置:
javascript
shadowCamera.left = -1000;
shadowCamera.right = 1000;
虽然范围大,但每个阴影像素对应的世界面积也更大,阴影会变模糊。
实时阴影应聚焦当前转炉工作区。
十六、聚光灯和点光源阴影范围
1. SpotLight
javascript
spotLight.shadow.mapSize.set(
1024,
1024
);
spotLight.shadow.camera.near = 0.5;
spotLight.shadow.camera.far = 40;
聚光灯阴影相机需要覆盖光锥。
2. PointLight
javascript
pointLight.shadow.camera.near = 0.5;
pointLight.shadow.camera.far = 14;
点光源的 far 越大,覆盖范围越广,阴影精度和性能压力也越高。
炉口点光通常建议:
javascript
furnaceLight.castShadow = false;
让方向光负责主要阴影。
十七、bias 与 normalBias
1. 阴影痤疮
表面出现密集黑色斑点或条纹,通常称为:
text
Shadow Acne
它来自深度精度和表面自身比较误差。
2. bias
javascript
mainLight.shadow.bias =
-0.0001;
合理值可以减少表面黑斑。
但绝对值过大可能让阴影离开物体,形成阴影悬浮。
3. normalBias
javascript
mainLight.shadow.normalBias =
0.025;
它沿表面法线方向偏移阴影查询位置。
对曲面、斜面和大尺度设备表面通常很有用。
4. 推荐调节顺序
text
1. 确认模型比例和场景单位
2. 缩小阴影相机范围
3. 设置合理 near 和 far
4. 调节 mapSize
5. 小幅调节 normalBias
6. 最后小幅调节 bias
不要直接使用很大的偏移值。
十八、动态阴影与静态阴影
默认情况下,阴影会随场景持续更新。
转炉倾动、氧枪升降和钢包移动时,需要更新阴影。
如果灯光和所有投射物都保持静止,可以:
javascript
renderer.shadowMap.autoUpdate = false;
需要重新更新时:
javascript
renderer.shadowMap.needsUpdate = true;
数字孪生项目可以采用混合策略:
text
静态厂房
→ 烘焙光照或低成本接收阴影
转炉、氧枪、钢包
→ 保留动态阴影
远景和小零件
→ 不参与实时阴影
十九、工业场景基础布光
1. 推荐基础结构
text
HDR 或 scene.environment
+
低强度 HemisphereLight
+
一盏 DirectionalLight 主光
+
少量无阴影 PointLight / SpotLight
2. 环境层
javascript
const hemisphereLight =
new THREE.HemisphereLight(
0xdbeafe,
0x332c27,
0.8
);
scene.add(hemisphereLight);
作用:
- 避免背面完全黑;
- 提供顶部与地面环境差异;
- 补充大型厂房间接照明。
如果已使用很亮的 HDR 环境,应降低半球光强度。
3. 主方向光
javascript
const mainLight =
new THREE.DirectionalLight(
0xffffff,
3
);
mainLight.position.set(
12,
20,
10
);
mainLight.target.position.set(
0,
4,
0
);
mainLight.castShadow = true;
scene.add(
mainLight,
mainLight.target
);
作用:
- 塑造设备体积;
- 提供统一主阴影方向;
- 覆盖转炉工作区。
4. 炉口高温光
javascript
const furnaceLight =
new THREE.PointLight(
0xff5a1f,
45,
14,
2
);
furnaceLight.position.set(
0,
7,
0
);
furnaceLight.castShadow = false;
scene.add(furnaceLight);
它应与:
text
emissive 材质
Bloom
火焰和粒子特效
配合使用。
5. 检修聚光灯
javascript
const maintenanceLight =
new THREE.SpotLight(
0xfff4df,
70,
25,
THREE.MathUtils.degToRad(35),
0.45,
2
);
maintenanceLight.position.set(
-6,
16,
4
);
maintenanceLight.target.position.set(
-2,
3,
0
);
maintenanceLight.castShadow = false;
scene.add(
maintenanceLight,
maintenanceLight.target
);
二十、完整转炉灯光与阴影示例
javascript
import * as THREE from "three";
import {
OrbitControls
} from "three/addons/controls/OrbitControls.js";
import "./style.css";
const app =
document.querySelector("#app");
if (!app) {
throw new Error(
"未找到 #app 容器"
);
}
const scene =
new THREE.Scene();
scene.background =
new THREE.Color(0x111827);
const camera =
new THREE.PerspectiveCamera(
55,
window.innerWidth /
window.innerHeight,
0.1,
200
);
camera.position.set(
16,
12,
20
);
const renderer =
new THREE.WebGLRenderer({
antialias: true
});
renderer.setPixelRatio(
Math.min(
window.devicePixelRatio,
2
)
);
renderer.setSize(
window.innerWidth,
window.innerHeight
);
renderer.shadowMap.enabled =
true;
renderer.shadowMap.type =
THREE.PCFSoftShadowMap;
app.appendChild(
renderer.domElement
);
const controls =
new OrbitControls(
camera,
renderer.domElement
);
controls.target.set(
0,
4,
0
);
controls.enableDamping = true;
const hemisphereLight =
new THREE.HemisphereLight(
0xdbeafe,
0x332c27,
0.9
);
scene.add(hemisphereLight);
const mainLight =
new THREE.DirectionalLight(
0xffffff,
3
);
mainLight.position.set(
12,
20,
10
);
mainLight.target.position.set(
0,
4,
0
);
mainLight.castShadow = true;
mainLight.shadow.mapSize.set(
2048,
2048
);
mainLight.shadow.camera.left =
-14;
mainLight.shadow.camera.right =
14;
mainLight.shadow.camera.top =
14;
mainLight.shadow.camera.bottom =
-14;
mainLight.shadow.camera.near =
1;
mainLight.shadow.camera.far =
50;
mainLight.shadow.normalBias =
0.025;
mainLight.shadow.bias =
-0.0001;
scene.add(
mainLight,
mainLight.target
);
const furnaceLight =
new THREE.PointLight(
0xff5a1f,
45,
14,
2
);
furnaceLight.position.set(
0,
7,
0
);
furnaceLight.castShadow = false;
scene.add(furnaceLight);
const spotLight =
new THREE.SpotLight(
0xfff2d8,
70,
30,
THREE.MathUtils.degToRad(32),
0.45,
2
);
spotLight.position.set(
-8,
16,
7
);
spotLight.target.position.set(
-2,
3,
0
);
spotLight.castShadow = false;
scene.add(
spotLight,
spotLight.target
);
const ground =
new THREE.Mesh(
new THREE.PlaneGeometry(
40,
40
),
new THREE.MeshStandardMaterial({
color: 0x2f3540,
metalness: 0,
roughness: 0.92
})
);
ground.rotation.x =
-Math.PI / 2;
ground.receiveShadow = true;
ground.castShadow = false;
scene.add(ground);
const converterRoot =
new THREE.Group();
scene.add(converterRoot);
const converterPivot =
new THREE.Object3D();
converterPivot.position.set(
0,
6,
0
);
converterRoot.add(
converterPivot
);
const converterMaterial =
new THREE.MeshStandardMaterial({
color: 0x646b73,
metalness: 0.72,
roughness: 0.66
});
const converterMesh =
new THREE.Mesh(
new THREE.CylinderGeometry(
2.4,
2.8,
5,
48
),
converterMaterial
);
converterMesh.position.y =
-1.7;
converterMesh.castShadow = true;
converterMesh.receiveShadow = true;
converterPivot.add(
converterMesh
);
const hotMouth =
new THREE.Mesh(
new THREE.TorusGeometry(
2.2,
0.18,
16,
48
),
new THREE.MeshStandardMaterial({
color: 0x7c2d12,
metalness: 0.15,
roughness: 0.72,
emissive: 0xff3d00,
emissiveIntensity: 2
})
);
hotMouth.rotation.x =
Math.PI / 2;
hotMouth.position.y =
0.82;
converterMesh.add(
hotMouth
);
const lance =
new THREE.Mesh(
new THREE.CylinderGeometry(
0.12,
0.12,
9,
16
),
new THREE.MeshStandardMaterial({
color: 0x98a2ad,
metalness: 0.85,
roughness: 0.38
})
);
lance.position.set(
0,
12,
0
);
lance.castShadow = true;
lance.receiveShadow = true;
scene.add(lance);
const ladle =
new THREE.Mesh(
new THREE.CylinderGeometry(
1.6,
1.2,
2.8,
32
),
new THREE.MeshStandardMaterial({
color: 0x74452b,
metalness: 0.45,
roughness: 0.82
})
);
ladle.position.set(
7,
1.4,
3
);
ladle.castShadow = true;
ladle.receiveShadow = true;
scene.add(ladle);
const directionalHelper =
new THREE.DirectionalLightHelper(
mainLight,
3
);
const shadowCameraHelper =
new THREE.CameraHelper(
mainLight.shadow.camera
);
// 调试时启用
// scene.add(directionalHelper);
// scene.add(shadowCameraHelper);
const clock =
new THREE.Clock();
function animate() {
const elapsed =
clock.getElapsedTime();
converterPivot.rotation.z =
THREE.MathUtils.degToRad(
-25 +
Math.sin(
elapsed * 0.5
) * 40
);
lance.position.y =
11 +
Math.sin(elapsed) * 1.2;
directionalHelper.update();
controls.update();
renderer.render(
scene,
camera
);
}
renderer.setAnimationLoop(
animate
);
function handleResize() {
const width =
window.innerWidth;
const height =
window.innerHeight;
camera.aspect =
width / height;
camera.updateProjectionMatrix();
renderer.setSize(
width,
height
);
}
window.addEventListener(
"resize",
handleResize
);
二十一、模型发黑问题排查
1. 检查材质类型
javascript
console.log(
mesh.material.type
);
如果是受光材质,临时替换:
javascript
mesh.material =
new THREE.MeshBasicMaterial({
color: 0xff0000
});
如果 Basic 材质能正常显示,问题更可能在灯光、环境或原材质。
2. 检查灯光是否加入场景
javascript
console.log(
light.parent
);
如果没有父节点,可能忘记:
javascript
scene.add(light);
3. 检查灯光强度
javascript
console.log(
light.intensity
);
强度为 0 时不会产生光照。
4. 检查方向光 target
使用:
javascript
const helper =
new THREE.DirectionalLightHelper(
light
);
scene.add(helper);
确认光线是否照向模型。
5. 检查法线
临时使用:
javascript
mesh.material =
new THREE.MeshNormalMaterial();
自定义 Geometry 可尝试:
javascript
geometry.computeVertexNormals();
6. 检查负缩放
javascript
mesh.scale.set(
-1,
1,
1
);
负缩放可能影响法线、面朝向和阴影。应优先在建模工具中应用并修复镜像变换。
7. 检查正反面
调试时可以临时:
javascript
material.side =
THREE.DoubleSide;
如果双面后可见,说明可能存在法线或顶点绕序问题。正式模型应修复,而不是长期依赖双面材质。
8. 检查颜色和纹理
颜色贴图会与:
javascript
material.color
相乘。
颜色贴图还应设置:
javascript
texture.colorSpace =
THREE.SRGBColorSpace;
9. 检查金属度与环境
高金属度材质主要反射环境。
如果:
javascript
material.metalness = 1;
scene.environment = null;
模型可能非常暗。
可以临时降低金属度判断:
javascript
material.metalness = 0.2;
10. 检查粗糙度
极低粗糙度会形成集中反射。普通转炉炉壳可以先从:
text
roughness = 0.5 到 0.8
附近调试。
11. 检查曝光
javascript
renderer.toneMappingExposure =
1;
曝光过低会让整个场景发黑,不要只通过无限增加灯光强度补偿错误曝光。
12. 检查模型位置与尺寸
添加:
javascript
const boxHelper =
new THREE.BoxHelper(
model,
0xffff00
);
scene.add(boxHelper);
确认模型没有:
- 远离原点;
- 位于其他模型内部;
- 尺寸极小;
- 超出相机范围;
- 被雾完全覆盖。
二十二、阴影不显示排查
按以下顺序检查:
text
1. renderer.shadowMap.enabled 是否为 true
2. 光源是否支持阴影
3. light.castShadow 是否为 true
4. caster.castShadow 是否为 true
5. receiver.receiveShadow 是否为 true
6. 投射物是否位于阴影相机范围
7. 接收面是否位于阴影相机范围
8. near 和 far 是否合理
9. 模型世界位置是否正确
10. 透明或自定义材质是否影响阴影
11. 阴影资源是否被关闭自动更新
可以使用:
javascript
new THREE.CameraHelper(
light.shadow.camera
);
直接观察阴影范围。
二十三、阴影质量问题排查
1. 阴影锯齿
尝试:
- 缩小阴影相机范围;
- 提高
mapSize; - 使用
PCFSoftShadowMap; - 调整 near 和 far;
- 不要覆盖整个厂房。
2. 阴影模糊
可能原因:
- 阴影范围过大;
- mapSize 太低;
- 阴影过滤方式;
- 阴影相机没有聚焦关键区域。
3. 表面黑斑
尝试小幅调整:
javascript
light.shadow.normalBias =
0.02;
light.shadow.bias =
-0.0001;
同时检查:
- 重叠表面;
- 法线;
- near 和 far;
- 模型比例。
4. 阴影悬浮
说明 bias 或 normalBias 可能过大。
还应检查模型是否真的接触地面。
5. 阴影抖动
可能来自:
- 阴影范围过大;
- 灯光持续移动;
- 大世界坐标;
- 模型远离原点;
- 低分辨率阴影贴图。
大型厂房后续可以评估:
- CSM;
- 分区阴影;
- 烘焙阴影;
- 阴影相机跟随关键区域。
二十四、性能优化建议
1. 只保留一盏主要阴影灯
text
DirectionalLight
→ 负责主要实时阴影
PointLight / SpotLight
→ 大多数只负责局部亮度
2. 缩小阴影范围
方向光阴影相机只覆盖转炉工作区,不覆盖整个厂区。
3. 控制 castShadow
优先让以下对象投射阴影:
- 转炉主体;
- 氧枪;
- 钢包;
- 大型平台;
- 关键动态设备。
可以关闭:
- 小螺栓;
- 远处管线;
- 标签;
- 辅助器;
- 小型装饰;
- 不可见内部零件。
4. 控制 receiveShadow
主要接收对象:
- 地面;
- 平台;
- 墙体;
- 大型设备表面;
- 关键空间关系对象。
5. 合理选择 mapSize
text
低性能模式
→ 512 / 1024
普通桌面模式
→ 1024 / 2048
高质量局部展示
→ 2048 / 4096
不要把 4096 作为无条件默认值。
6. 静态光照烘焙
厂房、墙体和固定平台长期不动时,可以在建模阶段烘焙:
- Light Map;
- AO;
- 固定阴影;
- 环境光照。
实时阴影只留给动态设备。
7. 使用假阴影
远处车辆、人员或大量重复对象可以使用:
- 半透明圆形平面;
- ShadowMaterial;
- Blob Shadow;
- 预烘焙阴影纹理。
二十五、推荐灯光模块
javascript
import * as THREE from "three";
export function createIndustrialLighting(
scene
) {
const root =
new THREE.Group();
root.name =
"LightingRoot";
scene.add(root);
const hemisphereLight =
new THREE.HemisphereLight(
0xdbeafe,
0x332c27,
0.8
);
root.add(hemisphereLight);
const mainLight =
new THREE.DirectionalLight(
0xffffff,
3
);
mainLight.position.set(
12,
20,
10
);
mainLight.target.position.set(
0,
4,
0
);
mainLight.castShadow = true;
mainLight.shadow.mapSize.set(
2048,
2048
);
mainLight.shadow.camera.left =
-14;
mainLight.shadow.camera.right =
14;
mainLight.shadow.camera.top =
14;
mainLight.shadow.camera.bottom =
-14;
mainLight.shadow.camera.near =
1;
mainLight.shadow.camera.far =
50;
mainLight.shadow.normalBias =
0.025;
root.add(
mainLight,
mainLight.target
);
const furnaceLight =
new THREE.PointLight(
0xff5a1f,
45,
14,
2
);
furnaceLight.position.set(
0,
7,
0
);
furnaceLight.castShadow = false;
root.add(furnaceLight);
return {
root,
hemisphereLight,
mainLight,
furnaceLight
};
}
独立灯光模块便于:
- 切换质量等级;
- 切换昼夜模式;
- 统一阴影参数;
- 显示调试辅助器;
- 页面销毁时统一释放资源。
二十六、本篇必须掌握的代码
环境光
javascript
const light =
new THREE.AmbientLight(
0xffffff,
0.35
);
半球光
javascript
const light =
new THREE.HemisphereLight(
0xdbeafe,
0x332c27,
0.8
);
方向光
javascript
const light =
new THREE.DirectionalLight(
0xffffff,
3
);
light.position.set(
12,
20,
10
);
light.target.position.set(
0,
4,
0
);
scene.add(
light,
light.target
);
点光源
javascript
const light =
new THREE.PointLight(
0xff5a1f,
45,
14,
2
);
聚光灯
javascript
const light =
new THREE.SpotLight(
0xffffff,
80,
30,
THREE.MathUtils.degToRad(30),
0.4,
2
);
启用阴影
javascript
renderer.shadowMap.enabled = true;
light.castShadow = true;
mesh.castShadow = true;
ground.receiveShadow = true;
阴影贴图大小
javascript
light.shadow.mapSize.set(
2048,
2048
);
方向光阴影范围
javascript
light.shadow.camera.left = -15;
light.shadow.camera.right = 15;
light.shadow.camera.top = 15;
light.shadow.camera.bottom = -15;
light.shadow.camera.near = 1;
light.shadow.camera.far = 50;
阴影偏移
javascript
light.shadow.normalBias = 0.02;
light.shadow.bias = -0.0001;
二十七、本文总结
Three.js 灯光可以分成环境照明和定向照明。
AmbientLight
text
均匀照亮场景
没有方向
不能投射阴影
适合低强度补光。
HemisphereLight
text
天空颜色与地面颜色渐变
不能投射阴影
具有上下环境层次
适合厂房和工业场景环境补光。
DirectionalLight
text
平行光线
由 position 和 target 决定方向
可以产生阴影
通常是转炉场景主光和主阴影灯的首选。
PointLight
text
从一点向所有方向发光
具有距离衰减
可以产生阴影
阴影成本较高
适合炉口和局部高温区域。
SpotLight
text
沿锥形方向发光
具有 angle、penumbra 和 target
可以产生阴影
适合检修灯和工位照明。
阴影三道开关
text
renderer.shadowMap.enabled
light.castShadow
mesh.castShadow / mesh.receiveShadow
必须同时正确设置。
阴影性能原则
text
保留一盏主阴影方向光
限制阴影相机范围
只让关键对象投射阴影
大多数局部灯不投射阴影
静态场景尽量烘焙
模型发黑排查顺序
text
材质类型
灯光是否加入场景
灯光强度
方向光 target
模型法线
负缩放
面朝向
颜色和纹理
金属度与环境贴图
粗糙度
曝光
模型位置和尺寸
最终可以用一句话概括:
环境光负责避免场景完全黑暗,方向光负责塑造主要体积和阴影,点光与聚光灯负责局部重点,而阴影必须在质量、覆盖范围和性能之间取得平衡。
对于转炉数字孪生,推荐从下面的结构开始:
text
HDR 环境
+
低强度半球光
+
一盏方向主光和主阴影
+
少量无阴影局部灯
二十八、下一篇学习内容
下一篇建议介绍:
第 11 篇:GLTF/GLB 模型加载与工业模型管理
主要内容可以包括:
- GLTF 与 GLB;
- GLTFLoader;
- 异步加载;
- LoadingManager;
- 加载进度;
- 模型场景树;
- 模型尺寸和坐标修正;
- 节点查找;
- 材质与纹理;
- 动画;
- DRACO;
- Meshopt;
- KTX2;
- 模型缓存;
- 模型释放;
- 转炉模型接入既有控制层;
- 生产环境路径和加载失败排查。