Skip to content

物理的迁徙

背景:物理设置藏在动作菜单里——重力不因跳跃而改变,碰撞不因换歌而失效。 过程:调试从布料子页剥离、根页参数化重构、全线搬迁到场景菜单——三刀完成 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 接口里只有 clothEnabledclothConfigsolverSubsteps。没有重力,没有时间缩放,没有碰撞主开关。

"重力藏在 env-bridge.ts 里,只有 WASM 能调。布料的重力在 cloth-manager 里写死的,乘了个默认 1.0 的因子——根本没有接口。"

"那就加。"

第二波改动波及五个文件:

  • config.tssolverTimeScalecollisionEnabledbodyCollisionEnabledgroundCollisionEnabled 四个新字段
  • cloth-manager.ts:重力 getter/setter、时间缩放 API、碰撞级联逻辑 _applyCollisionState
  • xpbd-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(通过 setGravityStrengthmmdRuntime.physics.setGravity runtime)和 XPBD 布料(通过 setClothGravity → 遍历 clothInstances runtime)。一个 slider,两个物理引擎同时响应。

"这还是第一次,"WASM 物理和 XPBD 布料异口同声,"我们被同一个控件控制。"


迁徙

第三刀最大——整个物理模块从动作菜单搬到场景菜单。

"物理不是动作属性,是场景属性。"议会重复道。

搬迁意味着:

  • motion-physics-levels.tsscene-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 能让两个引擎同时说话。


教训:菜单是架构的镜像——功能放错位置,代码迟早会跟上。