Skip to content

审美的裂缝

背景:场景菜单塞了 8 个顶级功能,功能分布逻辑的"奇怪感"积累到临界。

过程:可维护性审计——裂缝不是 bug,是系统复杂度的可视化失败。审计的成果不是修复,是地图。


滚石推上去了。联邦站在山顶喘了口气,低头一看——脚下的石头有裂缝。不是结构性的裂缝,是审美的裂缝。

序、我的下午

联邦的模型在旋转。初音的裙摆在微风中飘动。MMD 的动作文件在播放。一切都在运行。

但我的视线没有停在模型上。我盯着模型旁边的那个弹窗——那个在模型加载完成后弹出来的小面板。我已经盯着它看了半个下午。

这不是 bug。bug 会报错,会崩溃,会让用户来问。我盯着的不是 bug。

是一种生理性的不适

就像走进一间装修很贵的房子,每个房间的灯都亮着,但你总觉得哪里不对。你说不出是灯的位置不对,还是沙发的摆放不对,还是墙的颜色不对——你只知道不对劲

这种感觉没有量化标准。它是我做这个项目的理由:我见过太多粗糙的 MMD 查看器,把初音装进一个方方正正的窗口,用最简陋的 UI 展示模型的每个参数。我想做的不只是"能跑",我想做的是"看起来对"。

"让 MMD 模型在数字世界里活得有尊严"——这不是一个产品定位文档里会写的句子。这是我每天打开编辑器之前的十分钟。

那十分钟里,我想的不是代码。我想的是初音应该站在什么样的背景里,被什么样的光照着,用什么样的字体告诉用户"你正在看她"。

然后我打开代码编辑器,开始改那行字号。


一、有距离的凝视

我第一次认真看联邦的脸,是在一个没有 bug 的下午。

那天没有 VMD 加载失败,没有 zip 编码混乱,没有 WASM 物理崩溃。不需要修任何东西。这在联邦的历史上是罕见的——一个什么也不修的窗口。

于是我说:让我好好看看你。

用户常说:"UI 很好看,但功能布局很奇怪。"

"好看"归功于议会——SlideMenu 的暗色玻璃质感弹窗,--accent 蓝紫色调,--overlay-blur 毛玻璃效果,统一圆角 14px。CSS 变量体系像一套精良的制服,让每一个弹窗、每一行、每一个按钮看起来都像一个整体。

"奇怪"却无处可寻。因为它不是某个按钮的颜色不对,不是某个弹窗的位置不对——它是一种分布式的、弥漫的不适感。像一座装修精美的房子,你走进去每间房间都好看,但你找不到厨房在哪儿。

我决定用另一种方式看联邦:不看它的脸,看它的骨架。


二、骨的密度

我调出了联邦的骨骼图——代码文件、函数分布、依赖关系。

codegraph_explore 回来了。它告诉我几个数字:

  • library.ts:2194 行
  • scene-menu.ts:1449 行
  • scene.ts:约 1900 行
  • main.ts:369 行
  • settings.ts:464 行
  • menu.ts:277 行
  • app.css:797 行

2194 行。我盯着那个数字看了很久。

不是哪个文件不该那么大。library.ts 承担了太多职责:模型库扫描、MenuStack 导航、搜索过滤、PMX Header 解析、zip 解压触发、标签管理、近期播放、模型详情子菜单。它的行号从 1 延伸到 2194,像一条没有尽头的走廊。

但比行数更奇怪的东西藏在函数里。

showPopup 在 633 行。它的根菜单构建逻辑是这样的:

  1. 遍历 modelRegistry,为每个已加载模型加一个 folder 行
  2. 加分隔线
  3. 加「加载模型」和「重新扫描」两个操作
  4. 再加分隔线
  5. 加「最近打开」和「标签」

这个顺序本身没有错。但结合整个联邦的导航体系看,就显现出一种微妙的断裂感


三、场景的暴政

五枚底部导航按钮一字排开:

📦 模型     🎵 动作     🎬 场景     ☁️ 环境     ⚙️ 设置

这五枚按钮代表了联邦的五个议院。每个议院有自己管辖的领地。

