11 · 环境、阴影与间接光

返回系列目录

本章依据 Three.js r185。环境图、阴影图、AO 和 lightMap 虽然都是贴图,却表示不同量:环境图提供入射光,阴影图帮助判断某盏灯是否被挡住,AO 近似局部间接遮蔽,lightMap 存储烘焙照明。选择替换位置必须先判断数据含义。

1. envmap_common_pars_fragment:环境资源声明

在 USE_ENVMAP 下声明 envMapIntensity、envMapRotation,以及 envMap sampler。ENVMAP_TYPE_CUBE 时为 samplerCube,否则为 sampler2D。这意味着不能给同名 sampler 任意换成不相容类型。

Standard/Physical 常使用 PMREM 处理后的 CubeUV 二维图集,不能用普通 textureCube 读取它。想调环境强度或旋转,优先使用材质/场景公开属性,让 renderer 管理资源与程序参数,而不是覆盖已有 uniform 对象。

2. envmap_pars_vertex:传统环境路径的 varying

Phong 主顶点 shader 包含它。根据 USE_ENVMAP、PHONG、normal/bump 等条件,定义 ENV_WORLDPOS 并声明 vWorldPosition;另一条路径则传递 vReflect,并声明 refractionRatio。

它决定反射向量在顶点还是片元计算。Standard/Physical 的环境路径主要在物理 IBL 函数里完成,不能因源码里没有同一个顶点 include 就认定它没有环境反射。

3. envmap_vertex:世界位置或反射方向

ENV_WORLDPOS 分支把 worldPosition 传给片元;另一分支通过相机到顶点的方向和世界法线计算 reflect/refract。正交相机方向与透视相机不同,源码有对应处理。

它依赖 worldpos_vertex 已运行,并且 transformedNormal 与真实形状一致。只修改 gl_Position 会留下旧的世界位置和法线,反射看起来就会“贴不住”运动表面。

4. envmap_pars_fragment:传统环境的片元参数

它声明 reflectivity、vWorldPosition 或 vReflect、refractionRatio 等。该模块和顶点声明必须由同一套宏产生相容 varying。这里不是计算混合结果的位置。

refractionRatio 是传统环境折射路径参数,不等于 MeshPhysicalMaterial 中基于厚度、IOR 与屏幕背景的完整 transmission。两种路径不能只因为都出现 refract 就视为同一个效果。

5. envmap_fragment:Phong 的末段环境混合

Phong 在生成 outgoingLight 后调用它。在相应立方体环境分支中,它根据世界法线与观察方向采样 envMap,并按 multiply/mix/add 模式结合 reflectivity 和 specularStrength 修改 outgoingLight。

这里已经是光照之后的组合,作用与 Standard 的粗糙度相关 IBL 不同。一个替换 envmap_fragment 的补丁不能直接应用到 meshphysical 主体,因为后者没有这个最终混合锚点。

6. cube_uv_reflection_fragment:PMREM 图集采样工具

这个全局模块提供 textureCubeUV 等工具,把三维方向和 roughness 对应到图集中的面与级别。调用者需要正确的 PMREM 图集布局和相关编译参数。

不要通过随便改变图集 UV 让反射“流动”,那可能跨入另一立方体面或粗糙度级别。若要旋转环境,应改方向或公开的旋转设置,保留图集寻址逻辑。

7. envmap_physical_pars_fragment:物理 IBL 采样

定义 getIBLIrradiance、getIBLRadiance 和 anisotropy 相关函数。输入 normal、viewDir 在观察空间,内部转成世界方向并应用 envMapRotation。漫反射路径用较模糊环境,镜面路径结合 roughness 采样不同级别。

反射方向的基本公式是 reflect(-viewDir, normal)。源码还会按粗糙度将反射向量向法线混合,以减少粗糙表面采集背后光照的问题。因此独立重写一个 reflect 再 textureCube 并不等于保留原 PBR 环境行为。

函数声明在此,但调用发生在 lights_fragment_maps。若希望调反射的物理粗糙程度,改 roughnessFactor;若要美术性地调环境颜色,应明确会影响哪条 IBL 路径,并核对实际函数调用。

