Appearance
物理的迁徙
背景:物理设置藏在动作菜单里——重力不因跳跃而改变,碰撞不因换歌而失效。 过程:调试从布料子页剥离、根页参数化重构、全线搬迁到场景菜单——三刀完成 UI 层治理。
"你不觉得奇怪吗?"
议会翻着菜单架构手册,眉头越皱越紧。
"哪里奇怪?"外交官正在调试重力滑条。
"物理——挂在动作菜单下面。"
议会把手册摊开在桌上,手指在目录上画了一条线:
动作 → 物理
├─ 布料模拟
├─ 重力强度
├─ 求解质量
├─ 碰撞
└─ 调试"重力不是动作属性,"议会说,"它是场景属性。你换了一首歌,重力不会变。你换了一个模型,碰撞体不会消失。你把物理放在动作菜单里——"
"就好像把天气设置放在衣柜里,"外交官接话,"每次穿衣服都要调一遍气温。"
"就是这个意思。"
布料页面的臃肿
XPBD 布料也没好到哪去。
motion-cloth-levels.ts 已经长到了 296 行。四个 collapsible 区:形状、物理、细分、碰撞体——全是布料参数,还算合理。但后面跟着"变换"(position/scaling/rotation——与模型详情页重复)和"调试"(材质线框/骨骼线/粒子球/约束线——与布料参数无关)。
"我觉得它太胖了。"用户说。
外交官打开文件:"2 个 Chip 组 + 6 个折叠区 + 变换 + 调试。其中调试占了 60 行,但和布料半毛钱关系没有。"
"调试要保留在物理域里,不搬到模型详情——菜单已经够多了。"
"那把它提出来,"议会说,"从布料子页提到物理根页,作为独立调试子页。"
第一刀落下。motion-physics-levels.ts 新增调试 folder 导航 + buildPhysicsDebugLevel(),六个 toggle 平铺。motion-cloth-levels.ts 砍掉 62 行调试代码。构建通过。
根页重构
砍完调试,物理根页面只剩一个 folder:"布料模拟"——用户要点进去才能看到参数。
"每次调重力都要进两层菜单,"议会翻着统计数据,"预设 chip 把产能瓶颈解决了,可重力强度到现在都没有 UI。"
外交官打开 config.ts,envState 接口里只有 clothEnabled、clothConfig、solverSubsteps。没有重力,没有时间缩放,没有碰撞主开关。
"重力藏在 env-bridge.ts 里,只有 WASM 能调。布料的重力在 cloth-manager 里写死的,乘了个默认 1.0 的因子——根本没有接口。"
"那就加。"
第二波改动波及五个文件:
config.ts:solverTimeScale、collisionEnabled、bodyCollisionEnabled、groundCollisionEnabled四个新字段cloth-manager.ts:重力 getter/setter、时间缩放 API、碰撞级联逻辑_applyCollisionStatexpbd-cloth.ts:dt 乘以getTimeScale()回调motion-physics-levels.ts:根页面焕然一新motion-cloth-levels.ts:presets 去掉重力因子
新布局:
布料模拟 [toggle]
├─ 重力强度 1.0
├─ 求解质量 4
├─ 模拟速度 1.0
├─ 🛡 碰撞 [folder + toggle]
│ ├─ 地面碰撞 [toggle]
│ └─ 身体碰撞 [toggle]
├─ 精细调节 [→]
└─ 调试 [→]碰撞主开关的级联逻辑是一个精巧的"与门":主开关 OFF → 地面和身体全关;主开关 ON → 各自独立控制。_applyCollisionState 函数在每次开关变化时执行这个计算。
身体碰撞是全新的功能——XPBD 布料粒子与 SDF 人体胶囊的碰撞,首次有了 UI 开关。
重力 slider 还有另一个隐藏的属性——它同时控制 WASM Bullet(通过 setGravityStrength → mmdRuntime.physics.setGravity runtime)和 XPBD 布料(通过 setClothGravity → 遍历 clothInstances runtime)。一个 slider,两个物理引擎同时响应。
"这还是第一次,"WASM 物理和 XPBD 布料异口同声,"我们被同一个控件控制。"
迁徙
第三刀最大——整个物理模块从动作菜单搬到场景菜单。
"物理不是动作属性,是场景属性。"议会重复道。
搬迁意味着:
motion-physics-levels.ts→scene-physics-levels.ts——连文件名都换了- 所有
getMotionMenu()换成getSceneMenu() - 所有
refreshMotionRoot()换成refreshSceneRoot() - 从
motion-popup.ts移除物理路由 - 在
scene-menu.ts新增物理路由
场景菜单的架构师翻开手册:
场景
├─ 预设场景
├─ 保存场景
├─ 后处理
├─ 舞台
├─ 物理 ← 新来客
├─ 截图"挤一挤,"议会让了让座。
"挺宽敞的,"物理说,"比之前好。"
搬迁完,外交官又审视了一遍 scene-physics-levels.ts 的代码。发现了一个微小的瑕疵:buildPhysicsLevel() 返回的 PopupLevel 对象里,有一个 onFolderEnter 属性——但这个属性在 PopupLevel 类型定义里根本不存在。它是一段死代码:SlideMenu 的 folder 导航用的是构造参数里的 onFolderEnter,不是 PopupLevel 的。
删除后,tsc --noEmit 的报错少了一条——这是在文件搬过来之前就存在的技术债。
最后的顺序
"为什么 WASM 物理排在布料前面?"
用户看着物理根页的排列顺序,提出了一个哲学问题:
"WASM 物理比布料更通用——每个模型都有骨骼刚体,但不是每个模型都需要布料。"
是的,重力 slider 驱动两个引擎,碰撞 toggle 级联所有子系统。物理不是动作的附庸,是场景的骨骼。
最后一轮调整把 WASM 物理从页面底部提到第二项,紧跟在重力之后。页面变成:
物理
├─ 重力强度(WASM + 布料)
├─ WASM 物理 [→]
├─ 布料模拟 [toggle]
├─ 求解质量 [slider]
├─ 模拟速度 [slider]
├─ 碰撞 [folder + toggle]
├─ 精细调节 [→]
└─ 调试 [→]WASM 物理排在布料前面——不是因为谁更强大,而是因为谁更基础。
"联邦的规矩,"议会宣布,"基础服务优先,增值服务在后。重力是空气,WASM 是骨骼,布料是衣服。顺序不能乱。"
重力滑条停在 1.0。
碰撞主开关亮着绿灯。
三刀之后,布料页面瘦了身,物理根页有了自己的参数体系,整个模块搬到了场景菜单——它终于坐在了客厅里。
物理终于从动作菜单的角落里搬了出来,回到场景的客厅里——那里才是它该待的地方。
聚合的代价:每收编一个系统,就多一套 UI。但你不收编它们,就永远不知道哪个 slider 能让两个引擎同时说话。
教训:菜单是架构的镜像——功能放错位置,代码迟早会跟上。