12 · Physical 扩展与最终输出
本章以 Three.js r185 为准,连接 Physical 扩展与屏幕输出。某个视觉效果出现在画面最后,不代表应该在 shader 最后一行实现:湿润、涂层、薄膜干涉都影响光照模型,屏幕级变色则可能属于最终合成。先确定效果代表表面的什么物理量,再寻找对应阶段。
1. clearcoat_pars_fragment:涂层贴图参数
它在相关宏下声明 clearcoatMap、clearcoatRoughnessMap 等 sampler;清漆强度和粗糙度主 uniform 则在 meshphysical 主体条件区声明。不是全部 Physical 参数都藏在一个 pars Chunk 中。
clearcoat 表示独立涂层,不等于把基础 specular 翻倍。打开公开材质 clearcoat 属性可生成需要的程序分支,然后在已存在的参数流程内做程序化调制。只往 uniforms 里增加一个名叫 clearcoat 的值不会自动开启所有代码。
2. clearcoat_normal_fragment_begin:清漆基础法线
在 USE_CLEARCOAT 下,它创建 clearcoatNormal = nonPerturbedNormal。也就是说基础层应用 normalMap 后的 normal 不会无条件成为清漆法线。这样可表达下层粗糙纹理被平滑清漆覆盖。
若希望自定义变形同时影响两层,应该正确更新几何基础法线,而不是只在 normal_fragment_maps 后扰动 normal。这个阶段输出仍在观察空间。
3. clearcoat_normal_fragment_maps:独立涂层细节
在 USE_CLEARCOAT_NORMALMAP 下读取 clearcoatNormalMap,解码到 [-1,1],以 clearcoatNormalScale 缩放 XY,再经 tbn2 转到观察空间并归一化。tbn2 来自 normal_fragment_begin,不是此处临时随便创建的坐标轴。
这张图同样是数据纹理,不能当颜色图。基础层与涂层的 UV 可以不同;混淆 vNormalMapUv 和 vClearcoatNormalMapUv 会导致两层细节错位。
4. Sheen:没有独立的最终 sheen include
sheen 的参数在主 shader 和 lights_physical_fragment 设置,BRDF 在 lights_physical_pars_fragment,最后在主 ShaderLib 的 #ifdef USE_SHEEN 普通代码段中把 sheenSpecularDirect/Indirect 加到 outgoingLight。
它适合织物绒毛的边缘散射表现。若只想调整颜色或强度,优先公开属性或光照前的 material 参数。直接删除末段加法会改变与模型内部能量处理的关系,也会让代码高度依赖版本。
5. Iridescence:薄膜厚度与视角
iridescence_pars_fragment 声明薄膜相关贴图,iridescence_fragment 提供评估函数;lights_physical_fragment 设置 iridescence、iridescenceIOR、iridescenceThickness 等,lights_fragment_begin 根据观察角度计算相关 Fresnel 量。
因此程序化调厚度应在参数建立后、lights_fragment_begin 前完成,而且必须在 USE_IRIDESCENCE 中访问对应字段。若在灯光算完后才改厚度,已生成的颜色不会回溯重算。薄膜色随角度变化,与简单的时间 RGB 正弦波不同。
6. Anisotropy:切线方向也属于材质输入
各向异性主要在 lights_physical_fragment 内形成 anisotropy、alphaT、anisotropyT/B,依赖 tbn 和 anisotropyVector 或 anisotropyMap。方向图 RG 表达平面方向,B 控制强度;它不是普通基础色。
修改 roughness 后还可能影响 alphaT 等派生值,应该尽量在参数构造前处理输入。若 UV 退化或切线基底不稳定,拉丝高光会跳变;换一个 final color 锚点无法解决几何基底错误。
7. transmission_pars_fragment:体透射函数与资源
它提供透射所需参数与采样函数,包括对 renderer 准备的透射缓冲的采样、折射射线及体积衰减计算。该资源不等同于普通环境贴图;自定义传入一张 sampler2D 不能替代 renderer 的配套 pass。
使用 material.transmission、thickness、ior 等公开属性能让 renderer 知道需要哪条路径。把 opacity 调低只是 alpha 混合,不会因此获得同样的背景折射和厚度吸收。
8. transmission_fragment:混合透射的漫反射贡献
模块在 totalDiffuse/totalSpecular 已建立之后执行。它设置 transmission、thickness、attenuationDistance/Color,读取 transmissionMap 的 R 与 thicknessMap 的 G;利用世界位置、世界法线和相机方向调用体折射函数,再混入 totalDiffuse,并生成 material.transmissionAlpha。
这意味着在 include 后修改 material.thickness 对这一次计算已经太晚。若要程序化厚度,应在该模块内部“完成厚度赋值/贴图处理之后、函数调用之前”插入,或者用公开厚度贴图实现需求。此时需要局部展开 Chunk 副本,检查内部锚点,而不是硬找一个不存在的 thickness_fragment。
例如调制代码的插入位置可以依据当前源码中唯一的 vec3 pos = vWorldPosition;,但必须先检查唯一性,再对 THREE.ShaderChunk.transmission_fragment 的副本替换。这个锚点属于版本相关实现细节,应纳入升级检查和程序缓存签名。
9. opaque_fragment:写出线性结果与 alpha
此前主 shader 已组合 outgoingLight。opaque_fragment 在 OPAQUE 条件下把 diffuseColor.a 设为 1,在 USE_TRANSMISSION 下乘 transmissionAlpha,最后写 gl_FragColor = vec4(outgoingLight, diffuseColor.a)。
尽管名字是 opaque,它也存在于支持透明的主流程,不代表出现这个 include 就一定是不透明材质。这里之前适合对线性 outgoingLight 做明确定义的最终艺术处理;之后再改 outgoingLight 已经不会改变写出的 gl_FragColor。
shader.fragmentShader = shader.fragmentShader.replace(
'#include <opaque_fragment>',
`outgoingLight *= uDemoExposureScale;
#include <opaque_fragment>`
);
该 float 应在全局声明并注入,通常取非负。它会影响基础光照、反射、自发光等全部贡献,因此不是修改 base color 的替代方法。
10. tonemapping_fragment:HDR 到显示范围的映射
在 TONE_MAPPING 下对 gl_FragColor.rgb 调用 toneMapping。输入仍是线性光照结果;具体函数由 renderer 的程序生成配置提供。
在它之前乘 2 与之后乘 2 通常不相同,因为 tone mapping 是非线性函数。若为了让 emissive 更亮而把代码追加到 colorspace 后,会绕开预期的亮度处理。不要重复手写 ACES 处理又保留内置 toneMapping,否则会发生二次映射。
11. colorspace_fragment:转换到目标输出色彩空间
执行 gl_FragColor = linearToOutputTexel(gl_FragColor)。目标由实际渲染配置决定,并非任何离屏渲染都固定是屏幕 sRGB。
这里之后的 RGB 已不再保证是线性工作空间值。在此相乘、mix 或叠加,数学含义与光照阶段不同。通常应保留它,让自定义 diffuse/emissive 沿内置颜色链路输出。用 Color 从 CSS 色构建 uniform 与直接写 GLSL 常量也要注意输入空间差异。
12. fog_fragment:雾的最终混合
输入 vFogDepth 与雾参数,在 USE_FOG 下计算线性距离 smoothstep 或指数平方雾因子,再与 fogColor 混合。顶点 fog_vertex 根据观察空间位置提供深度。
只改 gl_Position 而不改 mvPosition 会使雾看见旧距离。把自己的颜色覆盖写在 fog 之后,则可能让物体不再融入雾中。如果这是标记或 UI 的故意效果,应明确它是最终覆盖。
13. premultiplied_alpha_fragment:匹配预乘混合约定
在 PREMULTIPLIED_ALPHA 下执行 RGB 乘 alpha。它不是用来让 opacity 生效的万能开关,而是匹配渲染器与材质的混合表示。
在它之后加入未预乘的高亮 RGB,可能在透明边缘形成错误光圈。自定义输出必须与所选 blend 约定一致,不能在保留该 Chunk 的同时提前无条件预乘一次。
14. dithering_fragment:输出抖动
在 DITHERING 下调用 dithering(gl_FragColor.rgb),配套函数由 dithering_pars_fragment 声明。细微噪声用于减轻色带,不是材质粗糙度,也不改变几何。
后续再完全覆盖 gl_FragColor 会丢掉它的作用。低位深渐变场景可做开启/关闭对照,但不要把任何噪点都归因于这个模块,alphaHash 与采样误差也可能产生噪声。
15. 裁剪与对数深度不可忽略
clipping_planes_pars_vertex/fragment 声明裁剪数据,clipping_planes_vertex 从 mvPosition 生成裁剪位置,clipping_planes_fragment 判断片元与平面的关系;alpha-to-coverage 分支还会调制 diffuseColor.a。位置、裁剪与输出必须保持同一形状。
logdepthbuf_pars_vertex/fragment 声明对数深度需要的数据,logdepthbuf_vertex 在 project_vertex 后生成对应值,logdepthbuf_fragment 在启用时写 gl_FragDepth。它缓解大深度范围下的问题,不是可随便删掉的装饰段。自行写深度还可能影响早期深度测试和其他 pass,应有明确需求。
16. 用执行阶段判断替换位置
要改表面属性,尽量在 light accumulation 前;要改透射厚度,进入 transmission 内部正确阶段;要改线性总亮度,在 opaque 前;要做输出级覆盖,则明确是否仍应经历 tone mapping、颜色转换、雾和预乘。
每一步都会消耗前一步的变量。修改已经被消费的输入,不会触发 GPU 重新执行之前的计算。GLSL main 是顺序程序,不是带自动依赖追踪的材质节点图。
练习(暂不提供答案)
- 为同一模型分别调整 base color、emissive、outgoingLight,解释关闭灯光时三种结果。
- 查找 transmission 的厚度消费位置,设计一个不会被内置赋值覆盖的调制方案。
- 比较 tone mapping 前后乘亮度的差异,并记录高亮区域的表现。
- 给双层表面设置不同 normalMap 与 clearcoatNormalMap,解释为何下层凹凸不必等于清漆凹凸。