09 · 基础颜色、透明度与自发光

返回系列目录

Three.js r185 的 Phong 和 Physical 主片元 shader 都先建立 diffuseColor = vec4(diffuse, opacity) 与 totalEmissiveRadiance = emissive,再执行本章模块,最后进入光照。理解“修改输入”和“修改最终输出”的区别,就能避免灯光、纹理和透明效果互相打架。

1. map_pars_fragment:基础贴图声明

它在 USE_MAP 条件下声明 uniform sampler2D map。这是全局区代码,没有进行采样,也不会为你生成 UV。UV 的声明与计算在上一章介绍的模块中。

给材质配置 material.map 会由 renderer 配套提供宏、sampler 和对应 UV 参数。添加自己的 uDemoMask 则应自己声明 sampler 并注入 uniform,不要覆盖内置 map 的名字,否则 renderer 的更新逻辑可能与补丁争夺同一 uniform。

2. map_fragment:基础色贴图乘法

输入 map 与 vMapUv,输出更新后的 diffuseColor。内置实现采样 RGBA,并执行 diffuseColor *= sampledDiffuseColor;视频纹理可能走专门的解码分支。普通颜色贴图应在 JavaScript 配置正确 colorSpace,让采样进入线性工作空间,不要在补丁里无条件再执行 pow 解码。

在它之后追加 tint,可保留纹理细节:

#include <map_fragment>
diffuseColor.rgb *= uDemoTint;

若直接赋 diffuseColor.rgb = uDemoTint,会有意丢掉已计算的基础贴图 RGB。它仍会受后续灯光影响,但材质原色不再参与。要混合两种纹理,明确是混合纹理样本再乘材质颜色,还是将第二种颜色覆盖已有结果;两种写法视觉含义不同。

3. alphamap_fragment:独立透明遮罩

在 USE_ALPHAMAP 下,它读取 texture2D(alphaMap, vAlphaMapUv).g,乘入 diffuseColor.a。这里取绿色通道,而不是 alpha。数据纹理应保持 NoColorSpace 语义。

map 自带的 alpha、材质 opacity、顶点 alpha、alphaMap 是逐次相乘关系。若其中任意一项为零,最后就是零。把不透明黑白遮罩误放在 alpha 通道会产生意外结果,因为模块根本不读取那个通道。

要添加程序化遮罩,可以在 alphamap_fragment 后乘入权重,使后面的 alphaTest/alphaHash 能看到它。仅改输出 alpha 而不调整材质渲染状态,不保证会出现透明混合。

4. alphatest_fragment:决定片元是否存在

普通分支在 diffuseColor.a 小于 alphaTest 时 discard。开启 ALPHA_TO_COVERAGE 时则用 fwidth 与 smoothstep 对阈值边缘处理,再丢弃零覆盖部分。它是片元执行逻辑,不会改变实际网格三角形。

alphaTest 常用于树叶、铁丝网、镂空贴图等二值轮廓,可避免很多透明排序问题;它与半透明玻璃不同。若希望阈值对新遮罩生效,必须在此之前生成 alpha。把溶解 alpha 赋值放在该 include 之后,只会改变后续输出,无法让已经执行完的 discard 再运行一次。

材质的 alphaTest 应通过公开属性配置,避免仅手动声明相同名字的 uniform。阴影材质必须采用相同裁切条件,否则叶子会投下完整矩形阴影。

5. alphahash_fragment:以抖动模拟覆盖率

在 USE_ALPHAHASH 下,它将 diffuseColor.a 与 getAlphaHashThreshold(vPosition) 比较,不通过就 discard。阈值函数来自 alphahash_pars_fragment,vPosition 在 common 声明,并由 begin_vertex 写原始局部位置。

alphaHash 通过空间噪声分布近似半透明覆盖,不能保证与排序正确的物理透射相同,画面也可能有可见噪点。它的 vPosition 并不是自动更新后的世界位置;自定义变形后噪声固定在原始局部坐标,可能恰好有利于随物体稳定移动。若改变该策略,应同步所有相关 shader 阶段和阴影变体,而不是只更改一个名字。

