Skip to content

追光的人

背景:模型走到哪儿,灯光不会跟——角色挪出主灯范围就变黑,画面里光与人脱节。 过程:每 actor 自动挂一盏 SpotLight 跟随根节点(腰骨候选匹配),可调强度/角度/颜色/光锥/阴影,disposeLighting 统一释放。


桌面壳让模型走了一圈。

模型从舞台中央走到角落,又走回来。动作没问题,骨骼没问题——但灯光有问题。

"它走进黑暗里了。" 桌面壳说,"主灯照着舞台中央,模型一离开,半个身子就没入黑暗。它明明是主角,为什么光不跟着它走?"

"光为什么要跟着它走?" 外交官问。

"因为它是主角。" 桌面壳说,"舞台上的主角,走到哪儿,光就应该跟到哪儿——这是舞台的基本法则。我的光只会照着固定的地方,像一根钉死的路灯,主角走远了就够不着。"

"舞台灯是固定的,追光灯是移动的。"

"我需要一盏追光灯。"


一、一盏灯,一个人

桌面壳找到了 attachPersonalLight

"每个 actor 模型,自动获得一盏 SpotLight。" 它说,"SpotLight——聚光灯,像舞台追光灯:一束光,从上方打下来,罩住一个人。"

typescript
// 职责: 每个 actor 模型自动获得一盏 SpotLight,跟随其根节点移动。
// 注册于 initLighting(onBeforeRenderObservable),disposeLighting 时释放。

"这盏灯属于这个模型。" 它说,"模型在哪儿,灯在哪儿——不是灯追着人,是灯和人长在一起。灯挂在模型的根节点上,像影子一样甩不掉。"

"每一帧都更新位置?"

"对。" 桌面壳说,"tickPersonalLights——每帧把灯的位置挪到模型根节点上方。模型平移、旋转、缩放,灯都跟着。舞台中央那盏主灯可以钉死,但个人灯必须跟着人走。"


二、腰还是脚

但有一个问题:灯该挂在模型的哪儿?

"挂在脚底?" 桌面壳比划了一下,"那光从脚底往上打——人站在光里,脸是黑的。挂在头顶?光从头顶打下来,人是亮的,但灯会穿透模型。"

"挂在腰上?"

"挂在腰上。" 桌面壳说,"躯干是人的重心,也是光最自然的高度——从腰的高度斜上方打下来,照亮全身,又不会穿模。"

它写了一份腰骨候选名单:

typescript
/** 腰骨候选名(按优先级),用于个人灯跟随躯干而非脚底 */
const WAIST_CANDIDATES = ['Waist', 'センター', 'Center', '腰', '上半身'];

"不同模型的腰骨叫法不一样——英文模型叫 Waist,日文模型叫 センター,中文模型叫 腰。" 桌面壳说,"按优先级逐个找,找到哪个用哪个。一个没有腰的模型?"

"有兜底吗?"

"有。" 它说,"候选全找不到,就退回根节点——总比没有强。追光灯可以跟不准腰,但绝不能不知道人在哪儿。"


三、灯的参数

个人灯不是一盏死灯——它有一整套可以调的脾气。

typescript
export interface PersonalLightSettings {
    enabled: boolean;       // 开关
    intensity: number;      // 强度
    color: [number, number, number];  // 颜色
    angle: number;          // 光束角度
    height: number;         // 挂灯高度
    offsetX: number;        // 前后偏移
    offsetZ: number;        // 左右偏移
    coneEnabled: boolean;   // 光锥可视化
    ...
    shadowEnabled: boolean; // 是否生成阴影
    shadowResolution: number;  // 阴影分辨率
}

"十二个参数。" 桌面壳说,"强度管亮不亮,颜色管什么光,角度管光束宽窄,高度管挂多高——每一盏灯都是可以调的,每个模型都可以有自己的追光风格。"

"为什么还要 shadowEnabled?"

"因为阴影是贵的。" 它说,"一盏灯生成阴影,要渲染一次阴影贴图——分辨率越高越贵。不是每个模型都需要脚下的影子:主角要影子,背景道具就不要。shadowEnabled 让阴影按需开启,省下的性能留给画面。"

"光锥呢?"

"光锥是给用户看的指示器。" 桌面壳说,"灯是看不见的,用户调角度只能靠猜。光锥把光束的形状画出来——你调 angle,光锥变宽变窄,所见即所得。指示器不是光本身,是光的说明书。"


四、灯的家务

灯多了,就要做家务。

"一个模型一盏灯,十个模型十盏灯。" 桌面壳说,"每一盏都要创建、更新、销毁——创建了不销毁,就是内存泄漏;更新了不注册,就是死灯。"

"怎么管理?"

"一张表。" 它说,"_entries——模型 ID → 灯条目的映射。挂灯时 attachPersonalLight 登记,卸灯时 detachPersonalLight 注销,全局清理时 disposeAllPersonalLights 统一释放。"

typescript
const _entries = new Map<string, PersonalLightEntry>();

"灯跟着模型生,跟着模型死。" 桌面壳说,"模型卸载,灯一起卸;场景销毁,灯全部熄。一盏灯的生命周期,必须和它的主人对齐——光不能比人活得久,也不能比人走得早。"

"为什么用 Map 而不是数组?"

"因为要按模型找。" 它说,"模型卸载时,detachPersonalLight(modelId)——直接按 ID 找到那盏灯,关掉、拆掉、注销。Map 是灯的户口本:谁家的灯,查谁家的册子。"


尾声

桌面壳让模型又走了一圈。

这一次,灯光跟着它。模型走到角落——头顶那盏追光灯跟着挪过去,把角落照亮。它转身——灯跟着转,光始终罩在它身上。它站定——灯停在它上方,影子落在地面,清晰完整。

"它走到哪儿,光跟到哪儿。" 外交官说。

"这就是追光灯。" 桌面壳说,"一盏灯,一个人,一生一世。模型活着,灯亮着;模型走了,灯熄了——灯是模型的光环,也是模型的影子。"

"最麻烦的是什么?"

"不是挂灯,是找腰。" 它说,"灯光系统最难的不是让光跟着人,是知道'人'在哪——腰骨候选找错,灯就挂在了头顶或脚底。光可以跟着人走,但得先认得人。"

它看着那个被光罩着的模型:

"舞台上的主角,不一定自带光环——但联邦可以借它一盏。追光灯不是宠溺,是尊重:主角走到哪儿,光就到哪儿。世界可以很暗,但主角必须亮着。"

"这是舞台的法则。"

"也是联邦的法则。" 桌面壳说,"每一盏灯都有自己的主人,每一个主人都有自己的光。灯随人走,光随人亮——这就是追光的人。"


教训:个人灯不是固定路灯,是每 actor 一盏的追光灯——挂腰骨(候选匹配)、跟随根节点、生命周期与模型对齐。

光可以跟着人走,但得先认得人——腰骨候选是追光的第一步。