Appearance
追光的人
背景:模型走到哪儿,灯光不会跟——角色挪出主灯范围就变黑,画面里光与人脱节。 过程:每 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 一盏的追光灯——挂腰骨(候选匹配)、跟随根节点、生命周期与模型对齐。
光可以跟着人走,但得先认得人——腰骨候选是追光的第一步。