6. emissivemap_fragment:调制自发光

模块从 emissiveMap 采样 RGB,再乘入 totalEmissiveRadiance。输入 emissive 在 renderer 侧包含材质相应配置;它与 diffuseColor 是独立通道。emissive 为黑时,即使贴图非常亮,相乘仍是黑。

设备状态灯可以在该模块后追加:

#include <emissivemap_fragment>
totalEmissiveRadiance += uDemoGlowColor * demoMask * uDemoGlowStrength;

这不会自动照亮旁边物体,也不会自动产生 Bloom。它使本表面向最终结果贡献亮度;需要 Bloom 后处理才可能形成光晕,需要实际灯光或其他全局光照方案才会照亮邻近对象。值大于 1 可以保留 HDR 亮度,但最终仍受 tone mapping 处理。

7. 真实例子:设备面板的警示扫描

假设已经有全局 varying vec2 vDemoUv,顶点端在 uv_vertex 后写 vDemoUv = uv。以下片段加在 onBeforeCompile 中,uniform 应在外部持有,持续更新 time.value:

shader.fragmentShader = shader.fragmentShader.replace(
  '#include <common>',
  `#include <common>
   uniform float uDemoTime;
   uniform vec3 uDemoGlowColor;
   uniform float uDemoGlowStrength;
   varying vec2 vDemoUv;`
);
shader.fragmentShader = shader.fragmentShader.replace(
  '#include <color_fragment>',
  `#include <color_fragment>
   float demoCenter = fract(uDemoTime * 0.2);
   float demoDistance = abs(vDemoUv.y - demoCenter);
   float demoMask = 1.0 - smoothstep(0.02, 0.06, demoDistance);
   diffuseColor.rgb = mix(diffuseColor.rgb, uDemoGlowColor, demoMask * 0.35);`
);
shader.fragmentShader = shader.fragmentShader.replace(
  '#include <emissivemap_fragment>',
  `#include <emissivemap_fragment>
   totalEmissiveRadiance += uDemoGlowColor * demoMask * uDemoGlowStrength;`
);

demoMask 在 main 的共同作用域中定义,并在稍后的 emissive 模块消费;不要把它声明在 #ifdef USE_MAP 里面,否则没有 map 的变体无法使用。扫描带到达顶部后回到底部,这里是明确的循环跳转,不是无缝距离;要让边界连续,需要采用环形距离 min(d, 1-d)。

这个效果同时修改受灯光影响的基色与不依赖直接灯光的发光。若只想保持物体的真实色彩,可以删掉 diffuseColor 混合而保留 emissive 注入。确认关闭场景灯光后的表现,就能区分两条通道。

8. 替换顺序与渲染状态

RGB 通常在 map/color 后、光照前处理;alpha 通常在 alphaTest/hash 前处理;emissive 通常在 emissivemap 后处理。最终 opaque_fragment 可能在 OPAQUE 宏下将 alpha 强制为 1,因此不能用“diffuseColor.a 已变小”推断最终透明度。

透明材质还涉及 transparent、depthWrite、排序、双面渲染等状态,不能全部由 fragment shader 的 alpha 控制。对于物理透射,应看 MeshPhysicalMaterial 的 transmission,而不是将 opacity 当作折射强度。修改 ShaderChunk 也无法替代场景渲染顺序的设计。

练习(暂不提供答案)

  1. 用同一张 RGBA 贴图分别作为 map 和 alphaMap,说明两个路径使用 alpha 信息的差异。
  2. 给扫描带增加基色开关和发光开关,在无灯场景中预测四种组合。
  3. 实现高度溶解,确保 alphaTest 在遮罩计算之后执行;列出阴影材质需要共享的数据。
  4. 比较 transparent、alphaTest 与 alphaHash 对一排相交树叶的效果,记录排序、边缘和噪点差异。