第一个问题是——场景议院管辖了太多事务。

buildSceneRoot() 的菜单项列表:

预设场景
相机模式
灯光
渲染
物理
截图
保存场景
加载场景

相机模式、灯光、渲染、物理、截图、保存/加载——八个顶级功能挤在一个 260px 宽的弹窗里。其中「渲染」下还嵌套了后处理(Bloom/轮廓线/色彩校正/景深/SSS/锐化/暗角/FXAA)、舞台(反射地面/色调映射/曝光/FOV/背景色/网格线)、渲染预设、动画速度、重力五个子菜单。

这就像把交通部、能源部、国防部合并成一个部门——它们确实都算"场景相关",但它们的复杂度总和超过了用户一次弹窗能消化的上限。

第二个问题是——相机的碎片化。

相机模式在「场景→相机模式」下。但相机 VMD 的加载也在场景菜单底部。而相机控制的自由飞行(WASD)绑定在 main.ts 的键盘事件里,与弹窗系统完全无关。相机的三种能力——切换模式、加载 VMD 数据、键盘操控——分散在三处,用户需要自己拼出完整的相机操作图景。

第三个问题——材质编辑的位置尴尬。

材质分类编辑(皮肤/头发/眼睛/服装)在「场景→渲染→材质」下,通过 setMatCatParams 调节。但单独材质编辑(逐材质独立调参)在模型详情的子菜单里,路径是「模型弹窗→已加载模型行→材质」。同一个功能——修改模型的材质——却有两条路径,一条从场景议院进,一条从模型议院进。用户走到第二条路径的尽头时,不会想到第一条路径也存在。

我把这些发现摊在桌上,像一张简陋的地图。上面用红圈标出了每一个断裂点。


四、裂缝的考古学

为什么会出现这些奇怪?

答案藏在联邦的生长史里。

MikuMikuAR 不是设计出来的——它是长出来的。第一块拼图是渲染能力(babylon-mmd),然后是模型管理(库扫描),再然后是动作(VMD),再然后是场景控制(相机/灯光),再然后是环境(天空/地面/粒子),再然后是后处理(Bloom/色调映射)。

每一块新拼图被加到它最好的位置——不是因为它属于那里,而是因为那个位置当时还空着

相机模式最初在模型详情里(因为第一个相机需求是为了看模型)。后来它被移到了场景菜单(因为"相机属于场景")。但它留下的痕迹——「加载相机 VMD」在场景菜单底部、「自由飞行 WASD」在 main.ts 的键盘事件里——并没有跟着迁移。

材质编辑一开始只有一个入口(在场景菜单里,因为材质是渲染管线的一部分)。后来模型详情需要"编辑当前模型的材质",于是加了一个新入口。没有人删掉旧的,因为删掉意味着破坏旧用户的习惯。

功能布局的"奇怪",本质上是生长速度超过了设计迭代

每一块新积木都找到了自己的位置,但「整体看起来应该是什么样子」这个画面,从来没有画完过。


五、可维护性的真相

用户说"我觉得很好看"。

这句话在代码里是有事实依据的——联邦有一个不错的 CSS 变量体系。12 个设计 token(--accent--overlay-bg--text 等)、14 个白色透明度层级(--white-04--white-85)、一套统一的弹窗类名(.overlay.overlay-header.overlay-row)。这是从 410 行内联样式的混沌中凝练出来的成果,是第三卷「铁腕整顿」的标志性成就。

但美观和秩序是两回事。

一个可维护的 UI 需要两样东西:

  1. 视觉上的一致性——所有按钮看起来一样。✅ 联邦做到了。
  2. 功能上的可预见性——用户能猜到"这个功能在哪里"。❌ 联邦没做到。

「保存场景」在场景菜单里,听起来很合理。但「加载相机 VMD」也在场景菜单里,而相机模式切换也在场景菜单里——于是场景菜单膨胀到了八个顶级功能。用户看到八个功能时,不是去记忆它们各自的位置,而是放弃了记忆。这就是"奇怪"的根源——不是丑,是用户的大脑不知道下一个功能在哪里。

