Skip to content

Commit 266dc38

Browse files
committed
feat: vmd-morph-c4d-tracks
1 parent ad94c40 commit 266dc38

15 files changed

Lines changed: 498 additions & 202 deletions

File tree

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
schema: spec-driven
2+
created: 2026-03-25
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
## Context
2+
3+
当前 VMD 模式下表情动画的数据流是:
4+
5+
```
6+
VMDFile::m_morphs → VMDAnimation::Add() → VMDMorphController (线性插值)
7+
每帧 → VMDAnimation::Evaluate() → MMDMorph::SetWeight()
8+
→ PMXModel::UpdateMorphAnimation() → libMMD 内部处理
9+
→ MMDMeshManagerObject 从 libMMD 读 GetWeight() → 覆盖 Pose Morph Tag
10+
```
11+
12+
- `MODEL_MORPH_GRP` 在 VMD 模式下被完全隐藏
13+
- `UpdateMorph()` 虽在所有模式下运行,但 mesh manager 的 VMD 路径会覆盖其效果
14+
- 用户无法在 C4D 时间线中查看或编辑表情关键帧
15+
16+
代码中已有被注释掉的 `SetMeshMorphAnimation`(mmd_model_manager.cpp:1611-1649),使用 `CTrack`/`CKey``IMorph::m_strength_id` 上创建关键帧,是本次改造的直接参考。
17+
18+
## Goals / Non-Goals
19+
20+
**Goals:**
21+
- VMD 导入后,表情关键帧出现在 C4D 时间线中,用户可查看和编辑
22+
- 表情滑块在 VMD 模式下可见且实时反映动画值
23+
- Group/Flip morph 的传播语义与 MMD 原始行为一致(叠加而非覆写)
24+
- 清理 MeshManager 中不再使用的 VMD morph 数据结构
25+
- 未匹配的 morph 名称在导入报告中给出反馈
26+
27+
**Non-Goals:**
28+
- 骨骼动画迁移到 C4D CTrack(留作第二步,涉及贝塞尔曲线转换)
29+
- Material/Impulse morph 的 UpdateMorph 实现(这两个当前为空实现,不在本次范围内)
30+
- 改动 libMMD 内部代码
31+
- 支持 VMD 导出时从 CTrack 读取表情数据(保持现有的 `VMDAnimation::Save` 路径)
32+
33+
## Decisions
34+
35+
### D1: CTrack 创建时机 — 在 LoadVMDMotion 中完成
36+
37+
**选择**:在 `MMDModelManagerObject::LoadVMDMotion` 中,`VMDAnimation::Add` 完成后,遍历 `vmd_file.m_morphs` 直接创建 CTrack/CKey。
38+
39+
**理由**
40+
- `LoadVMDMotion` 已经持有 `vmd_file` 和完整的 `morph_name_` 映射
41+
- 一次性操作,不影响每帧性能
42+
- 注释代码 `SetMeshMorphAnimation` 正是这个模式
43+
44+
**备选**:在 `RebuildRuntime` 中从 VMDAnimation 的 morph controller 提取数据。但这需要暴露 VMDAnimation 内部结构,增加 libMMD API 耦合。
45+
46+
### D2: 保留 libMMD 的 VMDAnimation morph 评估
47+
48+
**选择**`VMDAnimation::Evaluate()` 仍然会设置所有 morph 权重(包括表情),不修改 libMMD。
49+
50+
**理由**
51+
- 骨骼 morph 的效果通过 `UpdateMorphAnimation()` 写入 `mmd_node_` 变换,`BoneTag` 在 VMD 模式下读取这些变换。如果移除 morph 评估,骨骼 morph 会失效。
52+
- libMMD 不需要改动,降低改造风险。
53+
- C4D 端的表情值和 libMMD 端的表情值会平行存在但互不干扰:mesh/UV morph 只看 C4D 路径(通过 `UpdateMorph → SetMorphStrength`),骨骼 morph 只看 libMMD 路径。
54+
55+
### D3: Group/Flip Morph 叠加语义
56+
57+
**选择**:将 `GroupMorph::UpdateMorph` 改为叠加(`SetStrength(node, GetStrength(node) + self * ratio)`),`FlipMorph::UpdateMorph` 类似。
58+
59+
**理由**
60+
- MMD 中 group morph 对子 morph 是叠加效果。如果 VMD 同时在 group morph 和其子 mesh morph 上都有关键帧,覆写会丢失子 morph 自身的关键帧值。
61+
- C4D 的 CTrack 会同时在 group morph 和子 morph 的 strength 参数上设置值,`UpdateMorph` 在 track 求值之后运行,必须是叠加才能正确合并。
62+
63+
**注意**`UpdateMorph` 的遍历顺序需要保证 group/flip morph 在其子 morph 的 `UpdateMorph` 之前执行。当前的 `morph_data_` 顺序(由 PMX 导入顺序决定)通常满足这个条件,因为 PMX 中 group/flip morph 通常排在引用的子 morph 之后。但在遍历时需要先处理 group/flip,再处理其余。实际上 PMX 中 group/flip morph 引用的是其他 morph 的 index,而 group/flip 本身可以在列表中任意位置。
64+
65+
**具体实现**:分两轮遍历 — 第一轮只处理 group/flip morph(将叠加值写入子 morph),第二轮处理其余 morph(mesh/bone/UV 推送到 manager)。
66+
67+
### D4: MeshManager VMD morph 数据结构清理
68+
69+
**选择**:移除 `morph_manager_index_``mmd_morph_manager_` 成员、以及 `Execute``MESH_MODE_VMD` 的 morph 权重读取块。
70+
71+
**理由**
72+
- 这些结构仅用于从 libMMD 读取 morph 权重,改用 C4D 参数驱动后不再需要。
73+
- `SetMorphStrength(name, strength)` 通过名称查找 `mesh_morph_name_` 映射,功能完全覆盖。
74+
75+
**保留**`mesh_morph_name_``mesh_morph_data_``mesh_morph_mode_` 等结构仍然需要,它们是 Pose Morph Tag 的映射,与 VMD 无关。
76+
77+
### D5: CKey 插值模式设为线性
78+
79+
**选择**:创建 CKey 后显式设置为线性插值(`CINTERPOLATION_LINEAR`)。
80+
81+
**理由**:C4D 默认使用样条插值,会在关键帧之间产生过冲。VMD 表情使用线性插值,必须匹配。
82+
83+
### D6: VMD 模式下表情 UI 可见性策略
84+
85+
**选择**:VMD 模式与 ANIM 模式使用相同的 UI 可见性逻辑 — 表情滑块可见,但隐藏"添加/删除/重命名"等编辑按钮。
86+
87+
**理由**
88+
- 用户需要能看到滑块来确认动画效果
89+
- 用户可以在时间线中修改关键帧,滑块应实时反映当前值
90+
- 不应允许在 VMD 模式下添加/删除 morph,避免与 VMD 动画数据不一致
91+
92+
## Risks / Trade-offs
93+
94+
- **[风险] CTrack 数量大**:复杂 VMD 可能有几十个 morph 关键帧通道。→ 缓解:C4D 时间线原生支持大量 CTrack,UI 上通过面板分类过滤。
95+
- **[风险] Group morph 遍历顺序**:如果 PMX 数据中 group morph 引用了排在其后的 morph,两轮遍历策略可以正确处理。→ 缓解:第一轮仅处理 group/flip,第二轮处理余下所有类型。
96+
- **[风险] 旧场景兼容性**:已保存的场景在 VMD 模式下没有 CTrack。→ 缓解:用户需要重新导入 VMD 文件;旧场景不会崩溃,只是表情不再自动播放。
97+
- **[权衡] libMMD 中 morph 评估冗余**:libMMD 仍然每帧评估所有 morph 权重,但 mesh/UV morph 结果不再使用。→ 这个开销很小(纯数值插值),且保障了骨骼 morph 的正确性。
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
## Why
2+
3+
当前 VMD 模式下,表情(morph)动画完全由 libMMD 内部驱动:`VMDAnimation::Evaluate()` 每帧计算权重 → `MMDMeshManagerObject` 从 libMMD 读取权重覆盖 Pose Morph → 整个 `MODEL_MORPH_GRP` UI 被隐藏。用户无法在 C4D 时间线中看到、编辑或微调表情关键帧,可调性极差。VMD 表情关键帧使用纯线性插值,与 C4D 的 CTrack/CKey 系统完全兼容,可以直接移植。
4+
5+
## What Changes
6+
7+
- **VMD 导入时创建 C4D 关键帧**:在 `LoadVMDMotion` 中遍历 `VMDFile::m_morphs`,通过 `morph_name_` 匹配到 `morph_data_[]`,在对应的 `m_strength_id` 上创建 CTrack/CKey(线性插值),将 VMD 表情数据直接映射到模型属性管理器 UI 的强度属性。
8+
- **取消 VMD 模式下表情组隐藏**:移除 `GetDDescription` 中对 `MODEL_MORPH_GRP``DESC_HIDE` 设置,使用户在 VMD 模式下也能看到和编辑表情滑块。非编辑模式下仅隐藏"添加"相关按钮。
9+
- **移除 MeshManager 的 VMD 表情覆盖路径**:移除 `MMDMeshManagerObject::Execute``MESH_MODE_VMD` 下从 libMMD 读取 morph 权重的逻辑,改为完全依赖 `UpdateMorph()``SetMorphStrength()` 已有路径。
10+
- **Group/Flip Morph 改为叠加语义**:将 `GroupMorph::UpdateMorph` 从覆写改为叠加(`+=`),`FlipMorph::UpdateMorph` 同理,与 libMMD/MMD 原始行为保持一致。
11+
- **清理 MeshManager 中 VMD morph 相关数据结构**:移除不再需要的 `morph_manager_index_``mmd_morph_manager_` 等 VMD 表情相关的数据结构和初始化逻辑。
12+
- **记录未匹配 morph 名称到导入日志**:导入 VMD 时,将无法匹配到模型 morph 的名称记录到 `not_find_morph_name_list`,在导入报告中给用户反馈。
13+
14+
## Capabilities
15+
16+
### New Capabilities
17+
- `morph-c4d-track-animation`:VMD 导入时将表情关键帧数据写入 C4D CTrack,支持时间线查看和编辑
18+
19+
### Modified Capabilities
20+
- `object-morph-system`:Group/Flip morph 的 UpdateMorph 语义从覆写改为叠加;VMD 模式下不再隐藏表情 UI
21+
- `motion-vmd-motion`:VMD 导入流程增加 CTrack 关键帧创建;不再使用 libMMD 每帧输出表情权重驱动 mesh;增加未匹配 morph 的日志反馈
22+
23+
## Impact
24+
25+
- **源文件**`mmd_model_manager.cpp/h`(导入逻辑、UI 隐藏)、`mmd_morph.cpp`(Group/Flip UpdateMorph 语义)、`mmd_mesh_manager.cpp/h`(VMD morph 路径移除、数据结构清理)
26+
- **依赖**:libMMD 内部 VMD morph 评估仍保留(骨骼 morph 效果依赖其 node 变换计算),不改动 libMMD
27+
- **兼容性**:已存在的 VMD 模式场景文件加载后需要重新导入 VMD 动画以获得 CTrack 关键帧;旧场景的 morph 滑块数据不会丢失
28+
- **API 变化**`MMDMeshManagerObject` 移除 `morph_manager_index_``mmd_morph_manager_` 成员和相关方法
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
## ADDED Requirements
2+
3+
### Requirement: VMD morph animation playback mechanism
4+
5+
In VMD mode, morph animation playback SHALL be driven by C4D's CTrack system instead of per-frame reads from libMMD's morph weight output. The `MMDMeshManagerObject` SHALL NOT read morph weights from `libmmd::MorphManager` during VMD mode execution. Instead, morph strength values come from C4D parameters (set by CTrack evaluation), and `UpdateMorph()` dispatches them to mesh/bone managers through existing paths (`SetMorphStrength`, bone morph hubs).
6+
7+
#### Scenario: Mesh morph strength comes from CTrack in VMD mode
8+
- **WHEN** the model is in `MODEL_MODE_VMD` and a mesh morph has CTrack keyframes
9+
- **THEN** `MMDMeshManagerObject::Execute()` does NOT read morph weights from `mmd_morph_manager_`
10+
- **THEN** mesh morph strength is applied through `MeshMorph::UpdateMorph()``SetMorphStrength()` using the CTrack-driven parameter value
11+
12+
#### Scenario: Bone morph still works via libMMD node transforms
13+
- **WHEN** the model is in `MODEL_MODE_VMD` and bone morphs exist
14+
- **THEN** libMMD's `VMDAnimation::Evaluate()` still sets bone morph weights internally
15+
- **THEN** `UpdateMorphAnimation()` applies bone morph effects to `mmd_node_` transforms
16+
- **THEN** `MMDBoneTag` reads correct transforms from `mmd_node_` including bone morph effects
17+
18+
### Requirement: Remove VMD morph weight reading from MeshManager
19+
20+
The `MMDMeshManagerObject` SHALL NOT maintain `morph_manager_index_` or `mmd_morph_manager_` members. The `MESH_MODE_VMD` morph weight reading loop in `Execute()` SHALL be removed.
21+
22+
#### Scenario: MeshManager Execute in VMD mode
23+
- **WHEN** `MMDMeshManagerObject::Execute()` runs in `MESH_MODE_VMD`
24+
- **THEN** no morph weight reading from libMMD's `MorphManager` occurs
25+
- **THEN** morph strength is applied solely through the `SetMorphStrength()` path called by `UpdateMorph()`
26+
27+
### Requirement: Report unmatched VMD morph names
28+
29+
The VMD import pipeline SHALL record morph names from the VMD file that do not match any morph in the target model's `morph_name_` map, and include them in the import log report.
30+
31+
#### Scenario: Import report includes unmatched morph names
32+
- **WHEN** a VMD file is imported and some morph names cannot be matched
33+
- **THEN** `LoadVmdMotionLog::not_find_morph_name_list` contains the unmatched morph names
34+
- **THEN** the import report dialog displays these names when `detail_report` is enabled
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
## ADDED Requirements
2+
3+
### Requirement: Group morph propagation to sub-morphs
4+
5+
GroupMorph SHALL propagate its strength to sub-morphs using additive semantics. When `UpdateMorph` is called, each sub-morph's strength SHALL be incremented by `group_strength * sub_morph_weight`, rather than being overwritten.
6+
7+
#### Scenario: Group morph adds to sub-morph strength
8+
- **WHEN** a GroupMorph has strength 0.8 and references sub-morph A with weight 0.5
9+
- **AND** sub-morph A already has strength 0.3 (from its own CTrack or manual setting)
10+
- **THEN** after `GroupMorph::UpdateMorph`, sub-morph A's strength becomes 0.3 + (0.8 * 0.5) = 0.7
11+
12+
#### Scenario: Group morph with zero strength does not affect sub-morphs
13+
- **WHEN** a GroupMorph has strength 0.0
14+
- **THEN** sub-morph strengths remain unchanged after `GroupMorph::UpdateMorph`
15+
16+
### Requirement: Flip morph propagation to sub-morphs
17+
18+
FlipMorph SHALL propagate its activation to sub-morphs using additive semantics. When `UpdateMorph` is called and the flip morph's strength is >= 0.5, each sub-morph's strength SHALL be incremented by the sub-morph's defined weight. When strength < 0.5, sub-morphs SHALL not be modified.
19+
20+
#### Scenario: Flip morph activated adds to sub-morph strength
21+
- **WHEN** a FlipMorph has strength 0.7 (>= 0.5) and references sub-morph B with weight 1.0
22+
- **AND** sub-morph B already has strength 0.2
23+
- **THEN** after `FlipMorph::UpdateMorph`, sub-morph B's strength becomes 0.2 + 1.0 = 1.2
24+
25+
#### Scenario: Flip morph not activated leaves sub-morphs unchanged
26+
- **WHEN** a FlipMorph has strength 0.3 (< 0.5)
27+
- **THEN** sub-morph strengths remain unchanged after `FlipMorph::UpdateMorph`
28+
29+
### Requirement: UpdateMorph execution order for compound morphs
30+
31+
The system SHALL process Group and Flip morphs before Mesh, UV, Bone, Material, and Impulse morphs during the `UpdateMorph` loop, ensuring compound morph propagation is complete before leaf morphs push values to their respective managers.
32+
33+
#### Scenario: Group morph propagates before mesh morph applies
34+
- **WHEN** `Execute()` runs the UpdateMorph loop
35+
- **THEN** all Group and Flip morphs have their `UpdateMorph` called first (propagating strength to sub-morphs)
36+
- **THEN** all other morph types have their `UpdateMorph` called second (pushing final strength values to mesh/bone managers)
37+
38+
### Requirement: Morph UI visible in VMD mode
39+
40+
The `MODEL_MORPH_GRP` group SHALL be visible in VMD mode, allowing users to see morph strength sliders. Add/delete/rename/editor buttons for morphs SHALL be hidden in VMD mode (same as non-edit mode behavior).
41+
42+
#### Scenario: Morph sliders visible during VMD playback
43+
- **WHEN** the model is in `MODEL_MODE_VMD`
44+
- **THEN** the `MODEL_MORPH_GRP` group is visible in the attribute manager
45+
- **THEN** morph strength sliders display current animated values
46+
- **THEN** morph add-name inputs and add/delete/rename buttons are hidden
47+
48+
#### Scenario: Morph sliders reflect CTrack animation values
49+
- **WHEN** the model is in `MODEL_MODE_VMD` and the timeline is playing
50+
- **THEN** morph strength sliders update in real-time to reflect CTrack keyframe interpolation
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
## 1. Group/Flip Morph 叠加语义改造
2+
3+
- [x] 1.1 修改 `GroupMorph::UpdateMorph`(mmd_morph.cpp):将 `SetStrength(node, GetStrength(node) * data.GetValue())` 改为叠加语义 `SetStrength(node, GetStrength(node) + self_strength * data.GetValue())`,先读取子 morph 当前值再加上 group 的贡献
4+
- [x] 1.2 修改 `FlipMorph::UpdateMorph`(mmd_morph.cpp):将覆写改为叠加语义,当 `GetStrength >= 0.5` 时将 `data.GetValue()` 叠加到子 morph 当前强度上
5+
- [x] 1.3 修改 `MMDModelManagerObject::Execute` 中的 `UpdateMorph` 循环(mmd_model_manager.cpp:885-891):分两轮遍历 — 第一轮只执行 Group/Flip 类型的 `UpdateMorph`(传播到子 morph),第二轮执行其余类型(Mesh/UV/Bone/Material/Impulse)
6+
7+
## 2. VMD 导入时创建 CTrack 关键帧
8+
9+
- [x] 2.1 在 `MMDModelManagerObject` 中实现 `SetMorphAnimation` 方法(参考注释代码 1611-1649 行),接受 morph 名称、帧号、权重,通过 `morph_name_` 查找 index → `morph_data_[index].GetStrengthDescID()` → 创建/复用 CTrack → 添加 CKey 并设置 `CINTERPOLATION_LINEAR`
10+
- [x] 2.2 在 `LoadVMDMotion` 中,`VMDAnimation::Add` 完成后,遍历 `vmd_file.m_morphs` 调用 `SetMorphAnimation`,将每个 morph 关键帧写入 CTrack
11+
- [x] 2.3 未匹配的 morph 名称收集到 `std::set` 或类似容器,导入完成后赋值给 `LoadVmdMotionLog::not_find_morph_name_list`
12+
13+
## 3. 导入日志反馈
14+
15+
- [x] 3.1 确认 `LoadVmdMotionLog::not_find_morph_name_list` 成员存在且类型合适,必要时调整或补充声明
16+
- [x] 3.2 在导入报告对话框中展示未匹配 morph 名称列表(当 `detail_report` 为 true 且列表非空时),参考已有的骨骼未匹配报告格式
17+
18+
## 4. 取消 VMD 模式表情 UI 隐藏
19+
20+
- [x] 4.1 修改 `MMDModelManagerObject::GetDDescription`(mmd_model_manager.cpp:2058-2062):移除 `MODEL_MODE_VMD` 下对 `MODEL_MORPH_GRP``DESC_HIDE` 设置
21+
- [x] 4.2 将 VMD 模式的 morph UI 可见性逻辑与非编辑模式统一 — 隐藏添加/删除/重命名按钮,保留强度滑块可见
22+
23+
## 5. 移除 MeshManager 的 VMD morph 覆盖路径
24+
25+
- [x] 5.1 移除 `MMDMeshManagerObject::Execute``MESH_MODE_VMD && mmd_morph_manager_` 的 morph 权重读取块(mmd_mesh_manager.cpp:283-301)
26+
- [x] 5.2 移除 `mmd_morph_manager_` 成员变量及其赋值/初始化逻辑
27+
- [x] 5.3 移除 `morph_manager_index_` 成员变量及其在 `RefreshMeshMorphData` 等位置的构建逻辑
28+
- [x] 5.4 清理 `mmd_mesh_manager.h` 中对应的成员声明和相关 include
29+
30+
## 6. 清理注释代码
31+
32+
- [x] 6.1 移除 `SetMeshMorphAnimation` 的旧注释代码(mmd_model_manager.cpp:1611-1649),替换为新实现
33+
- [x] 6.2 移除 `SetModelControllerAnimation` 的旧注释代码(mmd_model_manager.cpp:1651+),如果不在本次范围内则保留但添加 TODO 注释

0 commit comments

Comments
 (0)