第 9 课:光照与法线
为什么同一种颜色有明暗
第 7 课的形变改变了物体形状,第 8 课的噪声改变了表面图案。要让它们看起来有体积,还需要知道每个片元的表面朝向:法线。本课用球体作为练习载体,先手写漫反射、高光与边缘光,再用法线贴图、环境图和近似透光构造常见材质感。
所有用于点乘的方向必须在同一坐标空间。本课顶点 Shader 用 normalMatrix * normal 把局部法线转到观察空间,并把 modelViewMatrix * position 的结果传为观察空间位置。片元 Shader 对插值后的法线重新 normalize,因为线性插值后的向量长度通常不再是 1。JS 中的 uLightDirection 也定义为观察空间方向,并在创建时归一化。若改为随世界中的灯移动,需每帧把世界光方向变到观察空间后传入,或把法线与位置统一改在世界空间计算。
漫反射、环境项与高光
Lambert 模型的核心是 max(dot(N,L),0.0):表面正对光源时接近 1,侧面对光时趋近 0,背面被截成 0。第 1 题暗面全黑,方便认出点乘结果。第 2 题加常量环境项 ambient + diffuse*strength,让暗面仍可见;这不是环境光的物理模拟,只是常用、廉价的基础补光。
Phong 高光还需要视线 V = normalize(-vViewPosition),以及光线关于法线的反射方向 R = reflect(-L,N)。pow(max(dot(R,V),0.0),shininess) 在视线对准反射方向时形成亮点。指数越大,高光越窄;但它并没有能量守恒、粗糙度分布或真实金属工作流。日常生产材质优先用 Three.js 的 MeshStandardMaterial,手写公式适合理解变量和制作风格化效果。
菲涅尔边缘光使用 1-max(dot(N,V),0) 作为掠射程度。正面靠近 0,轮廓靠近 1;取幂后可控制亮边宽度。很多轮廓光、能量盾和半透明边缘都用这一近似。第 4 题的滑块只改变指数,不改变法线或相机。
法线贴图与 TBN
法线贴图保存的是方向数据,不是供人观看的颜色,所以保持默认无颜色空间。采样得到的 RGB 在 0–1,需要乘 2 减 1 还原到 -1–1。贴图方向位于切线空间:Z 指向原表面法线,X/Y 沿 UV 方向。要与观察空间光方向点乘,先用切线 T、副切线 B、原法线 N 组成 TBN 矩阵,把贴图法线转到观察空间。
球体几何体先在 JS 中 computeTangents(),顶点 Shader 把 tangent 和 normal 都变到观察空间,并用 tangent.w 保留镜像 UV 的手性。片元 Shader 采样 normal-bumps.png,应用强度滑块后重新归一化。强度为 0 时,XY 分量归零,结果应接近光滑球体。法线贴图只改变光照方向,不改变球体轮廓;真正凹凸需要位移或几何体细分。
风格化光照与描边
卡通渲染把连续 dot(N,L) 用 step 分成几档,形成明确色块。水面题不移动顶点,而是用时间变化的正弦斜率直接构造片元法线,再算镜面高光;画面有波光,但轮廓仍是平面。
描边题用同一球体绘制两次:内层为普通暖色球体,外层顶点沿 normal 外扩,只渲染背面。外层在内层轮廓外露出黑色,形成倒置外壳描边。宽度以模型单位计算,透视投影下不保证屏幕像素宽度恒定;需要固定像素描边时,后处理边缘检测或屏幕空间扩张更合适。
金属反射与透光近似
金属题由 reflect(-V,N) 得到反射方向,换算成经纬 UV 去采样本地摄影棚环境图,并叠加窄高光。示例是简化的环境反射,不包括基于粗糙度的预过滤、BRDF 查找表或能量守恒。对生产级金属与可调粗糙度,使用 MeshStandardMaterial/MeshPhysicalMaterial 配合 PMREM 环境图更稳妥。
玉石题用前侧漫反射保留体积,再把背光项和菲涅尔边缘权重相乘,使轮廓出现绿色透光。它没有追踪光在物体内部传播,也不知道厚度,因此是次表面散射的风格化近似。适合小成本装饰物或低端设备;需要真实皮肤、蜡或玉石质感时,应考虑厚度图、屏幕空间散射或成熟的材质方案。
数据流、性能与排查
main.js 创建球体、平面或描边双层网格,并将光方向、法线贴图或环境图交给 ShaderMaterial。顶点的 position/normal/uv/tangent attribute 经顶点 Shader 变换;varying 在三角形内部插值;片元 Shader 根据法线、视线、光方向和采样纹理计算颜色,再由 Three.js 处理输出颜色空间。动态水面另由 JS 更新 uTime。本地法线图与环境图位于 exercises/assets/,从 exercises/ 启动静态服务器即可加载。
法线贴图、环境图、多次高光都增加纹理读取或数学成本。先按效果选择内建材质或手写 Shader,再在目标屏幕尺寸和设备测量。全黑时先把法线映射为 0.5*N+0.5 输出颜色,确认坐标空间和方向;高光消失时检查 L、V 是否归一化及 reflect 的入射方向符号;法线凹凸颠倒时检查 Y 通道约定和 tangent.w;环境图 404 时检查相对路径。公共运行时在页面离开时释放材质、几何体和纹理。
练习
| 难度 | 题目 | 可见验收 |
|---|---|---|
| ⭐ | 手写 Lambert 漫反射 | 球体明暗面 |
| ⭐ | 给 Lambert 加环境光 | 暗面可见 |
| ⭐⭐ | Phong 反射高光 | 集中亮斑 |
| ⭐⭐ | 菲涅尔边缘光 | 可调亮边 |
| ⭐⭐ | 切线空间法线贴图 | 表面凹凸明暗 |
| ⭐⭐⭐ | 三档卡通光照 | 三档色块 |
| ⭐⭐⭐ | 时间扰动法线做水面高光 | 移动的亮斑 |
| ⭐⭐⭐ | 沿法线外扩的反面描边 | 稳定黑色轮廓 |
| ⭐⭐⭐⭐ | 环境反射与镜面高光的金属 | 摄影棚光带反射 |
| ⭐⭐⭐⭐ | 背光与边缘透光的玉石感 | 柔和透光边缘 |