Day 101 · 光照与材质 — 让工业场景有质感
Day 99 第一天就踩过"全黑"的坑:Standard 材质没有灯就是黑。今天正面解决它——光照与材质是整个 3D"质感"的来源。你要掌握:四类常用材质各自什么时候用、四类光源各自长什么样、以及最重要的匹配原则(什么材质配什么光)。最后用 Day 66 的工业 token 纪律给场景上色,让立方体从"塑料玩具"变成"工业设备"。
目录
一、光照模型:光是怎么让物体"看见"的
1.1 三个要素
一个物体最终呈现的颜色 ≈ 光源 × 材质 × 表面朝向:
最终颜色 ≈ 光的颜色/强度 × 材质的吸收/反射属性 × 表面法线朝向光源的程度
光源:提供"光"(颜色、强度、方向)
材质:决定表面"怎么处理光"(吸收哪些、反射哪些、高光多亮)
法线:表面朝向——面朝灯就亮,背朝灯就暗(这就是"立体感"的来源)
记忆钩子:没有光源,Standard 材质一片黑;没有材质,光无处着落。两者必须同时在场,这就是 Day 99 三步排查法的第 2 步。
1.2 从"有没有立体感"看光照的意义
Day 99 只有一盏 AmbientLight——立方体是"一片均匀的亮",看不出棱角(环境光没有方向,四面等亮)。加一盏 DirectionalLight 后,朝灯的面亮、背灯的面暗——立体感立刻出现。今天的目标就是让场景"有明暗、有层次"。
二、材质:物体表面"吃什么光"
2.1 四种常用材质对比
2.2 MeshStandardMaterial 的两个关键属性
PBR 材质用两个旋钮描述"材质是金属还是塑料":
const material = new THREE.MeshStandardMaterial({
color: 0x00c6ff, // 基础色(Day 66 token)
metalness: 0.6, // 金属度 0~1:0 塑料 / 1 金属
roughness: 0.4, // 粗糙度 0~1:0 镜面抛光 / 1 磨砂
});
metalness = 1 + roughness = 0 → 镜面金属(反光强烈,环境要求高)
metalness = 0 + roughness = 1 → 粗糙哑光(吸收光,不易出效果)
工业场景常用折中:
metalness 0.3~0.8、roughness 0.3~0.6
现在就能解释 Day 99 的"全黑":Standard 材质要算光,而光没配齐。材质要效果,先问自己配了什么灯。
三、光源:四种灯怎么选
3.1 四种光源对比
3.2 工业场景的"三灯法则"
不是灯越多越好——每多一盏灯 = 每帧多一次逐像素计算。一套够用的默认组合:
// 三灯法则:兜底 + 主光 + 补光(照抄这个组合,再按需调整)
scene.add(new THREE.AmbientLight(0xffffff, 0.4)); // ① 兜底:让暗处可辨
const sun = new THREE.DirectionalLight(0xffffff, 1.2); // ② 主光:给立体感
sun.position.set(5, 8, 4); // 主光从斜上方来
scene.add(sun);
// ③ 补光(可选):PointLight 点亮局部,或用另一盏 DirectionalLight 提亮背光面
方向光的诀窍:position 决定了光照方向(从该点射向原点),所以方向光也要摆位置——摆歪了立体感就歪。
3.3 主光的立体感设置
// 主光常见参数:给阴影做准备(Day 106+ 阴影篇再深入)
const sun = new THREE.DirectionalLight(0xffffff, 1.2);
sun.position.set(5, 8, 4); // 斜上方 45° 方向,立体感最佳
scene.add(sun);
四、匹配原则:材质 × 光的组合表
这是今天最核心的交付物——照这张表配,就不会再全黑/过曝/死板:
排查口诀(第 2 步的完整版):先看材质要不要光 → 要光就查灯齐不齐 → 灯齐还黑就看强度和位置。
五、工业配色纪律:Day 66 token 的 3D 版
5.1 从 token 到 3D 材质
第 3 个月的 industrial-dark token 直接搬进 3D——视觉语言要跨 2D/3D 一致(Day 105 大屏与 3D 同框时配色不打架):
// src/core/three/theme.ts:3D 版 token(与 Day 66 industrial-dark 对齐)
export const threeTheme = {
bg: 0x0d1421, // 场景底色 = 大屏底色
primary: 0x00c6ff, // 主色(青蓝)
accent: 0xff6b35, // 强调色(橙)——告警/关键设备
success: 0x00e676, // 运行正常(绿)
danger: 0xff3b30, // 告警(红)
panel: 0x16233a, // 次级面板(深蓝灰)
} as const;
5.2 一句话配色纪律
大面积用 panel/bg(深色打底),主设备用 primary,状态色(绿/红)只给"有语义"的物体——和 2D 大屏完全一致:颜色是语义,不是装饰。
六、完整示例:一个有质感的工业设备
组装今天全部内容——一个"金属质感 + 状态指示灯"的设备:
// src/core/three/build-device.ts
import * as THREE from "three";
import { threeTheme as T } from "./theme";
/**
* 构建一个"工业设备":金属机身 + 指示灯 + 三灯照明
* 返回 device 与 lights,Day 102 会把它升级为可复用的 Group
*/
export function buildDevice(scene: THREE.Scene): {
device: THREE.Mesh;
setStatus: (s: "normal" | "alarm") => void;
} {
// —— 灯光:三灯法则(每场景配一次,不是每物体一次)——
scene.add(new THREE.AmbientLight(0xffffff, 0.4)); // 兜底
const sun = new THREE.DirectionalLight(0xffffff, 1.2); // 主光
sun.position.set(5, 8, 4);
scene.add(sun);
// —— 机身:金属质感(Standard + metalness/roughness)——
const bodyMat = new THREE.MeshStandardMaterial({
color: T.panel,
metalness: 0.7,
roughness: 0.4,
});
const body = new THREE.Mesh(new THREE.BoxGeometry(2, 1, 1), bodyMat);
body.position.y = 1;
// —— 指示灯:自发光质感(Basic 不需要光 = 恒亮)——
const lightMat = new THREE.MeshBasicMaterial({ color: T.success });
const indicator = new THREE.Mesh(new THREE.SphereGeometry(0.15, 16, 16), lightMat);
indicator.position.set(0.8, 1.45, 0);
// 组合:设备 = 机身 + 指示灯(Day 102 升级为 Group 层级)
const device = new THREE.Group();
device.add(body, indicator);
scene.add(device);
return {
device,
// 状态切换:改指示灯颜色(告警红 / 正常绿)——Day 104 交互的雏形
setStatus(s) {
lightMat.color.setHex(s === "alarm" ? T.danger : T.success);
},
};
}
观察点:
MeshBasicMaterial指示灯永远亮(不吃光、不衰减)——这是"指示灯/警示框"用 Basic 的根本原因:要恒亮就用 Basic,要真实感就用 Standard。
七、对照表第 2 行:手写 WebGL vs Three.js(光照)
结论:光照是"手写 WebGL 成本飙升"的第一个分水岭——真正的光照方程(Phong 反射模型:环境 + 漫反射 + 高光三部分)你自己写要一整天,Three.js 封装成一盏灯。知道它替你做了什么,就够了(面试被追问"Phong 模型三要素"能答出:环境光 + 漫反射 + 镜面高光)。
八、常见坑点
坑 1:材质要光,但只配了环境光
症状:物体"平"、没有立体感。原因:AmbientLight 无方向,四面等亮。修法:加一盏 DirectionalLight 当主光。
坑 2:方向光忘了摆 position
症状:物体一侧奇亮/奇暗。原因:方向光默认从原点(0,0,0)射向原点——位置没设,方向随机。修法:sun.position.set(x,y,z)。
坑 3:每物体都加一套灯
症状:性能骤降(多光源逐像素叠加)。原因:把"每物体配灯"当成了习惯。修法:三灯法则——灯是场景级的,配一次全局复用。
坑 4:Basic 材质想要"被光照"
症状:Basic 物体怎么调 metalness 都没反应。原因:Basic 根本不参与光照计算。修法:要光影就用 Standard,要恒亮就用 Basic,别混。
坑 5:金属度拉满后"太刺眼/全黑"
症状:metalness=1 的物体在弱光下发黑。原因:金属反射强,环境不足时反而显得暗。修法:环境贴图(Week 16)或降低 metalness 到 0.6~0.8。
九、自测挑战
T1 · 三灯场景(60 分钟)
搭一个场景:主光从斜上方 45° 照一个 metalness=0.7 的金属立方体,环境光兜底。截三张图对比:只有环境光 / 只有方向光 / 两者都有——亲眼看"立体感从哪来"。
T2 · 材质四连(40 分钟)
同一个立方体,依次换成 Basic/Lambert/Phong/Standard,各截一张图并记录:哪个最亮、哪个最有立体感、哪个最贵(渲染成本猜一下)。四张图钉进笔记,面试"材质怎么选"直接看表。
T3 · 指示灯(30 分钟)
做第 6 节的设备,实现 setStatus("alarm") 时指示灯变红。给 setStatus 加一个基于 getElapsedTime() 的闪烁动画(visible = sin(t) > 0),模拟告警灯——今天顺带预习 Day 104 的"状态驱动视觉"。
T4 · 配色对照(20 分钟)
用 Day 66 的 token 给 3D 场景上色,与 2D 大屏截图放一起对比——确认 2D/3D 视觉语言一致(这是 Day 105 的验收项之一)。
十、总结
金属设备有了立体感、指示灯会随状态变红。明天给设备装上更多零件:几何体与 Group 层级变换。
明日预告:Day 102 几何与变换——常用几何体清单、position/rotation/scale 三变换、以及 Group 怎么把"机身+指示灯+管道"组装成一台可以整体移动的设备。