10 · 材质参数与光照计算

返回系列目录

本章针对 Three.js r185。灯光不是在 shader 末尾给颜色随便乘一个系数:材质先形成自己的表面参数,renderer 提供灯光数据,光照模块在相容坐标空间中求每盏灯的贡献,再积累直接与间接光。改参数通常比替换整套光照循环更容易保持阴影和环境反射一致。

1. 四类光照结果

ReflectedLight 在 common 中声明,main 中初始化为零,包含 directDiffuse、directSpecular、indirectDiffuse、indirectSpecular 四个 vec3。这里的颜色是线性工作空间光照量,不是最终输出色。

漫反射的一项可简化为 Li * max(dot(n,l),0) * baseColor / PI。n 与 l 都应归一化且处在同一空间。物理材质的镜面项还依赖视线、粗糙度、菲涅耳和几何遮蔽等因素。把最终 outgoingLight 染成蓝色,会一起染掉灯光和反射;修改基础色则保持它们各自的物理角色。

2. specularmap_fragment:Phong 的高光强度

模块始终声明 float specularStrength;有 USE_SPECULARMAP 时读取 specularMap 红通道,否则设为 1。它位于法线处理之前,结果由 lights_phong_fragment 接收。

它不设置高光大小,也不是高光颜色:大小主要由 shininess 控制,颜色来自 specular。想让贴图某部分更容易出现高光,可以在此 include 后乘一个自定义 mask;想扩大亮斑则应该调整后续 material.specularShininess。

3. roughnessmap_fragment:Standard/Physical 的粗糙度因子

它创建 roughnessFactor,初值为 uniform roughness;有 roughnessMap 则乘绿色通道。此时只是表面输入,还未加几何粗糙度和最低值保护。

局部湿润可在其后注入:

#include <roughnessmap_fragment>
roughnessFactor = mix(roughnessFactor, 0.12, clamp(uDemoWetness, 0.0, 1.0));

这样保留原贴图区域差异,也让后续光照参数构造继续执行。不要在 JavaScript 每帧改整个 GLSL 字符串来表达湿度。湿度是运行时 float,源代码结构不变。

4. metalnessmap_fragment:金属度因子

它创建 metalnessFactor,初值 metalness,再乘 metalnessMap 蓝通道。roughness 的 G、metalness 的 B、AO 的 R 可组合成 ORM 数据纹理,因此这些图不应按颜色贴图解码。

金属度不仅改变高光强度,还改变漫反射和反射颜色的分配。若在光照完成后仅增加镜面结果,并不能等价地变成金属。已有 PBR 材质的纹理中间值常用于混合或抗锯齿,并不意味着物质本身一定是“半金属”。

5. lights_phong_pars_fragment:Phong 的函数与结构

全局模块定义 BlinnPhongMaterial,字段包括 diffuseColor、specularColor、specularShininess、specularStrength。它定义 RE_Direct_BlinnPhong 与间接漫反射函数,并通过 #define RE_Direct ... 等别名接入共享光照循环。

输入 directLight.direction、geometryNormal、geometryViewDir 都按观察空间使用。这里适合研究自定义 BRDF,但不适合仅为修改一处高光就整体复制。它依赖 common 中的结构以及 bsdfs 中的相关函数。

6. lights_phong_fragment:创建当前片元的材质参数

main 中的这个模块把 diffuseColor.rgb、specular、shininess、specularStrength 写入局部 material 结构。它不遍历灯,也尚未累计结果。

若需要程序化调整高光锐度,可以在其后写:

#include <lights_phong_fragment>
material.specularShininess = mix(8.0, 100.0, clamp(uDemoPolish, 0.0, 1.0));

这里 uniform uDemoPolish 应在全局声明并由 JavaScript 注入。该锚点不存在于 Standard/Physical 的主 shader,不能把这一补丁不加判断地安装到所有材质。

7. lights_physical_pars_fragment:物理模型定义

它在 main 外声明 PhysicalMaterial、多种 BRDF 和渲染方程函数。宏条件决定 clearcoat、sheen、iridescence、anisotropy 等扩展字段与函数是否存在。Standard 也使用它;名字 physical 不意味着仅 MeshPhysicalMaterial 可以使用。