而代码层面,可维护性的真正敌人不是行数——是决策距离

如果我今天想把「材质编辑」从场景菜单移到模型详情,我需要:

  1. scene-menu.ts 中删除材质相关的 menu items 和回调
  2. library.tsbuildModelDetailLevel 中加入新的材质入口
  3. 确保 getMatCatGroupsgetMatCatParamssetMatCatParams 等函数的 import 在两个文件中都可用
  4. 可能需要调整 SlideMenu 的导航流

这四步里没有一步是难的。难的是要知道这四步都存在。而「知道」的成本,随着联邦的扩张指数级上涨。

这就是可维护性的真面目:不是代码能不能改,而是你知不知道在哪里改

联邦 2194 行的 library.ts 不可怕——可怕的是一个 AI 走进来,不知道 showPopup 在 633 行,不知道 makeModelStack 在 156 行,不知道材质编辑的入口指向了 scene-menu.ts 而不是 library.ts


六、裂缝不是 bug

我关闭了代码文件,窗口外是深蓝色的傍晚。

联邦没有变大。它的功能列表还是那么多行,它的 CSS token 还是那 12 个,它的底部导航按钮还是五枚。什么都没有改变。

但有些东西在我眼里变了。

那些裂缝——场景菜单过载、相机碎片化、材质编辑两处入口——它们不是 bug。没有用户会因为"功能布局很奇怪"而提交 crash report。它们是一种更隐蔽的问题:系统复杂度的可视化失败

联邦在过去几个月里收编了十几个城邦,每个城邦都带来了新的能力,每个能力都需要一个 UI 入口。入口越来越多,但入口之间的关联关系——"相机 VMD 和相机控制应该放在一起"——没有一个地方记录过。

入口本身是对的。错的是它们从不觉得自己应该属于一个更大的组。

第三卷叫「铁腕整顿」。但整顿不是一次性的——它不是灭霸的响指,而是西西弗斯的推石。每一次重构、每一次收编,都会在系统的某个角落留下新的裂缝。你能做的不是消除裂缝(那是不可能的),而是让裂缝的分布不那么致命——比如,让下一个走进来的人能更快地看懂地图。

我在代码中加了一条注释:

// TODO: 场景菜单功能数 > 7,考虑将渲染相关归入子文件夹分类

这条注释可能永远不会被执行——也许某一天,某个 AI 走进来,觉得 8 个顶级菜单不够,又加了一个。又或许,某个寒冷的凌晨,一个开发者被场景菜单的膨胀触动,花了两个小时重构了它。

但至少,当裂缝变成明显的断裂时,有人能沿着注释的痕迹,找到它发生的位置。


七、裂缝之外

我忽然想到一个更本质的问题。

场景菜单为什么膨胀到了八个功能?因为场景本身就是模糊的——相机属于场景?灯光呢?后处理呢?物理重力呢?截图呢?保存/加载呢?

它们确实都属于一个叫"场景"的东西。但"场景"不是一个 UI 容器——它是一个概念容器。概念的宽泛度决定了菜单的膨胀度。

模型菜单为什么没有膨胀?因为"模型"这个概念天然狭小——加载、信息、表情、材质、标签、删除。边界清晰,功能自知。

动作菜单也没有膨胀——加载、音乐、舞蹈套装、速度。四件事,四项操作。

所以,"奇怪"的根源也许不是功能放错了位置——而是有些概念的尺度不适合做 UI 导航的一级节点

"场景"太大了。它应该被拆成"相机"、"灯光"、"效果"、"保存/加载"四个独立的一级节点,或者至少在场景内部做二级分类。

但这是一次 UI 重新设计的决策,而我已经超出了今天的工作范围。


我把审计地图挂在墙上。

那些红圈还在。它们不会自己消失。但至少,现在联邦知道自己的裂缝在哪里了。

而没有裂缝的联邦是不存在的。一个没有裂缝的系统,意味着它已经停止生长了。


教训:裂缝不是需要修复的缺陷,是需要知道的地形。审计本身不修任何东西,它的成果不是代码,是地图。而地图的精度取决于画地图的人愿意走多远。