41 · csm_FragColor、csm_UnlitFac 与 csm_Emissive

返回系列目录

“颜色不够亮”可能是灯光不足、基础色过暗,也可能是需求本来就是屏幕、指示灯或调试可视化。三种问题对应不同注入位置。继续运行 csm-color-comparison.js:左侧受光,中间自发光,右侧直接提供未受光颜色。

1. csm_FragColor 的位置不是最终 framebuffer

CSM 6.4.0 将 opaque_fragment 替换为:

#include <opaque_fragment>
gl_FragColor = mix(gl_FragColor, csm_FragColor, csm_UnlitFac);

此时内置光照与自发光已经累计,opaque_fragment 已根据材质模式处理输出 alpha。但后面仍有 tone mapping、工作空间到输出颜色空间转换、雾、预乘 alpha 和 dithering。因此 csm_FragColor 是线性输出链路中的候选颜色,不是“已经编码好的屏幕像素”。把 sRGB 图片的原始编码数值手工直接当线性值写入会造成亮度偏差。

csm_FragColor 默认与 csm_DiffuseColor 一样从 diffuse、opacity、map 初始化。源码使用它时,CSM 会在自定义 main 正文之前插入 csm_UnlitFac = 1.0;。所以只写:

void main() { csm_FragColor = vec4(0.1, 0.3, 0.8, 1.0); }

默认得到完全不受材质灯光影响的颜色。但 tone mapping 和雾仍能改变它;“不受灯光”不等于“不受全部渲染阶段影响”。只为调试关闭 toneMapped 可以用材质属性;若要求屏幕 UI 不受雾和场景曝光影响,应考虑单独的 UI 渲染路径。

2. csm_UnlitFac 是混合权重

公式为:

C=(1−f)Clit+fCcustom,f=csm_UnlitFac C=(1-f)C_{\text{lit}}+fC_{\text{custom}},\qquad f=\text{csm\_UnlitFac}
uniform float uDemoUnlit;
void main() {
  csm_FragColor = vec4(0.05, 0.3, 0.8, 1.0);
  csm_UnlitFac = clamp(uDemoUnlit, 0.0, 1.0);
}

这允许模型从正常材质过渡到选中状态的平面颜色。自定义 main 内的赋值在自动设置 1 之后,因而会生效。仅使用 csm_UnlitFac 而不出现 csm_FragColor,6.4.0 不会独立安装 opaque 混合补丁;务必同时使用后者。mix 同时混合 RGBA,不仅是 RGB;它也不会为你开启透明渲染。

f=0.5 是艺术化颜色插值,不是物理意义上“发出一半光”。透明对象更需要观察 alpha 是否被覆盖,因为这次操作发生在 opaque_fragment 之后;已有 discard 不会因为晚些时候重新赋值 alpha 而被撤销。

3. csm_Emissive 修改自发光输入

CSM 6.4.0 为 csm_Emissive 只开放 MeshStandardMaterial 和 MeshPhysicalMaterial。Phong 虽然原生拥有 emissive 属性,也不能据此推断该 CSM 变量支持 Phong;使用基础 emissive 属性或手动补丁是可选方案。

其默认值是 emissive uniform,Three.js 上传时已把基础 emissive 颜色和 emissiveIntensity 合并。实际替换为:

// 内置代码原本为 totalEmissiveRadiance = emissive
vec3 totalEmissiveRadiance = csm_Emissive;

位置在 emissivemap_fragment 之前,因此 emissiveMap 后续仍会乘到此值。如果写 csm_Emissive = myColor,你覆盖的是已含基础强度的输入,emissiveIntensity 不会在后面再自动乘一次;如果写 csm_Emissive *= pulse,才保留初始化的颜色与强度。

自发光进入 outgoingLight 时与反射光相加,Physical 外层 clearcoat 等步骤还可能继续调整它。它保留物体表面的其他反射细节,不会像完全 unlit 那样全部替换光照结果。它也不会自动照亮旁边的物体,更不会自动产生屏幕光晕;后者需要 bloom 等后处理,前者需要实际灯光或合适的全局光照方案。

4. 实践:设备指示灯与故障高亮

float pulse = 0.5 + 0.5 * sin(uDemoTime * 4.0);
csm_DiffuseColor.rgb = vec3(0.03);
csm_Emissive = uDemoSignalColor * (0.2 + 2.0 * pulse);

JavaScript 用 { value: new THREE.Color(0x35aaff) } 对应 uniform vec3 uDemoSignalColor,时间用 { value: 0 } 对应 float。每帧只改包装对象的 value。大于 1 的 HDR 值适合自发光,但显示器输出会经过 tone mapping,不一定线性变亮。

对于设备告警,发光区域应由 UV 遮罩或顶点权重决定,避免整个设备都成为发光体。若只是调试某个 mask,csm_FragColor = vec4(vec3(mask), 1.0) 更直接;若是实际指示灯,则使用 emissive 并保留表面材质。

5. 练习(不附答案)

  1. 把场景灯光强度设为零,预测并观察三个球各剩下什么颜色。
  2. 同时增加雾和修改曝光,验证 unlit 球是否仍变化,并解释阶段顺序。
  3. 将材质设为 Phong,设计一个不使用 csm_Emissive 的告警灯实现。
  4. 为 unlit 混合添加 0~1 动画,分别测试 opaque 和 transparent,检查 alpha 路径。