这一大块是相互关联的物理计算库。若整体替换,需要承担多重散射、环境光照、扩展层和能量分配等兼容性。大多数产品效果只需要改变粗糙度或颜色输入,不需要维护一份 BRDF 源码副本。

8. lights_physical_fragment:参数整合与派生值

r185 创建 material 并设置 diffuseColor、diffuseContribution = diffuseColor.rgb * (1.0 - metalnessFactor)、metalness 等。specularColor 与 specularColorBlended 分别描述相关镜面参数。旧版本字段语义可能不同,不能仅靠搜索旧教程中的 material.diffuseColor 直接照搬。

roughnessFactor 会被设最低值 0.0525,加上由 nonPerturbedNormal 屏幕导数估计的 geometryRoughness,再限制不超过 1。这个设计避免高频几何法线产生过尖的不稳定高光。

若使用 IOR,还要根据 ((ior-1)/(ior+1))² 等关系生成镜面参数;Physical 扩展贴图也在此消费。直接在它之后只改 material.metalness,不会自动重新计算已经生成的 diffuseContribution 和 specularColorBlended。因此优先在 metalnessmap_fragment 后改 metalnessFactor,让所有派生量一起更新。

9. lights_fragment_begin:建立几何上下文并计算直接光

模块读取 normal 与 vViewPosition,创建 geometryPosition = -vViewPosition、geometryNormal 和 geometryViewDir。透视相机使用 normalize(vViewPosition),正交相机使用固定观察方向。它根据灯光数量宏展开点光、聚光、方向光及受支持的区域光循环。

循环先获取灯光方向与强度,再根据 receiveShadow 等状态乘阴影可见度,最后调用 RE_Direct 累计 reflectedLight。它还初始化间接光需要的 irradiance、iblIrradiance、radiance 等变量。

所以在 include 后覆盖 geometryNormal 已经太晚,直接光已算完。若想改变所有光照的方向,应该在 normal_fragment_maps 之后、lights_fragment_begin 之前调整 normal。这里的循环宏涉及灯光数量与 sampler 数组,不要用自定义 uniform 替换 NUM_POINT_LIGHTS。

10. lights_fragment_maps:收集贴图与环境照明

它把 lightMap 样本乘 lightMapIntensity 后加到 irradiance;在相应分支调用 getIBLIrradiance 与 getIBLRadiance 收集环境漫反射和镜面输入。此时数据是“入射光”,最终材质响应还在后面。

可在该模块之后、lights_fragment_end 之前添加自定义间接照明近似,但需要严格遵循对应变量的宏作用域。直接访问 radiance 在不定义 RE_IndirectSpecular 的 Phong 程序中可能不存在。

11. lights_fragment_end:完成间接材质响应

它调用 RE_IndirectDiffuse、RE_IndirectSpecular,把已收集的 irradiance/radiance 与 material、几何参数结合,写入 reflectedLight。Phong 的环境漫反射路径会先将 iblIrradiance 加入 irradiance。

在这之后改 reflectedLight 会直接调整已经算好的能量,可做美术控制,但它不再是材质参数修改。不要拿一个“统一乘 0.5”的效果证明粗糙度改变成功;粗糙度应同时影响高光形状和环境模糊,而不仅是亮度。

12. 场景:一块逐渐抛光的金属面板

输入是一张 roughnessMap 与 float uDemoPolish。将 roughnessFactor 在贴图结果和低粗糙度目标之间混合,保持 metalnessFactor 不变。场景需要有形状明确的光源或环境图,才能看出高光由散到聚;没有环境且没有合适灯光时,“没有明显变化”不代表 uniform 没上传。

将模型转动、相机移动、粗糙度归零与恢复,分别检查高光是否随表面和视线正确变化。再对比直接在 opaque_fragment 前放大 outgoingLight:后者只是变亮,无法模拟抛光。此类对照比只截一张图更能验证锚点选择。

练习(暂不提供答案)

  1. 为 Phong 添加独立的高光强度和锐度控制,说明两个参数各在哪个阶段修改。
  2. 为 Standard 实现局部湿润遮罩,同时保持金属度不变,并解释原因。
  3. 在 lights_physical_fragment 后仅改 metalness,列出会失配的派生字段并改正注入位置。
  4. 用同一材质对比直接光和环境光,验证粗糙度补丁是否同时影响两条路径。