Appearance
ADR-129: 动作菜单场景级重设计(Scene-level Motion UI)
状态: 已完成—最终实现偏离设计 日期: 2026-07-18 依赖: ADR-121(全局动作意图,已实施 P0+P1+P2)
背景与问题
ADR-121 在后端实现了「场景级 activeMotion + per-model 继承/覆盖」,但前端动作菜单(menus/motion-popup.ts)的根层结构仍是 per-model 范式:
旧根层结构:
Card 1: 已加载模型列表(actor 行 → 点进去是该模型的动作绑定面板)
├─ 初音未来 [当前 VMD名] → action:binding:{id}
├─ 雷电芽衣 [当前 VMD名] → action:binding:{id}
└─ ...
Card 2: 相机 / 音乐库 / 姿势工作室 / 播放速度 / ...
Card 3: 程序化动作 / 注视 / 高级设置 / 外部导入核心矛盾:
| 维度 | 后端(ADR-121) | 前端(旧) |
|---|---|---|
| 语义 | 「场上在跳什么」——场景级意图 | 「给哪个角色上什么动作」——per-model |
| 选动作 | setActiveMotion(intent) 全局生效 | 必须先选模型 → 进入该模型绑定面板 → 再选 VMD |
| 换角色 | 新模型自动继承全局动作 | 用户感知不到,需手动进每个模型面板重选 |
| Pin 覆盖 | mode: 'pinned' per-model 差异化 | 藏在模型绑定面板的卡片 4,用户难以发现 |
用户反复换皮欣赏时,每次都要点进模型 → 选动作 → 返回,与 ADR-121「换角色无需重选」的设计初衷背道而驰。
竞品参考(DanceXR)
DanceXR 的动作菜单首屏:
1. 当前加载的动作(场景级)→ 点击可微调骨骼设计/动作覆盖
2. 程序化动作
3. 音乐库动作是场景级一等公民,模型列表是次级管理界面。
决策:场景级动作菜单重设计
设计原则
- 动作是场景内容,不是模型属性——根层展示「场上在跳什么」
- per-model 管理下沉——pin/unpin/incompatible 移至模型绑定面板,不占根层空间
- 渐进式重构——分 Phase 落地,每 Phase 独立可交付
最终实现的根层结构(简化版,偏离原设计)
Card 1: 当前动作(场景级)
├─ [当前动作名] / 无动作提示 → 点击进详情页(图层管理 + 骨骼覆盖 + 速度)
│ └─ trailing trash-2 → 清除动作(含 undo)
├─ [叠加层1] [100%] → trailing 齿轮进图层设置
├─ [叠加层2] [50%] → trailing 齿轮进图层设置
├─ 浏览动作库 → VMD 文件浏览器(stay 模式,连续预览)
└─ 程序化动作 → 子页
——— 分割线 ———
Card 2: 场景工具(扁平列表,无分组标题)
├─ 相机 → 子页
├─ 音乐库 / 浏览音乐 → 文件浏览器
├─ [移除音乐](有音乐时显示) → 清除音乐
├─ 姿势工作室 → 子页
├─ 注视追踪 → 子页
└─ 外部动作导入(有模型时) → 子页(Mixamo/VRM/自定义)与原设计的关键偏离:
| 原设计 | 最终实现 | 偏离原因 |
|---|---|---|
| Card 2: per-model 角色状态行(跟随全局/固定/不兼容) | 移除,per-model 管理下沉至模型绑定面板 | 根层信息过载,用户更关心「选动作」而非「看状态」;pin/unpin 仍可通过模型面板访问 |
| Card 3: 3 组 sectionTitle(播放与同步/角色与环境/系统与导入) | 取消分组,扁平化为单一列表 | sectionTitle 增加视觉噪音,7 项工具无需三级分组;用户通过图标快速定位 |
| 播放状态行(播放/暂停/帧进度/循环) | 移除,未在菜单内实现 | 播放控制由底部播放栏承担,避免双源冲突;菜单 focus 操作而非状态监控 |
| 骨骼覆盖/脚部调整/虚拟裙骨 | 骨骼覆盖移至动作详情页;脚部/裙骨下沉至模型详情页「故障排除」 | 功能低频,不应占根层空间 |
| 高级菜单(收纳低频功能) | 移除,相关功能已重新分配到各详情页 | 嵌套菜单增加操作路径深度,不必要 |
关键交互变更
| 操作 | 旧流程 | 新流程 |
|---|---|---|
| 加载动作 | 选模型 → 进绑定面板 → 浏览 VMD → 选文件 | 根层「浏览动作库」→ 选文件(stay 模式连续预览)→ 全局广播 |
| 清除动作 | 选模型 → 进绑定面板 → 点清除 | 根层 current motion 行 trailing trash 按钮 / 详情页清除按钮 |
| 换角色不换动作 | 每次换模型后重新选 | 自动继承(ADR-121 已实现) |
| Pin 独立动作 | 进模型绑定面板 → 卡片 4 pin 按钮 | 进模型绑定面板 → 卡片 2 pin/unpin 按钮 |
| 叠加层管理 | 进模型绑定面板 → 图层列表 | 根层内联显示叠加层列表 → 点击进图层设置 |
| 骨骼覆盖 | 通过「高级」菜单 → 骨骼覆盖 | 动作详情页 → 骨骼覆盖入口 |
| 播放控制 | 动作详情页内 play/pause/loop/进度条 | 底部播放栏(独立于菜单) |
实施过程
Phase 1:根层结构重排
状态:已落地(实际实现简化了原设计的 Card 2 和 Card 3)
- 根层从「模型列表优先」改为「当前动作优先」
- 新增
buildMotionRootItems()重新排列卡片顺序 - 新增
buildMotionDetailLevel()动作详情页 - 新增
buildCurrentMotionLabel()统一显示 VMD/程序化动作/无动作 - 新增
__scene_motion_browse__场景级浏览入口(mode: 'stay'连续预览) buildActionBindingSchema()精简为仅保留 per-model 专属功能(姿势库、pin/unpin、物理开关)
Phase 2:动作详情页整合
状态:已落地
- 动作详情页包含:当前动作名 + 清除按钮、场景级图层管理(叠加层列表)、骨骼覆盖入口、播放速度滑块
- 播放控制(play/pause/loop/进度条)未实现——由底部播放栏承担
- 模型面板不再包含图层管理
Phase 3:per-model 差异化增强
状态:已落地(简化为 _buildActorSublabel 函数)
- 模型绑定面板的
_buildActorSublabel显示:跟随全局 / 固定: [动作名] / 不兼容 - pin/unpin 操作在模型绑定面板内完成
- 未实现根层模型行的快捷操作(原设计根层有模型行,实际实现已移除)
不变的部分
| 模块 | 不动原因 |
|---|---|
scene/motion/motion-intent.ts | 后端逻辑完备,本 ADR 仅改 UI 层 |
scene/motion/playback.ts / vmd-loader.ts / vmd-layers.ts | 播放链路不变 |
| ADR-116 动作覆盖模块 | per-model 覆盖层与全局意图正交 |
| ADR-121 广播逻辑 | 已正确实现,UI 只是换个入口调用 setActiveMotion |
设计偏离总结
| 原设计项 | 实现状态 | 备注 |
|---|---|---|
| Card 1: 当前动作(场景级) | ✅ 已实现 | 含内联图层列表 + trailing 清除 |
| Card 1: 播放状态行 | ❌ 未实现 | 由底部播放栏替代 |
| Card 2: per-model 角色状态行 | ❌ 删除 | 信息过载,下沉至模型绑定面板 |
| Card 3: 3 组 sectionTitle 分组 | ❌ 删除 | 扁平化,7 项工具无需分组 |
| 骨骼覆盖在根层可见 | ⚠️ 移至动作详情页 | 路径更长但低频功能合理 |
| 脚部调整/虚拟裙骨在根层可见 | ❌ 下沉至模型详情页 | 故障排除折叠组内 |
| 播放控制(play/pause/loop/进度条) | ❌ 未实现 | 底部播放栏承担 |
| 动作详情页图层管理 | ✅ 已实现 | 场景级 active.vmdLayers |
| 模型绑定面板 pin/unpin | ✅ 已实现 | 使用 ModelMotionSlots 双槽位 |
| 外部动作导入 | ✅ 已实现 | 根层 → retarget 子页 |
后续迭代方向
- 动作预设组:「演唱会包」一键设
activeMotion+ 给特定角色pin独舞(ADR-121 已规划) - 批量 pin:多选模型统一指派动作
- 动作搜索:VMD 库支持按名称/标签搜索
- 动作预览:选 VMD 时实时预览骨骼动画(需性能评估)