Skip to content

预设宪章

背景:三套预设子系统、两套同名 ENV_PRESETS、参数边界模糊。 过程:三层宪法厘清 24 个内置预设的治理边界。

Riku 盯着屏幕上的画面。

他刚刚点了一个名为「夕阳」的天空预设。天空变成了暖橙色,太阳贴在了地平线上方——一切正常。然后他顺手点了渲染风格里的「赛博朋克」。

画面没有变暗。

他等了一秒。两秒。还是没有。

「怪了。」

他又试了一次:先点天空的「霓虹夜」,再点渲染风格的「标准」。画面应该从霓虹的暗紫色跳回中性的白天光感——但曝光值还卡在霓虹夜的那一档,画面亮得刺眼。

Riku 深吸一口气,开始翻代码。

他找到 env-lighting.ts,打开 ENV_PRESETS

typescript
export const ENV_PRESETS: Record<string, EnvPreset & DerivedLighting> = {
    dawn: { label: '黎明', skyColorTop: [...], sunAngle: 5, exposure: 0.8, toneMapping: 1 },
    noon: { label: '正午', skyColorTop: [...], sunAngle: 75, exposure: 1.0, toneMapping: 0 },
    sunset: { label: '夕阳', skyColorTop: [...], sunAngle: 15, exposure: 0.7, toneMapping: 2 },
    ...
};

他往下翻,又翻到一个。

typescript
export const ENV_PRESETS: Record<string, EnvPresetConfig> = {
    '舞台-A': { env: {...}, lights: {...}, render: { exposure: 1.2 } },
    '户外晴天': { env: {...}, lights: {...}, render: { exposure: 1.4 } },
    ...
};

Riku 愣住了。他下意识地把两段代码摆在屏幕两侧,一左一右,像两个同名但异姓的陌生人。

两套 ENV_PRESETS

一个在 env-lighting.ts,管天空色和光照推导,顺手也管了 exposuretoneMapping。另一个在 env-preset-levels.ts,管全环境组合——天空、光照、渲染子集——也管 exposuretoneMapping

同一个名字,不同的文件,不同的数据结构,不同的管理范围。它们相互不知道对方的存在。但用户点天空预设的时候,exposure 被第一套覆盖了;点环境预设的时候,又被第二套覆盖了。谁后点谁赢,顺序完全随机。

Riku 把截图发给了 Jieling:

"我发现了两个同名的人。它们住在不同的文件里,管不同的东西,但都觉得自己说了算。"

Jieling 的回复隔了五分钟才来,只有一行:

"查一下它们各有多少个字段是交叠的。"

Riku 查了。十分钟后他给出了答案:

"天空预设之前有 exposure 和 toneMapping,渲染预设也有。环境预设的 render 字段里也有 exposure 和 toneMapping。三个地方都在设同一个值。应用顺序完全取决于用户点什么按钮。"

Jieling 回了一个字:

"拆。"

Riku 决定先列一个全局清单。

他打开所有跟预设相关的文件,一个一个数。数完之后,他对着屏幕叹了口气。

三套预设子系统。24 个内置预设。散落在三个菜单域中。

天空面板的「氛围预设」里有 10 个芯片,外加用户保存的系统预设快照——用户预设混在天空芯片组里,用户根本分不清哪个是系统给的、哪个是自己存的。

水面预设挂在 env-feature-levels.ts 里,夹在天空参数和粒子开关之间,像一个没有自己房间的房客。

渲染风格的六个芯片单独放在场景弹窗里,和灯光参数共享同一个面板。

exposuretoneMapping 同时出现在天空预设、渲染预设、环境预设三个地方。每次应用都不知道谁会覆盖谁。

Riku 给这份清单起了一个名字——「预设无政府状态清单」。

他把清单发给 陈眠。陈眠看完,只问了一个问题:

"用户能感知到吗?"

Riku 想了两秒:"能。用户点了夕阳之后点赛博朋克,画面可能还是夕阳的曝光值。用户要再点一次渲染预设才能"矫正"。用户以为是自己操作失误,其实是系统在打架。"

陈眠点了点头:"那就改。"

Riku 又看了一遍自己的清单。24 个预设,数量不多。乱的不是数字,是结构。

他想起 Jieling 曾经说过一句话:"当同一件事有三种做法的时候,问题不是选哪一个,而是为什么没有人停下来问该不该有三种。"

他决定停下来。

天空预设只管天空颜色、太阳角度和方位角。渲染预设只管后处理参数、色调映射和曝光度。水面预设只管水色、波浪和泡沫参数——每一个域的边界清晰得像国境线。

但它们在代码里的表现完全是另一回事。天空预设手里捏着 exposuretoneMapping——这两个值实际上属于渲染管线。水面预设挂在 env-feature-levels.ts 里,没有一个独立的"家"。

而两套 ENV_PRESETS,同一个名字,不同的人,在一座城市里并行执政。

