40 · csm_DiffuseColor:修改进入光照的基础颜色

返回系列目录

本章针对 three-custom-shader-material 6.4.0 与 Three.js r185。完整对照案例见 csm-color-comparison.js,运行方式沿用 README 的 Vite 模块入口。左球修改基础色,中球修改自发光,右球修改输出颜色。场景自行添加灯光;公共 runtime 不提供灯光。

1. 它替换的究竟是什么

csm_DiffuseColor 是片元阶段的 vec4。对于常用 Phong、Standard、Physical,CSM 在自定义 main 前把它初始化为:

// 未设置 map 时
csm_DiffuseColor = vec4(diffuse, opacity);
// 设置 map 时的概念表达,普通贴图已按 renderer 配置进入工作颜色空间
csm_DiffuseColor = vec4(diffuse, opacity) * texture2D(map, vMapUv);

用户代码先修改这个中间值。随后内置 main 继续运行,在 color_fragment 后 CSM 插入:

#include <color_fragment>
diffuseColor = csm_DiffuseColor;

因此它不是“在光照结束后给屏幕染色”,而是替换光照计算尚未消费的 diffuseColor。Phong 用它计算漫反射;Standard/Physical 还用它确定金属的反射颜色及非金属的漫反射贡献。灯光、法线、高光和阴影仍参与后续计算。

这也是它与手动注入的直接对应:手写 onBeforeCompile 时,你需要自己找到 color_fragment 并插入同样的赋值;CSM 将这个常用位置做成了语义接口。自定义 main 不等于内置 main 末尾,不能在其中读取尚未计算的 reflectedLight 或 roughnessFactor。

2. 覆盖与相乘有不同的业务含义

// 重新定义基础色,原 color 和 map 的 RGB 不再保留。
csm_DiffuseColor.rgb = vec3(0.2, 0.05, 0.01);

// 对初始化后的 color × map 染色。
csm_DiffuseColor.rgb *= vec3(0.8, 0.5, 0.2);

// 只修改 RGB,保留初始化时的 opacity × map alpha。
csm_DiffuseColor.rgb = mix(csm_DiffuseColor.rgb, uDemoAlarmColor, alarm);

三个常见需求分别是:程序化材质完全生成颜色、给已有模型统一染色、设备报警时逐渐变红。它们不应都写成整个 vec4 的固定赋值。将 alpha 固定为 1 会丢掉原来的 opacity/map alpha;只更新 RGB 则不会。

CSM 通过源码中是否出现变量名决定启用对应补丁,而不是只识别赋值语句。即使只是读取 csm_DiffuseColor,也可能触发覆盖。删除该变量的使用,比写一个“看似没有作用的赋值”更接近恢复原材质路径。

3. 顶点颜色为何可能消失

内置顺序是 map_fragment → color_fragment → alphamap_fragment → alphatest_fragment。CSM 初始化值包含材质颜色和 map,但没有把顶点颜色乘进去。等 color_fragment 乘完 vColor,后面的整体赋值又覆盖了这个结果。

若需要保留普通顶点颜色,显式加入:

void main() {
  #if defined(USE_COLOR) || defined(USE_COLOR_ALPHA)
    csm_DiffuseColor *= vColor;
  #endif
  csm_DiffuseColor.rgb *= uDemoTint;
}

声明 uniform vec3 uDemoTint;,JavaScript 提供 { value: new THREE.Color(...) }。r185 的 vColor 在上述两个分支下都是 vec4;普通 RGB 顶点颜色对应的 alpha 初始化为 1,所以可以统一乘 RGBA。不要沿用旧版本 diffuseColor.rgb *= vColor 的写法,否则 vec3 与 vec4 类型不匹配。vColor 由内置 ShaderChunk 根据宏声明,不要另行同名声明。实例颜色、颜色变形也可能沿顶点颜色链路到达这里,应针对真实模型验证,而不是仅测试没有 color attribute 的球体。

alphamap_fragment 和 alphaTest 在覆盖后运行,所以仍可进一步调制/裁切新 alpha。透明混合、深度写入和排序则属于材质与渲染状态:写 alpha 不会自动打开 transparent。遮罩裁切可优先采用 alphaTest,连续半透明则需要单独分析排序与 depthWrite。

4. 实践:条纹设备外壳

示例顶点阶段把 uv 写入自定义 varying,片元阶段使用插值后的 UV:

float stripe = smoothstep(0.35, 0.65,
  0.5 + 0.5 * sin(vDemoUv.y * 48.0 - uDemoTime * 2.0));
vec3 pattern = mix(vec3(0.025, 0.12, 0.3), vec3(0.8, 0.18, 0.04), stripe);
csm_DiffuseColor.rgb = pattern;

sin 的输出从 [-1,1] 映射到 [0,1],smoothstep 形成有软边的条纹。48 控制空间频率,时间系数 2 控制相位速度;两者之比决定条纹沿 UV 的移动速度。UV 跨接缝时函数未必连续;高频条纹在远处可能混叠,生产中可采用导数抗锯齿或可 mipmap 的纹理。

这里 GLSL 常量颜色是线性工作空间值。JavaScript new THREE.Color(0x...) 会在默认颜色管理下转换输入 sRGB;不要以为两种写法的相同数值总是同一种颜色。真实彩色贴图标记为 SRGBColorSpace,roughness/metalness/遮罩纹理则是数值数据,不应标记 sRGB。

观察左球:背光处暗,受光处亮,高光覆盖在条纹上。把灯光强度降到零可验证基础色本身不发光。与第 41 章的右球对比,比单看漂亮截图更容易确认替换阶段。

5. 练习(不附答案)

  1. 给左球加入一张有 alpha 的 map,对比修改 .rgb 和整个 vec4 的结果,并分别解释 alphaTest 与 transparent 的作用。
  2. 为球体添加 color attribute,开启 vertexColors,复现颜色丢失,再用宏保护代码恢复。
  3. 将扫描从 UV 高度改成世界高度,让两个旋转不同的设备共享同一水平扫描带。
  4. 将告警颜色放进 THREE.Color uniform,用状态值平滑混合,并说明为什么更新该值不需要重编译。