Appearance
光影之协奏
背景:环境预设切换时,天空变了,但后处理管线(曝光/色调映射)还在用上一曲的乐谱。 过程:
applyEnvPreset同步调用setRenderState推送曝光/色调映射,后处理与环境预设形成完整"光场"。
环境系统把天穹立了起来,物理系统让裙摆能飞,多相机让视角能舞——但 Riku 总觉得,联邦还缺一层东西。
他站在屏幕前,看着同一个场景反复切换:正午、夕阳、夜景、阴天。天空颜色在变,太阳位置在变,灯光方向也在变。但不知为何,有些时刻看起来对,有些时刻看起来假。
正午的天空明明很亮,画面却发灰。夜景的星空已经点亮,地面却亮得像下午。夕阳的橙红铺满了天穹,可模型的边缘没有那层金边辉光。
Riku 把问题抛给 Jieling。
"光是对的,影是错的;天是对的,地是错的。"
这不是某一个城邦的 bug。这是两个城邦——环境系统与后处理管线——没有统一指挥。
史官的第一份奏报:两个独奏者
在 Babylon 的世界里,后处理是一串挂在 DefaultRenderingPipeline 上的效果:泛光、景深、暗角、抗锯齿。它们各有各的参数,但需要一个统一的指挥——RenderState。
另一边,环境系统 EnvState 里有天空、太阳、地面、水、粒子。ENV_PRESETS 预设表已经把天空色、太阳角、光照强度、曝光和色调映射都打包好了。
问题就出在这里:applyEnvPreset 最初只调用了 setLightState() 来更新灯光,把天空色推导成方向光和半球光,却没有去碰管线里的曝光和色调映射。
于是黄昏的天空配上了正午的曝光,夜景的暗角没有跟上,正午的泛光阈值却按夜景来算。
光与影各弹各的,联邦听起来像一个没合过排的乐团。
追查:指挥棒在哪
Riku 打开 scene.ts 里初始化管线的代码:
typescript
export const pipeline = new DefaultRenderingPipeline("default", true, scene, [scene.activeCamera!]);
pipeline.samples = 1;
pipeline.fxaaEnabled = false;
pipeline.bloomEnabled = false;
pipeline.imageProcessingEnabled = true;管线的默认状态很保守:泛光关、FXAA 关、图像处理开。它就像一支把乐器都收在盒子里的乐团,等待 RenderState 来逐个发令。
setRenderState 是"人类手动调节"的接口。用户点开菜单,拖动曝光滑块,打开暗角开关——这些操作都被正确地反映到管线上。然而,当用户从"正午"切到"夜景"时,环境系统却忘了告诉管线:曝光该降了,色调映射该从 ACES 切到 Neutral 了。
Riku 在 env-lighting.ts 里找到 ENV_PRESETS:
typescript
noon: {
label: "正午",
skyColorTop: [0.53, 0.71, 0.91],
skyColorBot: [0.3, 0.5, 0.8],
sunAngle: 75,
exposure: 1.0,
toneMapping: 1, // ACES
...deriveLighting([0.53, 0.71, 0.91], 75),
},
night: {
label: "夜景",
skyColorTop: [0.05, 0.05, 0.15],
skyColorBot: [0.02, 0.02, 0.08],
sunAngle: -15,
exposure: 0.4,
toneMapping: 4, // Neutral
...deriveLighting([0.05, 0.05, 0.15], -15),
},曝光和色调映射明明就坐在每个预设里,却没人去念它们。城邦又犯了老毛病:每个城邦都自带完整配置,但联邦只读了一半。
根因:半截子指挥
applyEnvPreset 的核心流程是"先断联、再设置、最后恢复":
typescript
export function applyEnvPreset(name: string): boolean {
const preset = ENV_PRESETS[name];
if (!preset) return false;
const wasLinked = envAutoLink;
envAutoLink = false; // 防止自动推导互相覆盖
envSunAngle = preset.sunAngle;
setLightState({ /* ... */ });
// 后处理部分——原本缺失!
envAutoLink = wasLinked;
return true;
}setLightState 修改了方向光和半球光,却没动 imageProcessing.exposure 和 imageProcessing.toneMappingType。于是环境系统拼完了天空,后处理乐队却还拿着上一曲的乐谱。
更隐蔽的是水面。WaterMaterial 的反射依赖环境纹理,但程序天空是一个自绘渐变球体——WaterMaterial 看不到它,只能看到普通网格,水面反射出的是"世界本身"而不是天空。为了让水面有波纹,Riku 为它加了 Perlin 噪声凹凸纹理。但没有正确的曝光和色调映射,水再真,也只是"一面亮度错误的镜子"。
修复:完整的指挥
修复并不复杂:让 applyEnvPreset 在设置完光照之后,也调用 setRenderState 把预设的曝光和色调映射推送进管线。
typescript
setLightState({
dirColor: preset.dirDiffuse,
dirX: preset.dirDirection[0],
dirY: preset.dirDirection[1],
dirZ: preset.dirDirection[2],
dirIntensity: preset.dirIntensity,
hemiIntensity: preset.hemiIntensity,
});
setRenderState({
exposure: preset.exposure,
toneMapping: preset.toneMapping,
});这两行把后处理乐队拉进了环境 preset 的指挥下。正午用 ACES 的高动态、夜景用 Neutral 的柔和收敛、夕阳用 Reinhard 来压住那团橙红——每个天空都有了自己的影调。
同步地,太阳盘光晕也做了调整:太阳越高光晕越收敛,越低越铺张。天空与后处理共同完成一个完整的"光场"。
验证
Riku 逐个切换预设,验证光影是否同步:
| 预设 | 天空 | 后处理 | 结果 |
|---|---|---|---|
| 正午 | 明亮蓝 | ACES,高曝光 | 高光不发白 ✅ |
| 夕阳 | 橙红低垂 | Reinhard,中曝光 | 金边辉光正确 ✅ |
| 夜景 | 深蓝星空 | Neutral,低曝光 | 暗部层次分明 ✅ |
水面反射亮度与天空一致,不再像"发光的水银"。暗角与夜景曝光形成完整氛围。
ENV_PRESETS 的断言从 4 个同步到 7 个,223 个测试全部通过。
尾注
修复完成后,Jieling 在场景菜单里看到了一个新的层级:「后处理」。它下面有泛光、抗锯齿、边缘高亮、景深、暗角。每一个都连接着 RenderState 的一个字段,而 RenderState 又通过 applyEnvPreset 与环境预设绑定。
她终于不用手动调三个面板来换一个天空了。选「正午」,就是正午的完整光场;选「夜景」,就是夜景的完整影调。
Riku 在提交信息里写:
光影之协奏:让环境预设同时指挥后处理管线。
窗外是黄昏。屏幕上的夕阳也是。两个黄昏,一个在外面,一个在 DefaultRenderingPipeline 里。
教训:光与影要一起指挥,否则各弹各的。