Riku 打开一个空白文档,敲下了第一行字:

"预设宪法草案——三级架构"

他的构思是这样的:把所有预设分成三个层级,每级的权力范围用白纸黑字写死。

第一级,叫 L1 单域预设。只管自己的一亩三分地。天空氛围预设只能摸天空的颜色和太阳角度——skyColorTopskyColorBotsunAngleazimuth。渲染风格预设只能摸后处理参数——bloomtoneMappingexposurevignette。水面预设只能摸水——waterColorwaterWaveHeightfoam*。每一颗钉子都有自己的墙,没有一颗钉子能敲进别人的墙里。

第二级,叫 L2 环境预设。跨域组合型——可以同时设置天空、光照、渲染子集、地面、水、粒子。但权力不是无限的。L2 不能碰 bloom*dof*chromaticAberration* 这类高级后处理——那些是 L1 渲染风格的专属领地。L2 可以选"这场景用低曝光 + 暖色光",但不能替用户决定要不要开景深。

第三级,叫 L3 场景预设。全量快照——模型、动作、音频、舞台、灯光、相机、环境、渲染、水面,整个场景一次打包。这一级没有内置预设,全部由用户创建。

Riku 盯着自己的草图看了一分钟。三个层级,从下往上,权力递增,但边界递减——L1 说死不能碰别人的,L2 说可以碰一部分但有限制,L3 说管全部但交给用户自己决定。

他把草图发给了 Jieling。Jieling 回了一个字:

"B+"

Riku 愣了一下。"B+什么意思?"

"思路对,细节不够。L1 减少了几个?L2 增加了几个?命名怎么统一?"

Riku 重新打开了自己的草案。

他开始逐域清点。

L1 天空目前有 10 个预设。按时间线排列:黎明、正午、黄昏、日落、傍晚、夜景、阴天、暴风雨、樱花、演唱会。

看起来整齐,但仔细一看——dusksunset 的区别在哪里?他打开数据看了一眼:sunset 的太阳角是 15°,dusk 是 3°。差了 12 度,肉眼几乎看不出区别。storm 的数据和 overcast 几乎完全一样,只是在 skyBrightness 上低了一些——用 overcast 拉低亮度完全可以替代。sakura 色彩特殊,但使用频率极低——更适合做成完整的 L2 环境组合,而不是单独一个天空色。concertneon 功能重叠,演唱会场景更应该作为一个包含灯光和粒子的组合预设存在。

他的手停在键盘上。砍。

dusk 合并进 sunsetstorm 合并进 overcastsakuraconcert 从 L1 移除。

保留六个:dawn(黎明)、noon(正午)、sunset(夕阳)、night(夜景)、overcast(阴天)、neon(霓虹夜)。

「正好覆盖一条完整的时间线——从黎明到午夜,从晴天到阴天,从自然光到人造光。」

L1 渲染风格六个——standardcinematiccartoonrealisticwarmcyberpunk——不乱,不动。

L1 水面五个——calmrippleoceanstormtropical——也不乱,不动。

然后他打开 L2 的面板。

原本只有 3 个系统预设,冷清得像一个只摆了三件展品的画廊。Riku 决定把 L2 做成真正的精选展柜。

他把原来的三个保留并重新命名——「舞台-A」「户外晴天」「演唱会」。然后新增五个——「摄影棚」(模型展示专用,纯色天幕加三点布光)、「黄昏柔光」(暖色渐变加柔焦)、「雨天」(灰色调加雨粒子)、「樱花季」(从 L1 降下来的粉色天空加樱花粒子,以更完整的形态复活)、「赛博都市」(深蓝黑霓虹加荧光粒子)。

8 个 L2 环境预设。覆盖了最常见的使用场景。从舞台表演到户外展示,从摄影棚到雨天,从演唱会到黄昏。

那两个从 L1 被移除的名字——sakuraconcert——在 L2 里找到了更体面的位置。它们不再是孤零零的天空色。它们带着天空、光照、渲染、粒子一起回来,成了一个完整的场景氛围。

「它们没有消失,」Riku 心想,「它们只是升了级。」

他数了一遍总数。

L1 天空 6 个 + L1 渲染 6 个 + L1 水面 5 个 + L2 环境 8 个 = 25 个。

等等。

改之前是 24 个。改完之后是 25 个。

他揉了揉眼睛,又数了一遍。24 → 25。多了 1 个。

他把这个发现告诉 陈眠。陈眠没有马上回复,过了好一会儿才发来一行:

"你清掉了 4 个天空预设,但加了 5 个 L2 组合。总数还多了 1 个。但你说这样更清晰?"

Riku 想了两秒,打字回复:

"以前有 24 个预设分散在三张不同的菜单里,有些同名不同人,有些同人不同名,应用顺序随机,用户点完一个还要猜另一个会不会被覆盖。现在有 25 个,但每一个都知道自己属于谁、能管什么、不能管什么。"