8. shadowmap_pars_vertex:阴影投影所需数据

它声明各灯光的阴影矩阵、varying 坐标数组和阴影参数结构。数组长度受 NUM_DIR_LIGHT_SHADOWS 等宏决定。聚光灯投影纹理也可能需要 vSpotLightCoord,即使没有启用对应阴影采样。

这些矩阵和数据由 renderer 根据灯光配置上传,不要用 uniform uCount 随意替代数组长度。手动修改数量宏会让 CPU 实际上传、GLSL 声明与循环不一致。

9. shadowmap_vertex:把表面变到阴影查询空间

输入 worldPosition 与 transformedNormal。它先把观察空间法线转换成世界法线,用 shadowNormalBias 沿法线偏移采样位置,再乘每盏灯的阴影矩阵输出 varying。

对方向光/聚光灯,可概念化为 shadowCoord = shadowMatrix * worldPosition,fragment 再按坐标查深度。点光源使用对应距离采样逻辑,不能简单视为一台普通透视相机的深度图。

若主表面法线错误,normalBias 的偏移方向也会错,导致阴影痤疮或脱离。只把 bias 调大可能掩盖错误,同时制造悬浮阴影。

10. shadowmap_pars_fragment:阴影采样函数

这是全局工具与数据声明,包含深度比较、过滤、点光阴影相关函数及条件声明。实际调用主要在 lights_fragment_begin 的每盏灯循环中,返回可见度后乘 directLight.color。

因此只搜索一个名叫 shadow_fragment 的锚点无法理解这些材质的接收阴影。修改采样函数会影响过滤和兼容性;只是想某个物体不接阴影,使用 mesh.receiveShadow 更直接。不同灯型及阴影模式的分支也需分别核对。

11. lightmap_pars_fragment 与实际 lightMap 消费点

声明模块提供 sampler2D lightMap 和 lightMapIntensity,UV 在对应 UV 模块中生成。真正采样并加入 irradiance 的代码在 lights_fragment_maps 内,不存在本流程独立的 #include <lightmap_fragment>。

这个例子很适合练习锚点查找:看到功能名不代表能拼一个 include 名。若想修改 lightMap 贡献,可在本材质局部展开 lights_fragment_maps 的副本再修改明确代码,或设计自己的间接照明输入。不要全局篡改 ShaderChunk。

12. aomap_fragment:调制间接贡献

它在光照积累之后读取 aoMap 红通道,通过强度参数生成 ambientOcclusion,再乘 indirectDiffuse;Physical 的 clearcoat/sheen 间接高光和 Standard 环境镜面还做相应处理。

它不是把 outgoingLight 整体乘黑,也不应让自发光和每盏直接灯都同时变暗。人为的风格化总遮罩可以这么做,但不要称为等价 AO。AO 是数据纹理,读取值不应经过 sRGB 转换。

13. 真实问题:波浪动了,地面阴影却静止

主材质的 worldpos 和 shadowmap_vertex 描述的是“这个表面如何接收阴影”。生成阴影贴图时,renderer 另用深度/距离材质绘制投影物体。所以主材质正确的变形,不会自动进入那一遍渲染。

方向光、聚光灯需要 mesh.customDepthMaterial,点光源需要 mesh.customDistanceMaterial 的对应变形。两者必须共用相同时间值、振幅和公式。否则接收阴影的表面位置正确,投射出的遮挡形状仍旧错误。透明裁切也同理。

排查顺序可以是:停住时间看主轮廓;检查阴影 pass 是否安装同一补丁;确认使用正确灯型;再检查阴影相机范围、mapSize、bias。主材质 shader 里修改 shadowmap_vertex 不是替代阴影投射材质的办法。

练习(暂不提供答案)

  1. 在 Phong 与 Standard 主源码中查找环境路径,解释为什么不能共用 envmap_fragment 锚点。
  2. 用同一波浪物体分别接受和投射阴影,列出两条路径各自使用的 shader 数据。
  3. 给模型关闭 AO,观察直接灯、自发光与环境照明,验证 AO 主要影响哪些部分。
  4. 找出 lightMap 实际采样代码所在 Chunk,并设计不修改全局 ShaderChunk 的局部替换方案。