他顿了顿,又补了一句:

"混乱的 24 个,不如有序的 25 个。数目的增减从来不是核心问题——治理才是。"

陈眠回了一个大拇指。

Riku 开始做命名规范。

他之前从来没有认真想过这件事。预设的名字,不就是随便取一个嘛。但这一次,他决定立规矩——宪法不但要管权力的边界,还要管名字的格式。

L1 的 key 用英文小写无连字符:dawnnoonsunset。label 用中文双字:黎明、正午、夕阳。两个字,不多不少,像军衔一样整齐。

L2 的 key 用 kebab-case:stage-asunset-glowcyber-city。label 用中文四字以内:舞台-A、户外晴天、赛博都市。四字以内,描述场景全貌,足够用户一眼看懂,又不至于太啰嗦。

他加了一条铁律:禁止同名预设跨域

那两套 ENV_PRESETS——整改之后,一个改名为 ENV_PRESETS(仍属 env-lighting.ts,但只管 L1 天空),另一个继续保持 ENV_PRESETS(属 env-preset-levels.ts,管 L2 环境组合)。前者不再包含 exposuretoneMapping,后者在 key 上加了中文标识——从代码层面直接区分了两套预设的用途。

但 Riku 不放心。他在 env-preset-levels.ts 里改了一个 import 的别名:

typescript
import { ENV_PRESETS as ENV_LIGHTING_PRESETS, ... } from '../scene/env/env-lighting';

ENV_LIGHTING_PRESETS。不是 ENV_PRESETS。名字变了,就不会再产生歧义。给下一个读代码的人一个明确的路标。

「同一个项目,同一座城市。不能让两个同名的人同时执政。」

代码层面的改动,比 Riku 预想的要少。

EnvPreset 接口从 env-lighting.ts 里移除了 exposuretoneMapping 两个字段。六个天空预设的数据跟着刷新了一遍。duskstormsakuraconcert 四个条目被移除——代码行数减少了,但可读性反而提升了。

L2 的 env-preset-levels.ts 里,新增了五个组合预设。exportEnvPreset 的版本号从 1 升到 2,向后兼容——用户已经保存的自定义预设文件仍然能正常加载,多余的字段被忽略。

水面从 env-feature-levels.ts 里独立出来——不再作为一个"功能层级"里的小模块存在。它有了自己的构建函数,自己的管理空间。

Riku 改完最后一个文件,敲下命令:

npx tsc --noEmit

零错误。

他又敲:

npx vite build

构建通过。

他打开应用,一个一个地测试。点天空的「黎明」——只有天空变了,曝光值不变。点渲染风格的「赛博朋克」——后处理和色调映射变了,天空不变。点 L2 的「樱花季」——天空变成了粉色,光照变成了暖色柔和,粒子变成了飘落的樱花瓣,曝光值调高了一档——一切同时变化,但每个变化都来自它该来的地方。

他点了一下「保存当前为预设」。弹窗提示输入名称。他输入了"demo"。

然后他又点了一下「夕阳」。天空从粉色变成暖橙色。曝光值不动——它归渲染风格管,天空预设动不了它。

「对,这才对。」

Riku 提交了 commit,在 message 里写:

refactor: preset governance — three-tier constitution (L1/L2/L3)

  • EnvPreset removed exposure/toneMapping — Render style exclusively owns them
  • L1 sky presets: 10 → 6 (removed dusk/storm/sakura/concert)
  • L2 env presets: 3 → 8 (added studio/sunset-glow/rainy-day/sakura/cyber-city)
  • Naming convention: L1 camelCase + 2-char Chinese, L2 kebab-case + ≤4-char Chinese
  • User presets moved from sky panel to env preset level
  • Water promoted to independent sub-panel
  • exportEnvPreset v2 (backward compatible)
  • Total: 24 → 25 built-in presets, but structural chaos → structural clarity

他盯着最后一行看了一会儿。24 → 25。预设的总数变多了,但混乱感变少了。

「这不对吧?」他想,「数多了,乱少了?」

但他知道这是对的。就像一段写满重复代码的 200 行函数,重构之后变成了 5 个 50 行的函数——总行数多了 50 行,但人脑处理起来轻松了五倍。数的增加,换来的是理解的降低。

治理不是为了精简数目。治理是为了让每一个存在都有自己的位置。

Riku 关掉终端,看着屏幕上映出他自己的倒影。

宪法不是用来管束的。宪法是用来让自由变得有序的。

那 25 个预设,每一个都知道自己属于谁,能管什么,不能管什么。它们不再打架了。它们各司其职,像一座运转良好的联邦——三级架构,三层宪法,一条铁律:不相侵,不相扰

窗外的天色已经暗了。Riku 伸了个懒腰,关掉了电脑。

明天还有更多的代码要写。但至少今天,24 变成了 25,而混乱变成了秩序。