# 文档层:生成器、覆盖与缓存 格式设计把工程定义为“意图 + 用户覆盖”:生成器只保存参数,生成结果由纯函数算出,用户在生成结果上的修改按稳定 ID 记录,生成器重跑时做三方合并。第一阶段以摆动生成器为样板验证了机制;本页记录推广到其余生成器、按测量接入生成缓存,以及工程格式 v2 的实现状态。 ## 已实现 | 部分 | 位置 | 内容 | | --- | --- | --- | | 内容哈希 | `format-compile` `document/ContentHash` | 各部分规范文本的 SHA-256,按长度分隔,IR 值可直接哈希 | | 依赖图 | `document/GeneratorGraph` | 生成器声明读取与拥有的稳定 ID;拓扑排序、禁止环、一个对象只能有一个拥有者;给出变化后需要重算的下游生成器 | | 生成缓存 | `document/GenerationCache` | 按输入内容哈希缓存生成结果(LRU);未命中时在锁外计算,慢生成器不阻塞其他键 | | 三方合并 | `document/ThreeWayMerge` | 逐键合并:用户未改的跟随生成器,生成器未变的保留用户值,两边都改且不同时保留用户值并报告冲突 | | 文档生成器图 | 根项目 `core/DocumentGenerators` | 把工程的生成器登记为依赖图节点;重放按图的顺序运行摆动与模拟;覆盖按图查找关键形的拥有者 | | 生成结果覆盖 | `core/GeneratedOverrides` | 摆动 Warp 关键形与烘焙模拟网格关键形上的画布编辑记录为 `generated_override`,在所有生成器之后按控制点 / 顶点三方合并重放 | | 骨架与动作缓存 | `core/SkeletonRig.generate`、`core/MotionPresets.tracks` | 骨架烘焙与生成动作按输入内容哈希接入 `GenerationCache` | | 重放检查点 | `core/legacy/ReplayCheckpoints` | 只用于没有固化点的日志:按基础 Rig 实例与日志前缀内容缓存重放的中间模型;追加只重放新条目,撤销命中已有检查点 | | 生成器复用 | `core/DocumentGenerators`(`GeneratorReuse`) | 摆动与模拟声明对象级读取;读取不变时把上次输出作为补丁套用,不重新生成 | | 拆分物化 | `core/ArtPrimitiveJournal`、`core/RigRegenerationCheckpoint`(`split`)、`application/WorkspaceArtPrimitives` | 拆分是一次再生成:部件取代原图层参与生成,用户对原图层的修改经三方合并带到部件上,`art_primitive` 记录之后写固化点;见[拆分物化](#拆分物化画元记录-art_primitive) | | 作者态 Rig 与固化点 | `core/RigEditOverlay`(`replayAuthored`/`finish`)、`core/RigCheckpoint`、`core/RigRegeneration`、`project/MaterializedRigStore` | 重放分为作者态阶段与收尾阶段;作者态 Rig 按修订保存并直接用于构建;生成结果在日志之下变化时三方合并并写固化点,见[固化点](#固化点)与[固化 Rig](MATERIALIZED_RIG.md) | | 工程格式 v2 | `project/ProjectFormatV2` | 每个修订拆为按内容寻址、带 schema 版本的文档节点;v1 打开后在下次保存时迁移,见[工程格式](PROJECT_FORMAT.md) | `format-compile` 中的四个框架部分为 MIT,不依赖任何 GPL 模块。 ## 预设设置草稿 预设控件只投影设置补丁,不把当前预览的 `RigEditOverlay` 或网格基线作为设置编辑提交。`WorkspaceSettingsIntent.patchSettings` 提供即时显示的联动规则;`WorkspaceGenerationCommands.prepareSettingsDraft` 在捕获的已提交文档上应用同一意图、释放作者姿态、重建并合并生成结果。预览与提交使用同一个纯候选,预览产生的检查点只存在于临时模型中。 滑块、数字输入与颜色弹窗按编辑会话划分操作;`WorkspaceSettingsDraft` 将连续数值采样合并为最终补丁,同时保留会释放姿态的开关切换顺序。完成会话后经 `WorkspaceDraftQueue` 和运行时 CAS 发布一个历史节点;无变化不追加。模拟与物理编辑继续使用各自的应用层命令。保存会先结束控件会话并等待草稿提交;旧编辑记录与历史快照保持原样。 ## 生成器依赖图 `DocumentGenerators.graph` 为每个工程构造如下节点(对象 ID:`document:<部分>` 为文档状态,`rig:<阶段>` 为整体 Rig 阶段,`parameter:` 为生成的参数,`keyform::<目标>@<参数>` 为生成器沿该参数在目标上增加的关键形): | 节点 | 读取 | 拥有 | | --- | --- | --- | | `rig.footprints`(网格轮廓) | 源图、设置、逐层网格设置 | `rig:footprints` | | `rig.scaffold`(锚点、面部、框架与变形器) | `rig:footprints`、图层分类、设置、骨架定义 | `rig:scaffold` | | `mesh:<图层>`(逐层网格与关键形;未给图层列表时合为 `rig.meshes`) | `rig:scaffold`、该层像素、分类与网格设置 | `rig:mesh:<图层>` | | `rig`(组装与纹理集绑定) | 各层网格、`rig:scaffold`、源图、设置、图层分类 | `rig:base` | | `skeleton`(骨架烘焙) | `rig:base`、骨架定义 | `rig:skeleton` | | `journal`(编辑重放) | `rig:skeleton`、编辑日志 | `rig:authored` | | `skeleton.skin`(日志安放网格的骨骼蒙皮;仅当日志把骨骼绑定的网格挂到自己的变形器下时存在) | `rig:authored`、骨架定义 | 这些网格在其肢体骨骼、上方身体半段与骨架姿态参数上的关键形 | | `swing:`(每个未烘焙摆动) | `rig:authored`、该摆动的设置 | 其参数,及其参数在每个目标 Warp 上的关键形 | | `simulation:`(每个已烘焙且启用的模拟) | `rig:authored`、该模拟的设置与烘焙 | 其模式参数,及模式参数在烘焙网格上的关键形 | | `overrides`(覆盖合并) | 所有生成的关键形、覆盖、生成器集合 | `rig:final` | | `seeThrough`(透过刘海的复制件) | `rig:final`、设置 | `rig:seeThrough` | | `physics`(物理组) | `rig:final`、`rig:seeThrough`、生成的参数、物理与骨架设置 | `physics` | | `motions`(生成动作) | `rig:final`、`rig:seeThrough`、`physics`、动作设置、骨架定义 | `motions` | - 重放(`RigEditOverlay.applyTo`)按图的拓扑顺序运行日志之后的节点:先 `skeleton.skin`,再摆动与模拟,与此前“先全部摆动、再全部模拟”的顺序一致;声明顺序打破平局。 - 透过刘海的复制件(`SeeThroughLayers`,见[变形器与参数参考](DEFORMER_AND_PARAMETER_SPEC.md#自动形变)):`rig` 节点只记录候选(被刘海覆盖的眉毛与各眼部图层及其刘海),`seeThrough` 节点在覆盖合并、图层显隐与绘制顺序覆盖之后(`AuthoredRig.finished` 末尾,`withSeeThrough`)按设置从原件制作复制件、为眼睛在刘海上开窗(刘海反向剪切,`BuiltRig.seeThroughWindows`),蒙版、绘制顺序与不透明度都由当时的原件与刘海得出;开窗改的刘海蒙版只在成品 Rig 中,作者态与日志里的刘海不带它们,检查器经 `authoredMasksOf` 显示和编辑文档自己的蒙版,旧版软删除删去全部开窗眼部图层时收回窗口(`withSeeThroughWindowsPruned`);复制件不在作者态 Rig 中,不拥有任何 `keyform:` 对象,在引用字段(拆分的引用字段与 `id`、`masked_by` 等)点名它的任何文档操作(`WorkspaceDocumentEdits` 分发处与日志记录处)以 `WorkspaceGeneratedCopyEdit`(`generated_copy`)拒绝;三项设置只触发重放,不算生成设置。复制件没有自己的图层条目,按图层处理网格的地方经 `BuiltRig.layerOf` 取原件的图层(旧版软删除随原件删去复制件、图集压缩与重新绑定);生成迁移读作 Rig 数据的构建不制作复制件(`SeeThroughLayers.withoutCopies`)。未提交的预览(画布各工具经 `CanvasEditor.preview` 的赋值、检查器实时字段、Warp 控制场)统一经 `SeeThroughLayers.refresh` 按预览中的原件与刘海重做沿用下来的复制件,蒙版、顺序与外观同提交时一致;整个重建的预览(摆动、内部造型试播)其复制件已是新实例,原样返回同一模型,按引用识别预览的地方不受影响。 - 文档若让两个生成器声明同一参数,先声明者保留拥有权,后者的声明被丢弃,重建仍可进行,冲突由生成器自身报告。 - `DocumentGenerators.stale(before, after)` 给出一次文档变化后需要重跑的生成器:读取了变化部分的生成器及其全部下游;被修改或删除的生成器原先拥有的对象也视为变化。例如调整一个摆动只重跑该摆动、覆盖合并、物理与动作,不重跑 Rig 生成、骨架或其他模拟。 - Rig 生成按阶段登记为节点(见下文[Rig 生成阶段](#rig-生成阶段));`graph(overlay, layers)` 给出图层列表时每层一个 `mesh:<图层>` 节点,某层像素变化只让该层网格、组装及其下游过期。 ## 增量重放 `RigEditOverlay.applyTo` 依次执行作者态(有固化点时从最后一个固化点开始,只重放其后的条目,见[固化点](#固化点);没有固化点时执行旧格式静态编辑——参数删除/创建、静态 Warp、结构、关键形 set/copy/delete——与 `authoringJournal` 全部条目)、摆动与模拟、覆盖合并和延后的面板编辑。生成的文档第一次编辑后就有固化点,之后最多重放 `CHECKPOINT_INTERVAL` 条;下面的重放检查点只服务没有固化点的日志(较早版本写下、尚未编辑过的修订)。摆动与模拟由 `GeneratorReuse` 按读取集复用。 **重放检查点**(`core.legacy.ReplayCheckpoints`,只用于没有固化点的日志) - 检查点是“旧格式静态编辑 + 前 `i` 条日志”之后的模型。查找条件:基础模型是同一个实例;旧格式部分相等(含摆动/模拟参数集合,它决定结构编辑哪些延后,以及界面语言);前 `i` 条每条都与检查点重放过的条目相同(先比引用,不同再比内容)。 - 失效只看前缀内容,不看下标。改写较早条目的命令(拖动合并、生成结果覆盖的捕获、网格规范化)会让该条及之后的检查点自然失配,从最近的有效检查点重放。 - 每条链保留下标 0、每 16 条以及最后 3 条的状态,均为软引用(内存紧张时释放);最多保留 4 条链(最近使用优先)。分支时保留旧链,供重做或切回使用。 - 追加一条只重放这一条;撤销到前一个状态直接命中,不重放。完整重建(重开、改变图块尺寸的绘画与换图)产生新的基础 Rig 实例,从头重放;不改变任何图块尺寸与位置的绘画或换图沿用同一基础 Rig 实例(见下文“纹理集与绘画”),检查点照常命中。 - 骨架在基础 Rig 内烘焙(日志之前),日志之后只有画布蒙皮写回。骨架编辑改变基础 Rig 的内容,此前所有检查点都不再成立,因此从头重放;“从第一条受影响条目之前续放”对骨架不适用。不按内容匹配基础 Rig:在 2 倍 tml(4096²)上整个基础 Rig 的 IR 内容哈希约 160 ms,而 26 条日志的完整重放约 6 ms;内容相同的重建由阶段缓存交回同一实例(见“Rig 生成阶段”),实例匹配已覆盖。 - `PuppetModel` 及其对象按不可变值共享。`-Dpsd2live.replayCheckpoints=false` 同时关闭检查点和生成器复用;代码中二者是两个开关(`ReplayCheckpoints.enabled`、`GeneratorReuse.enabled`),可分别关闭。 **生成器复用** - 摆动读取:每个目标 Warp 及其全部祖先(测量所在的父级空间)、参数列表与参数树、世界原点(自动支点)。写入:自己的参数、参数树和目标 Warp。 - 已烘焙模拟读取:目标网格和烘焙网格(几何、混合形、通道)、参数列表与参数树、运行时目标(决定是否用混合形)。写入:自己的参数、参数树和这些网格。 - 生成器运行后,其输出记录为补丁:新的参数列表和参数树,加上被替换的对象。记录前校验:只改了读取集中的对象,其余字段引用不变;否则不缓存。 - 设置相同、读取集逐项相同(先比引用,不同再比内容)时,直接把补丁套到当前输入上,读取集之外的对象保持当前值。比较引用几乎没有成本;按内容哈希读取集反而比生成器本身更贵(见下表)。 - 内容比较只会漏判(数组字段按引用比较),不会误判:漏判只是重新生成。 - `DocumentGenerators.stale` 仍给出文档级的重跑范围;重放时以更细的对象级读取比较为准,例如编辑无关网格时摆动和模拟都复用,编辑摆动目标 Warp 或其祖先时只重跑该摆动。 **效果**(`CommitPerfTool.baseline`,同机前后对比;“关闭”指关闭检查点与生成器复用,“追加”指先重放去掉最后一条的日志,再重放完整日志): | 场景 | 此前重放 | 关闭 | 追加一条 | 重复重放同一文档 | | --- | ---: | ---: | ---: | ---: | | 场景 a(骨架 + 两个摆动 + 烘焙模拟) | 约 3.7 ms | 约 4.3 ms | 约 1.0 ms | 约 0.6 ms | | 日志 64 条 | 约 16 ms | 约 20 ms | 约 0.7 ms | 约 0.1 ms | | 日志 214 条 | 约 43 ms | 约 45 ms | 约 0.4 ms | 约 0.1 ms | 追加 200 条几何编辑后,单次提交从约 98 ms 降到约 46 ms,提交内的重建从约 79 ms 降到约 23 ms;剩下的主要是 moc3 打包和几何安全检查,不再随日志长度增长。 ## 固化点 日志重放的前提是条目写下时所面对的生成结果不变。改变生成输入的编辑(头发模拟开关、骨架、生成设置与分类、拆分等)使基础 Rig 变化后,旧条目不再在新结果上重放,而是合并后保存为数据: - **触发**(`WorkspacePreviewBuilder.build`,`RigRegenerationCheckpoint.checkpointed`):新文档延续当前文档(`RigEditOverlay.continues`:日志以当前日志开头;共同开头没有固化点时旧格式静态字段也须相同,有固化点时不再读取它们),且不能走追加路径。生成新文档的基础 Rig,取旧条目所见的部分(`BuiltRig.resolvedPuppet(records)`:边界之前的 v2 拆分记录的部件就位,之后记录的乘客保留),与当前基础 Rig 的 `resolvedPuppet()` 按中立 IR 比较;不同则以当前基础为 G、新基础为 G′、当前作者态 Rig 为 M 合并(`RigRegeneration.merge`,G 与 M 先重新绑定到新纹理集),在当前日志末尾、本次编辑的新条目之前插入 `rig_checkpoint`。相同则照常重放。 - **父级覆盖与绘制顺序覆盖**:都是基础生成的输入,生成的模型上改动它们同样按再生成合并并写固化点(只经固化点构建时不生成基础 Rig,不合并就看不到它们)。 - **生成设置与分类**(`RigGenerationChange.changed` 认定的变化:脸部特征、嘴型、仅网格、头身强度、`rigTuning`、图层分类):生成的模型在准备编辑时即合并并写固化点(`WorkspacePreviewBuilder.normalizeMeshEdits`),G′ 由当前源图生成(保存的网格输入照用,`generationSource` 冻结之后加入的图层也在其中),不再写 `rig_generation_transition`、`rig_generation_frames` 与随迁移创建的网格记录。合并从当前日志末尾的作者态 Rig 出发,所以同一次编辑只能在其后追加条目;同时改写较早条目的编辑被拒绝(“分开提交”),不再静默地把旧条目重放到新生成结果上。导入 CMO3 的模型没有生成结果可合并,仍写迁移记录(见[旧版重放](#旧版重放))。 - **拆分**(`RigRegenerationCheckpoint.split`):见[拆分物化](#拆分物化画元记录-art_primitive);固化点写在拆分记录之后,记录本身不重放。 - **记录**(`RigCheckpoint`):作者态 Rig 的索引(对象在 `RigObjects`,随文档持久化)、它所基于的生成快照与合并问题。模型预设开启头发模拟时在预设内部同样合并并写固化点,再按合并后的 Rig 计算权重。 - **直接编辑**:生成的模型上,延续日志并追加条目的编辑在日志没有固化点、或最后一个固化点之后已有 `RigEditOverlay.CHECKPOINT_INTERVAL`(32)条时,先在原日志末尾、新条目之前写入当前作者态 Rig 的固化点(`RigEditOverlay.checkpointsBeforeEntries`、`WorkspacePreviewBuilder`);生成快照沿用上一个固化点保存的,没有时取当前基础 Rig。因此第一次编辑后任何构建最多重放 32 条加本次编辑的条目:追加的条目作用于当前作者态 Rig,改写较早条目(拖动合并、撤销、重做)的编辑从固化点重放其后的条目或取已保存的修订(`PSD2LivePipeline.updateRigEdits`、`RigPreviewModel.authoredWithoutBase`),都不生成基础 Rig。tml 样例上第一次编辑约 0.2 s、之后的固化约 50 ms,冷构建约 0.2 s。导入 CMO3 的模型同样写这两种固化点(不带生成快照、绑定键为 `cmo3-import`),在导入基线上从固化点重放。 - **重放**:`replayAuthored` 从最后一个固化点开始,其 Rig 经 `reboundTo` 绑定到基础 Rig 的纹理集,再逐条重放之后的条目(不经重放检查点;重放检查点 `core.legacy.ReplayCheckpoints` 只用于没有固化点的日志);`authoredRig` 的网格映射取自固化点头部。之后没有 `art_primitive` 记录时,构建直接由固化点进行(`RigEditOverlay.authoredFromCheckpoint`、`materializedPreview(rebind = true)`),不生成基础 Rig。 - **生成基线**:`rig_generation_baseline` 冻结的生成设置与分类只在其后没有固化点时约束基础生成(`RigGenerationBaseline.restore`);之后的条目都从固化点开始重放,基础生成按文档当前的设置进行。 - **提交**:运行时把重建插入的固化点并入提交的文档(`WorkspaceRuntime.adopted`);批量成员由前一个候选构建(`rebuildFrom`),合并的 M 是前一个候选的作者态。 - **收尾阶段**照常扫描整条日志:生成结果覆盖与延后的面板编辑不受固化点影响。 - **报告**:最后一个固化点的问题经 `RegenerationQuality` 进入 `workspace_inspect` 的 `quality.regeneration`。提交前可用 `workspace_preview_regeneration` 试运行:与提交相同地准备并重建候选,报告将写入的固化点(`regeneration` 带合并问题,`materialized` 为新条目前的作者态)与增删的对象,不发布任何东西(`WorkspaceDocumentCommands.previewRegeneration`)。 - **模拟烘焙**读取作者态 Rig 经收尾阶段的结果(`SimAuthoring.AuthoredRigs`、`RigPreviewModel.authoredPuppet`),不生成基础 Rig。 - 生成的模型启用骨架后不再在日志中重采样顶点组(合并迁移它们);导入 CMO3 的模型没有生成结果可合并,仍按原方式处理。 ## 生成结果覆盖 - 关键形的拥有者由图决定:编辑的键中某个参数离开默认值,且图中有生成器拥有 `keyform:<类型>:<目标>@<该参数>`,该格点就是生成的格点。 - **摆动**:拥有其参数轴在目标 Warp 上增加的关键形。覆盖记录生成器、目标 `warp:`、完整键、当时生成的格子(`base`)与用户格子(`points`),逐控制点合并。 - **烘焙模拟**:拥有模式参数在烘焙网格上的关键形(以关键形轴写回的模拟;以混合形写回时网格格子里没有该轴,编辑不会被识别为覆盖)。覆盖记录目标 `mesh:`、当时生成的顶点偏移(`base`)与用户编辑后的偏移(`points`,由在当前模型上套用该画布编辑得到),逐顶点合并。重新烘焙或改变增益后,用户没动过的顶点跟随新烘焙,用户动过而烘焙未改的顶点保留,两边都改的保留用户值并报告冲突。 - **通道**:生成器在自己的轴上键入的透明度、绘制顺序、乘算色与屏幕色关键形同样归它所有。某个通道的轨道在日志之后才有生成器的轴(作者态 Rig 的同一轨道没有这根轴)、编辑的键中该参数离开默认值时,该格点是生成的;关键形 `set` 写入它时,每个这样的通道写成一条带 `channel`(`opacity` / `draw_order` / `multiply_color` / `screen_color`)的覆盖:`base` 为当时生成的值、`points` 为用户的值(标量一个数,颜色三个分量),重放时逐分量三方合并,冲突与孤立照常报告(`GeneratedOverrideIssue.channel`,质量报告的 `evidence.channel`)。同一条 `set` 中生成器没有键入的通道、日志自己在该轴上建立的轨道仍是普通日志编辑。复制到或复制自生成的通道格点、删除生成器在通道上的键同样在记录时拒绝。没有作者态 Rig 的调用方(`ownedWrites` 不传 `authored`)不识别通道覆盖,行为与之前相同。目前没有生成器在日志之后键入通道,这条路径为静态主光、穿透显示等会写颜色 / 透明度关键形的生成器准备。 - 此前在这些关键形上的画布编辑写成普通 `canvas_geometry`,重放时参数尚不存在,整个重建失败,编辑无法提交;现在提交时改写为覆盖。 - 生成器被删除、烘焙(摆动)、取消烘焙(模拟)或改变轴后,键不再对应生成的格点,覆盖不生效并报告为孤立,不会落到其下的静止形上。网格重新生成后顶点数变化同样报告为孤立。 - 生成参数在默认值处的编辑仍是普通编辑,修改的是静止形状。旧工程中的命令不变;只有新的提交会写出覆盖。 - Bezier 编辑与关键形 `set`(含 MCP `rig_deform`)写入生成格点时同样改写为覆盖;保留子级位置的编辑、复制到或复制自生成格点、在生成器的轴上增删键无法作为覆盖表达,在记录时拒绝(`GeneratedOverrides.ownedWrites`)。 - 在其他对象上使用生成参数(打键、通道关键形、改名、改范围)时,先在日志中按生成器当前定义创建该参数(`GeneratedParameterAdoption`),之后参数属于文档,生成器沿用它;见[固化 Rig](MATERIALIZED_RIG.md#41-记录层与作用层)。 - 覆盖不影响基础 Rig 生成:生成几何缓存的键忽略覆盖命令,增删覆盖不会让基础 Rig 重新生成。 - **问题报告**:每次重放把未按记录生效的覆盖作为结构化记录(`GeneratedOverrideIssue`:类型 `conflict` / `orphaned`、生成器 ID、目标、完整键、冲突点数或孤立覆盖的点数、形状总点数,孤立时附原因 `missing_keyform` / `shape_mismatch` / `superseded`)存入 `BuiltRig.overrideIssues`,按日志顺序排列,不改变合并语义。被拆分取代的网格上的覆盖原因为 `superseded`。 - `core/quality/GeneratedOverrideQuality` 把这些记录转为观察报告(版本 4,领域 `overrides`,三项检查:`generated_overrides`;`superseded_entries`——版本 2 拆分之前、只因其取代的网格失败而按空操作重放的条目,等级 info;`see_through`——设置要求却没能制作的透过刘海复制件,`SEE_THROUGH_DROPPED`,等级 info):冲突为 `GENERATED_OVERRIDE_CONFLICT`,孤立为 `GENERATED_OVERRIDE_ORPHANED`,等级由规则枚举唯一决定(均为 warning,`can_proceed` 恒为 true),不从文本推断;没有问题时报告完整且 findings 为空。MCP `workspace_inspect`(scope `project`,工程已加载时)返回 `quality.overrides`;严格结构见 `WorkspaceQualitySchemas`。 - 检查器工具栏在有问题时显示计数徽标,选中对象的问题在检查器顶部列出;点击徽标切换为列出全部问题。文案按类型与原因代码本地化(中/英/日)。 骨架烘焙的修正关键形没有改为覆盖(`skeleton.skin` 写在日志安放网格上的关键形除外,见[日志安放的网格](#日志安放的网格)):骨架在编辑日志之前运行,日志中的画布编辑本来就在骨架之后重放,并会随网格重建一起迁移到新拓扑;改为覆盖反而会在重新划分网格后变成孤立。物理组与生成动作沿用已有的整体覆盖(同 ID 的用户物理组取代生成组,`builtin` 动作片段取代生成动作)。 ## 测量与生成缓存 `GeneratorCostTool`(`PSD2LIVE_TOOLS=1`)在 tml 上测量各生成器与对其输入做内容哈希的耗时,输出 `build/tools/generator-cost/report.txt`: | 项目 | 生成 | 输入哈希 | 结论 | | --- | --- | --- | --- | | 骨架烘焙(自动骨架) | 约 4.7 s | 约 5 ms(基础 Rig 的 IR + 骨架定义) | 接入缓存,命中后 Rig 生成约 20 ms | | 生成动作(全部预设,含骨架预设) | 约 16 ms | < 1 ms | 接入缓存,命中后约 1 ms;Rig IR 编译从约 16 ms 降到约 2 ms | | 摆动(两个) | 2.8 ms | 8.4 ms | 不做内容哈希缓存;按对象级读取集比较引用后复用输出补丁(见[增量重放](#增量重放)) | | 烘焙模拟写回 | 约 0.8 ms | 约 8.6 ms(烘焙 JSON) | 同上 | | 物理组目录 | 约 0.1 ms | 约 0.1 ms | 不缓存 | | 模拟烘焙本身 | 约 13 s | — | 显式操作,结果已保存在文档中,不重算 | - 骨架缓存键只含烘焙实际读取的内容:骨骼绑定的网格(完整,UV 为未绑定的图层偏移);挂骨骼、弯曲 Warp 与绑定网格所用的变形器(身体、呼吸、倾斜、腿部 Warp,头部旋转,骨骼自身 ID,绑定网格的父级)及其全部祖先;参数、部件、Glue 与参数树;其余变形器和 Drawable 只取 ID、种类、顺序与父级;另含骨架定义、身体框、绑定网格中的手工拓扑、站姿和界面语言(生成的变形器与参数名称已本地化)。纹理集、源文件与变形路径既不读也不写,不入键。命中但基础 Rig 不同时,未读取的变形器与 Drawable(按烘焙时的改挂父级)、纹理集、源文件与路径取自新基础 Rig,并重新推导绘制顺序;烘焙若以其他方式改动了未读取对象,该结果只对整个 IR 相同的基础 Rig 命中。缓存保留最近 4 个结果。撤销、历史切换、导出前后重复生成同一 Rig,以及骨骼未绑定网格的拓扑编辑或网格设置(不改变身体框时)都会命中;带骨架的完整预览重建从约 5.4 s 降到约 0.5 s。 - 为使内容哈希稳定,Rig 生成的眼部与嘴唇变形路径改为由网格 ID 和路径角色派生的确定性 ID,此前每次生成都是随机 UUID。 - 请求没有给出 ID 的新对象(画布与 MCP 创建的 Warp、旋转变形器、Glue、变形路径,拆分片段、深度拆分的图层与网格、导入与添加的图层)由 `StableIds` 从请求内容(去掉 `state`、`request_id` 等发送信息)派生 ID:同一文档上的同一请求,重试、试运行与提交得到相同的 ID,已占用时依次加 `_2`、`_3`。需要在批量中引用的对象仍应在请求中给出 ID。日志与文档中已有的 ID 原样保留。会话、任务、历史节点、参数快照、素材工作流记录等不属于 Rig 或文档对象的 ID 不受此约束。 - 生成动作的缓存键包含预设名、骨架定义、调整值与排除的参数。 - 基础 Rig 自身仍由已有的 `GeneratedGeometryCache`(保存了生成输入的工程)和快速重放路径处理。前者保留最近 4 个结果(只有最新一个强引用,其余为软引用,内存紧张时释放),撤销与历史切换回到近期输入时复用。 - `canvas_geometry` 日志条目从版本 2 起保存相对执行位置几何的稀疏量化增量(`v`、`n`、`q`、可选顶点索引 `i`、整数增量 `d`),生产方仍提交绝对点,由 `RigAuthoringJournal.compile` 编码,预览与提交应用的都是解码后的命令;版本 1 的绝对点照常重放。 - 编辑器预览的 moc3 包不再回读校验(`validateBundle`),写给用户的导出仍校验;测试 JVM(启用断言)或 `psd2live.validatePreviewBundles=true` 时预览同样校验。 - moc3 包中 physics3/motion3 的生成与规范化(展开指数的正则替换)此前约 18–21 ms,占整包近一半;`Moc3Target` 按输入(物理组、动作、参数集合、文件名前缀)缓存最近 4 份文本,几何编辑不改变这些输入。Rig IR 编译的动作列表同样按输入缓存最近 4 份,此前每次编译都为每个预设序列化骨架定义查生成动作缓存(约 2.5 ms,IR 编译 2.9 → 0.3 ms)。`POS_VALUES` 先算出总长再整块写入,写入器在大小恰好时直接交出缓冲区而不再复制(tml 骨架 Rig 的 moc3 约 7 MB,写出 13.7 → 6.8 ms)。整包从约 48 ms 降到约 20 ms,导出逐字节不变(`ExportGoldenTool`)。逐 Drawable 的 IR 缓存没有做:`PuppetIr.toIr` 只包装数组,约 0.2 ms。 - 几何安全检查比较对象时不再把每个网格和变形器的几何装箱成列表:同一实例直接视为相同,其余按数组逐位比较。逐坐标采样复用每个目标的参考面积与有效性检查,网格采样跳过选择器用的归一化域,基线与候选点相同时复用状态。完整报告与此前逐字节一致(`SafetyGoldenTool` 核对 24 组随机编辑),普通网格编辑从约 15–30 ms 降到 2 ms 以内;绑定骨骼、组合域被采样到 16384 个坐标的网格仍需约 1 s(此前约 2–3 s)。提交另按新增日志命令缩小比较范围,规则见 [MCP_AUTHORING.md](../agent/MCP_AUTHORING.md#几何检查与试运行)。 - Rig 生成不读取纹理集布局:`RigBuilder` 先生成未绑定 Rig(`BuiltRig.unbound`),网格的纹理坐标是顶点相对图层矩形左上角的画布单位偏移,纹理集只有不带放置的图块、没有页面;随后 `UvBinding` 按当前放置换算成页 UV 并写入页面与图块。同一组图层换一种排布时,未绑定 Rig 及其内容哈希不变,只有绑定结果改变。使用偏移而非 0..1 图层单位,是为了让绑定逐位复现原有的页 UV(导出逐字节不变)。骨架在未绑定 Rig 上烘焙(自 `BAKE_VERSION` skeleton-3 起):关节细分新增的顶点按图层偏移插值,随后与其他顶点一起由 `UvBinding` 换算成页 UV,因此烘焙不读取纹理集,移动、缩放或重排图块都命中骨架缓存(tml 带自动骨架时,固定图块、改密度、改预算的单次提交由约 4.5–10.8 s 降到约 30–140 ms,另软删除一层时约 90–150 ms;`TexturePerfTool`)。与此前在页 UV 上插值相比,只有新增顶点的 UV 在末位舍入上不同。canvas、偏移、栅格与页 UV 之间的换算统一由 `LayerTexture` 提供(绘画重映射、生成 Rig 重排到当前纹理、网格迁移日志与 CMO3 导入共用)。 - 纹理集与绘画(`AtlasLayout`):自动布局是规范的矩形排布(`RectPack`,MaxRects:每块四周留 `padding`,按面积、长边、高三种顺序各用“最短边剩余”与“最低底边”两种评分,取页数最少、末页用高最小者),只由文档决定;新建、重开、撤销、绘画提交与导出得到同一布局。求解结果按图块尺寸、锁定、固定与预算缓存(最近 8 个),不改纹理尺寸的提交不重新求解。增量的是页面而非布局:每页按“页尺寸 + 各图块矩形与像素摘要”的配方合成并缓存(软引用,最近 8 页),配方未变的页直接沿用上一模型的同一 `AtlasPage` 实例;变化的页重新合成,并记下与它所替换页面不同的行。没有采用“未变图块固定、变化图块另找空位”的历史相关布局:尺寸不变时两者相同(图块原位重绘);尺寸变化时任何布局都会改动该图块的 UV,原生预览都要从内存重建模型,而规范重排只多出一次页面合成与所跨行的条带编码(约 20 ms),换来布局与编辑历史无关。 - 页面 PNG 惰性编码:`AtlasPage.png` 是导出写出的 ImageIO 规范编码,只在导出或 IR 编译读取时生成;编辑器运行包使用 `previewPng`,按 16 行一条带、Sub 滤波、各自独立的 deflate(level 1,以 full flush 结束)编码,每条带一个 IDAT 块,Adler-32 由各条带合并。解码像素与规范编码完全相同。派生页只重新编码脏行所在条带,其余条带取自被替换页;整页从零编码时条带并行。未变页的字节数组保持同一实例,预览包指纹不再重新哈希,`CubismPreviewReload` 只上传变化页。 - 绘画候选不再做第二遍打包:保留网格的绘画(及换图)在 `WorkspaceRasterEdits` 中只裁剪像素(`RasterPaintCommit.paintedSource`)并冻结生成输入,Rig、纹理集与运行包全部来自随后的一次重建;需要重建网格的绘画仍调用 `RasterPaintCommit.prepare` 取得迁移记录,但不再编译预览运行包。重建时 `WorkspacePreviewBuilder` 把当前模型的纹理集交给 `buildPreview`,只借用其页面与条带,结果与从零构建完全相同。 - 重排后的基础 Rig 按“生成几何缓存条目 + 布局 + 纹理图层元数据(不含像素)+ 新建网格 ID”缓存最近 4 个:图块尺寸与位置不变的绘画得到同一基础 Rig 实例,重放检查点继续命中。图层透明度统计(`LayerClassifier`)按栅格数组身份缓存,合成预览图(`PreviewRenderer.composite`)按各图层栅格身份与位置缓存最近 3 个,重建只重新扫描被绘画的图层。 ## Rig 生成阶段 `RigBuilder.build` 拆为带声明输入的阶段,每个阶段的输出由 `RigStageCache`(每个管线一份,挂在 `PreviewMeshCache` 上)按“只含该阶段所读内容”的键缓存。键取已有的廉价摘要与设置子集:栅格按数组身份记忆的 SHA-256(`RasterDigest`),图层元数据(ID、名称、组、顺序、边界、语义、足迹边界与质心、不透明像素数),以及 `stageConfig`——只保留网格、嘴部、强度与调校、生成模式和运行时目标;骨架、日志、逐层表、显隐、绘制顺序、纹理/物理/动作/导出设置一律不入键,读取骨架的阶段自行以所读部分入键。 | 阶段 | 键(所读内容) | 容量 | | --- | --- | ---: | | 足迹 `footprint` | 栅格尺寸与摘要、整数位置、alpha 阈值、该层网格设置、网格尺度 | 512 | | 框架(每次计算,约 1 ms) | 带足迹的分析:锚点、面部 Rig、头/脸/头发框、站姿与身体框 | — | | 变形器 `deformers` | 站姿内容键、各框、面部 Rig 全部数值、锚点、有左右侧的图层(元数据 + Rig 空间边界)、上衣边界、设置、骨架只取开关与第一根可用上身骨的两端(`torsoFrame`) | 8 | | 面部前倾 `lean` | 变形器列表实例 | 8 | | 网格 `mesh:<图层>` | 图层元数据与像素摘要、Rig 空间边界与质心、父级及其框、头部空间、眼部图层取面部 Rig、匹配的眼白边界、网格尺度、该层网格覆盖、设置 | 1024 | | 通道 `channels` | 类型、参数、开关键、语义、牙齿淡出 | 256 | | 嘴唇 `lips` | 所属 Drawable、网格阶段输出实例、父框、唇层元数据/像素/是否打包/绘制顺序/显隐、设置 | 64 | | 绑定与骨架 `bound` | 组装模型(结构相等,网格按实例)、头发模拟开关、纹理集放置与页尺寸、各层绑定元数据、骨架定义与输入、其余结果字段、骨架缓存清空次数 | 4 | - 组装(遮罩、部件、参数)每次重做,约 1 ms。所有阶段命中时 `bound` 直接返回上次的同一 `BuiltRig` 实例,`generatedBase` 的重排缓存改以几何 Rig 实例(而非几何缓存条目)为键,因此只改了任何阶段都不读的内容(例如追加日志条目使生成几何缓存失配)时,基础 Rig 仍是同一实例,重放检查点继续命中。 - 命中 `bound` 时把其骨架烘焙标为最近使用(`SkeletonRig.touch`),保存工程时头部缓存(`ProjectHeadCache`)取到的仍是当前烘焙;`SkeletonRig.clearCache` 递增的计数入键,清空后会重新烘焙。`bound` 仍以纹理集放置为键,但其中的骨架烘焙不读放置:纹理集变化时 `bound` 失配,骨架缓存命中,只重做绑定。 - 网格设置改变时,该层足迹可能移动锚点与框架,父框变化的网格随之重建(tml 上改下装网格约重建 5 个网格),面部各层网格保持原实例。 - `PreviewMeshCache` 的栅格摘要改用按数组记忆的 `RasterDigest`,此前每次调用都重新哈希每个图层。 - `-Dpsd2live.rigStages=false`(`RigStageCache.enabled = false`)关闭阶段缓存与摘要记忆,供对比。 **正确性**:`ExportGoldenTool` 与改动前(bcf919f2)逐字节一致。 **骨架提交**:骨架编辑(建骨、绑定、移动关节、删骨)使 `bound` 失配并重新烘焙骨架;烘焙在 `SkeletonRig` 内再分两级缓存,未触及的肢体不重算: - 单网格蒙皮(`skinDrawable`,烘焙的主要耗时)按其读取的全部内容缓存:该网格与其变形器链(肢体树各骨、父级、宿主及全部祖先)的无损 IR、IR 中出现的参数与该树各骨参数的范围与默认值、静止画布顶点、树内各骨及其父骨、预设姿势、归属骨、树内的混合形骨、采样、手绘权重与宿主。值为软引用,最多 128 项,`SkeletonRig.clearCache` 一并清空。 - 关节细分(`SkeletonMeshRefine`)按网格数组、静止画布顶点、关节带与段数缓存。 - 整次烘焙的缓存键对同一基础 Rig 实例与相同输入只计算一次。 - 被触及网格本身的蒙皮:每个样本(某组骨骼参数值下的 `computeDeltas`:各骨变换、关节模板、ARAP 与表面平顺)只取决于其参数值,因此拟合、关键形格点与姿势混合形所需的样本成批在公共线程池上并行求解,按原顺序读回,结果与线程数无关、与顺序求解逐位相同(`SkeletonRig.parallelSamples` 仅供测试关闭)。拟合(`fittedSides`)各骨两侧同步推进,每轮按原顺序检查各段,只预取少数后续段,多算的只是失败段之后的样本。ARAP 的全局方程(自由顶点的拉普拉斯矩阵加上该姿势的引导罚项)在一次求解的全部局部/全局迭代与两个坐标轴上不变,改为每次求解做一次包络 Cholesky 分解(逆 Cuthill-McKee 排序与包络按网格只算一次),每次全局步只做两次三角回代,取代每步最多 200 次迭代的共轭梯度;与共轭梯度结果的差异在 4096² 画布上不超过 0.0005 px(`SkeletonBakeDiffTool`,全部关键形与混合形键,渲染逐像素相同),`BAKE_VERSION` 为 skeleton-4、蒙皮缓存版本为 skin-2,旧版本写入的头部缓存不再命中。拟合容差仍为 1 画布像素:样本数随画布增大而增加,但在当前耗时下不必为此改变已有工程的关键形数量。 - 变形器阶段只以开关与上身骨入键(见上表),其他骨骼、绑定、姿势与权重的改动保持变形器实例。 规范化网格编辑(`normalizeMeshEdits`)在有顶点组时先构建一次不含顶点组条目的预览以迁移权重,随后的提交构建命中进程级烘焙缓存,只多出非骨架部分(约 60–120 ms)。顶点组条目可经 `SkeletonCanvasSkin.lockedTopology` 影响基础 Rig,故不把前一次构建直接用于后一次。 只改日志的提交走快速路径(`PSD2LivePipeline.canFastUpdateRig`,在缓存的基础 Rig 上重放),但骨架烘焙也读取日志:手工拓扑(`canvas_topology`)的网格、骨骼绑定的拆分部件记录、日志放置的画布蒙皮网格及其顶点寻址记录的顶点数(`RigBuilder.skeletonJournalInputs`)。这些内容变化时(例如对骨骼绑定网格的拓扑编辑及其撤销)不走快速路径,改为重建,结果与冷重建一致(`SkeletonIncrementalBakeTest`)。 效果(`SkeletonCommitTool.profile`,tml 每边放大 2 倍,4096² 画布,自动骨架 + 24 条几何日志 + 一个顶点组;同机前后对比,经 `WorkspaceDocumentCommands` 的提交墙钟时间): | 提交 | 此前 | 现在 | 说明 | | --- | ---: | ---: | --- | | 新建骨骼(未绑定) | 约 6.6–7.6 s | 约 0.25 s | 此前每次重做两只手部网格的蒙皮(各约 3 s)与关节细分 | | 把小网格绑定到新骨 | 约 6.2 s | 约 0.2 s | 只蒙皮新绑定的网格 | | 移动无网格骨的关节 | 约 5.5–7.3 s | 约 0.25 s | | | 移动蒙皮最大网格(654 顶点手臂)的肢体关节 | 约 2.2–3.2 s | 约 0.5–0.77 s | 该网格重新蒙皮:709 个样本,ARAP 约占 87%;现并行求解 830 个样本并分解求解 ARAP,蒙皮约 1.9–2.9 s → 0.14–0.34 s;剩余主要为关节细分(约 0.17–0.2 s)与提交构建 | 剩余主要为规范化构建与提交构建中的分析与运行时包。`SkeletonCommitTool.equivalence` 在建骨、绑定、移动、几何编辑、撤销、重做、切换分支与删骨后逐步比较运行时模型与清空全部缓存、关闭阶段缓存与检查点的冷重建(重放后 Rig、基础 Rig 的 IR 哈希与运行时文件),全部一致;`SkeletonIncrementalBakeTest` 覆盖缓存命中范围与冷烘焙一致,并比较并行与顺序求解样本的烘焙逐位相同;`SkeletonArapTest` 比较分解求解与共轭梯度求解。 **效果**(`CommitPerfTool.rigStages`,`PSD2LIVE_TOOLS=1`,同机同次运行,先关闭再开启阶段缓存;经 `WorkspaceGenerationCommands` 的提交墙钟时间中位数): | 场景 | 关闭 | 开启 | 说明 | | --- | ---: | ---: | --- | | 单层网格覆盖(场景 a:骨架 + 两个摆动 + 烘焙模拟;308×564 腿部图层) | 约 334 ms | 约 112 ms | 提交内 Rig 构建合计约 89 → 23 ms,分析约 68 → 27 ms | | 改分类(tml 去骨架,眼睫改头发) | 约 2.44 s | 约 2.38 s | Rig 阶段全部命中;剩余为分析:生成迁移多次 `CharacterAnalyzer.analyze`(约 0.5 s/次)与拆分基线分析 | | 完整重建(新构建器,重开) | 约 2.16 s | 约 2.20 s | 冷构建,阶段缓存不起作用 | 场景 a 上的分类修改会被生成迁移拒绝(“Generation mesh neutral frame cannot be inverted”),因此改分类在去掉骨架的文档上测量。分类提交的主要耗时已不在 `RigBuilder` 内,而在分析(`RigGenerationSource.prepare` / `CharacterAnalyzer.analyze` 对迁移产生的新栅格重新分类)与迁移本身的多次构建,见[后续](#后续)。 ## 逐层尺寸 图层的画布位置与像素数量相互独立(`core/LayerSpace.kt`): - 画布矩形:浮点画布单位,即源图层的 `rect`,缺省为整数边界。它决定图层在画布上的位置,绘画、换图等像素操作不改变它。 - 栅格:图层实际的源像素 W×H,可以是任意分辨率。 - 原生密度:栅格像素 / 画布单位,两轴可以不同。栅格恰好覆盖整数边界(一像素一画布单位)且没有分数矩形的图层与此前完全相同。 几何(`core/CanvasDensity.kt`): - 分析、分类、锚点、轮廓、网格生成、嘴唇与连通块拆分都按画布单位读取栅格。密度不为 1 或矩形为分数的图层在分析时换成画布分辨率视图 `CanvasDensityLayer`:同一图层,栅格按面积平均(放大时双线性)重采样到整数边界、一像素一画布单位,矩形外为透明。因此在 `meshTrace = CANVAS` 下,32 单位矩形上的 1024² 栅格与 32² 栅格生成相同的网格;完全透明的占位栅格不换视图。`meshTrace = TEXTURE`(新工程默认)时只有网格轮廓改读贴图本身(按原密度铺到整数边界,按最大 alpha 缩减到至多网格单位 4 倍细的工作分辨率),顶点间距与偏移仍是画布单位,见[网格拓扑](MESH_TOPOLOGY_AND_SPLIT.md#描轮廓的分辨率);分类、锚点等其余分析仍读画布视图。 - 生成器的纹理坐标仍是相对整数边界左上角的画布单位偏移。`UvBinding` / `LayerTexture` 按纹理图层(`textureLayer`,视图背后的原栅格)的空间把偏移换算到原栅格再到图块:`raster = (offset − (rect − bounds)) × 密度`,矩形为整数时减项为 0,结果与此前逐位相同。 - 预览合成(`PreviewRenderer.drawLayer`)、素材工作流和观察渲染把栅格拉伸到画布矩形绘制。 纹理集(`AtlasLayout`): - 每个图块的纹理像素 = 栅格像素 × `textureUpscale` × 密度倍率(`textureOverrides.density`,缺省 1,即栅格原分辨率)× fit。`textureUpscale` 仍是按摘要缓存的源图预处理,效果与此前相同。 - 页尺寸取预算的 `pageSize`,并与此前一样增长到能容纳最大图层的画布分辨率尺寸(下一个 2 的幂,至多 16384);密度带来的超出部分由 fit 处理。 - fit ≤ 1 是所有未锁定图层共用的一个系数:先按 fit 1 排布,不超出 `maxPages` 页即用 1;否则在 1/4096 的整数步长上二分查找能放下的最大步长。结果只由文档决定,相同输入得到相同布局。锁定图层(`lock`)保持 fit 1;固定图层(`pin`)先放在指定页面位置,矩形排布绕开它们。 - 锁定或固定的图层单独就超出预算时,不会悄悄缩小:解除锁定与固定后一起求 fit,并在 `PackedAtlas.notices` 中说明(只写入程序日志);连 1/4096 都放不下时按 fit 1 超出页数并说明。fit < 1 同样给出说明。说明进入导出警告。 - 图块尺寸不等于栅格时按 `RasterResample`(`format-compile`,确定性,预乘 alpha)重采样;页面配方以“图块矩形 + 像素摘要”为键,不受影响。放置的 `scaleX/scaleY` = 图块像素 / 栅格像素。 - 默认值(所有栅格等于边界、默认密度、已有设置、fit 1)下布局、UV、页面与导出与此前逐字节相同(`ExportGoldenTool`)。 - 保存的排布(`atlasArrangement` 设置,`project/AtlasArrangement.kt`):存在时 `AtlasLayout` 不再求自动布局,而由 `AtlasArrange.keep` 按保存的位置与 fit 放置:仍在预算页内、未出页、且不与已放置图块冲突的图块原位保留;其余(新增图层、变大后与邻居重叠或出页的图块)按页面 4×4 像素单元格网格放入第一个空闲位置;只有原有存储位置失效的图块给出说明,新增图层(包括被拆分替代、尚未压缩掉的原图层)静默放入。矩形图块之间按 `padding` 间距判断,涉及网格覆盖区域时按单元格判断并按 `ceil(2×padding/4)` 格扩张。保存的覆盖区域每次构建按图块当前栅格校正(`AtlasArrange.covering`):单元格网格与栅格尺寸不符(绘画或换图改变了裁剪)时作废、按整块矩形处理;栅格中 alpha ≥ 8 的像素落在覆盖区域外(绘画超出网格)时补上这些单元格,因此相邻图块与之冲突时被挤到空位,而不是互相覆盖。移到空位的图块同样按覆盖区域搜索并只写入自己的单元格。冻结规范布局得到的排布重建出逐字节相同的页面。 - 一次性排布(`atlas_pack`,`AtlasLayout.arrange`):网格覆盖区域取自当前模型最终网格的纹理坐标并补上栅格中网格外的像素单元格(`WorkspaceAtlasFootprints`,单元格 4 栅格像素、每边至多 192 格,三角形与单元格相交需深入 0.01 像素,避免布局变化后的末位舍入改变结果),按面积、高度、长边三种顺序在单元格网格上自上而下、自左而右首次适配,取页数最少、末页用行最少者,矩形可以互相嵌套;放不下预算页数时 fit 先按 64/4096 步长二分,再细化到 8/4096。只按矩形排布(`shape=rect`)直接冻结自动布局的结果。页面合成时,带覆盖区域的图块只写入自己的单元格(`TileKey.mask` 入配方),升维纹理也不向邻居外扩边缘。之后的网格编辑若超出排布时的覆盖区域,纹理集读取的 `notices` 提示重新排布。自动排布(`auto=true`)删除保存的排布;关闭或移动图块时保存当前布局。保存的排布下,没有可保持位置的图块(新图层、拆分部件、因邻居变大被挤出的图块)每次重建都会重新放入第一个空闲位置;因此移动图块和修改密度/锁定时先把当前模型的所有图块位置写回排布(`WorkspaceTextureEdits.settled`),这些图块之后保持原位,不会填入移走或缩小后空出的位置;同时为没有覆盖区域的图块补上其网格的覆盖区域(`meshed`,`WorkspaceAtlasFootprints`),此后图块只按网格实际使用的单元格判断重叠,与纹理集视图拖动时的判断(`WorkspaceTextureView.meshFootprint`)完全一致,视图允许的位置就是落地的位置。图块可绕中心旋转(`ArrangedTile.rotation`、`AtlasPlacement.rotation`):页面合成按旋转反采样(`AtlasLayout.writeTurned`,只写网格单元格),覆盖区域按旋转后的单元格求交(`AtlasArrange.shape` 的 `rotation`),网格 UV 经 `LayerTexture.tilePlacement` 以 Umamo 的旋转放置换算,导出同样带出旋转。纹理集视图的移动、缩放、旋转为编辑会话(`TextureWorkspaceState.session`),只改视图显示;应用时把密度与位置作为一个 `layer_set_pixel_density` / `atlas_set_tile` 批量提交,一次重建、一个历史节点。这两种编辑(单项或批量,GUI 或 MCP)在发布前由 `WorkspaceTextureEdits.requireKept` 比较候选模型:任一此前保持位置的图块在新布局中被挤离排布位置即抛出 `WorkspaceTileCollision`(`tile_collides`),整次不提交;批量只检查最终布局,并且只在批量不含重新排布、改预算、替换像素或移动画布矩形时检查。成对判断(`AtlasArrange.meet`)先比较按间距外扩的旋转外接框,相交才比较网格单元格,避免紧密矩形排布补上覆盖区域后因单元格取整误判相交。 - 网格身份:改变生成输入的编辑(增删图层、改分类或生成设置)在规范化时先把当前每个网格的 ID 写入 `splitDrawableIds`(`WorkspacePreviewBuilder.pinnedIdentities`),生成器按此命名;否则按分类与同类序号派生的 ID 会改名或重新编号,再生成合并会把用户的旧网格当作退役对象保留在新网格旁。 - 删除图层(`layer_delete`,`WorkspaceLayerEdits`)是真删除:图层离开源图与生成输入,它的网格经日志记录 `layer_delete`(`LayerDeletionJournal`)离开作者态 Rig,构建在该记录前写固化点,之前的创建记录不再重放;撤销即切换历史。以下延后过滤只用于旧版本软删除的图层(`deletedLayerIds`,只读,`layer_restore` 可恢复)及嘴唇条带等派生行。 - 软删除图层(含旧式拆分留下的原图层)在延后过滤模式下仍参与完整生成与重放,完整纹理集为其打包图块。过滤后的模型只按保留图层重新打包(`RigLayerDeletion.compact`),剩余网格经 `RigGenerationSource.repack` 移到新纹理集,按画布单位换算,采样同一纹素;预览、纹理工作区、CLI 与 GUI 导出因而都不含已删除图层的像素。基础 Rig 所绑定的完整纹理集记在 `RigPreviewModel.generationAtlas`,快速重放(`updateRigEdits`)在过滤后同样重绑。导入 CMO3 保留其原布局;剩余网格无法在任一纹理集找到图块时退回完整纹理集。恢复图层后重新得到完整纹理集。被拆分替代的原图层(`ArtPrimitiveJournal.visibleAnalysis` 过滤掉的)同理:生成时仍打包,预览与导出都在重放后按可见图层压缩;生成几何纹理集(`generationSource` 的几何 Rig)不读纹理覆盖与保存的排布,纹理提交不使几何缓存失效。 导出: - moc3 只读取页面与页 UV,无需额外处理。 - cmo3(`Cmo3LayerArt`,`targets/cubism`):Cubism Editor 图层一像素一画布单位,且编辑器会从图层重建纹理集。默认 `canvas`:密度不为 1 的图层以画布矩形尺寸(降采样)写入分层图像,存储的纹理集页仍保留高分辨率,图块打包缩放随之增大;只要某个图块的纹理集密度(纹理像素 / 画布单位)大于 1,导出给出 `TEXTURE_SIZE` / `APPROXIMATED` 损失项(“Cubism Editor rebuilds the atlas from canvas-resolution layers”),原有 `textureUpscale` 工程同样适用。内部设置 `layer_art=native` 按原分辨率写入图层并以模型图像变换缩放到画布,尚未经编辑器实测,默认关闭。 换图(`core/LayerImageReplace.kt`): - `LayerImageReplace.replace(document, layerId, raster, fit)` 返回候选文档:画布矩形与整数边界不变,只替换栅格(`STRETCH` 直接拉伸,`CONTAIN` 保持比例居中并补透明),首次替换时把此前的源图冻结为生成输入(与保留网格的绘画相同)。网格、关键形与绑定不变,重建后只有绑定的 UV 与页面不同。调用方照常重建并提交。 导入(`core/LayerImport.kt`、素材工作流、CMO3 导入): - 图片文件导入(`layer_import_images`)裁剪透明边缘后保留原栅格;超过画布时只把画布矩形等比缩小到画布内,栅格不变,因此密度大于 1。能放下的图片仍为一像素一画布单位的整数边界,与此前相同。 - 导入的图片一次提交即在作者态 Rig 中有自己的网格(`canvas_mesh_create`),不冻结生成输入、不写占位:生成器把创建记录拥有的图层当作透明占位(`RigGenerationSource.createdCoverage`),只为它打纹理块。 - 图层整体变换(`layer_transform`,`application/WorkspaceLayerTransform`):图层的帧(整数边界与浮点矩形)不变,像素、纹理图块和纹理坐标都锚定在帧上;变换 `LayerTransform`(2×3 仿射)记在图层上,图层的每个网格在静止姿态下写一条物体模式 `canvas_geometry`。生成器只读帧,不读变换,所以移动是作者的编辑而不是再生成。绘画会话在帧中进行,画布按完整变换显示(会话栅格画成随图层旋转的四边形,笔触与形状按逆变换落到帧上,笔刷大小按变换的面积比换算)。物体模式拖动覆盖某图层全部网格时,画布以该操作提交(`AffineFit` 拟合拖动前后的点,依次取纯平移、轴向缩放、一般仿射中第一个在 0.01 画布单位内吻合的,未旋转的拖动不会因浮点噪声记成旋转;变换复合时舍去百万分之一级的残差,`isAxisAligned` 容许 1e-5 的相对转角)。`layer_set_canvas_rect` 即从当前显示矩形出发的一次变换。只在图层变换上不同的源图对生成与纹理集是同一份(`PSD2LivePipeline.sameArt`),因此移动走快速路径,模型改用新源图;只增删创建记录拥有的图层或移动图层的提交跳过再生成比较(`WorkspacePreviewBuilder.onlyUsersLayersChanged`)。开发工具 `LayerFlowPerfTool` 测量导入、移动、换父级、重建与删除的提交耗时。 - 素材图层(`layer_add_from_asset` / `layer_set_placement`)以素材像素为栅格、放置的画布矩形为图层矩形;`WorkspaceCanvasPlacement.canvasUnitsPerPixelX/Y` 为实际比值。平移/缩放/显式镜像的配准不插值(镜像只翻转行列),只有带旋转的配准按素材自身密度栅格化到外包框。 - CMO3 导入按每个图块的模型图像变换(`_materialLocalToCanvasTransform`,umamo `Cmo3AtlasIngest.canvasTransformByTile`)与打包缩放求页面纹素/画布单位:大于 1 时图层在同一整数边界上以该密度重建栅格(例如高密度图层只存于纹理集页、或图层本身按模型图像缩放到画布两种写法),否则与此前逐字节相同。umamo 源图层清单仍按文件记录分层图像上的矩形,不换算到画布。 - 尺寸限制(`LayerSizeBudget`):栅格与画布分辨率视图(外包框)各不超过 16MP,原生密度每轴不超过 256 像素/画布单位,矩形每轴至少 0.5 单位;超限明确拒绝,CMO3 导入超限时整体降低密度。 绘画(`core/PaintSpace.kt`): - 绘画在图层自身的栅格网格上进行(`PaintSpace`):草稿图像的像素就是图层的栅格像素,按同一密度向画布边缘延伸,超过 16MP 时按比例收缩图层周围的边距。手势、半径、线宽与取色都是画布单位,经仿射映射到草稿像素;两轴密度不同时笔刷半径取几何平均。密度为 1、矩形为整数的图层(以及完全透明的图层)的草稿就是画布本身,与此前逐位相同。 - 提交携带裁剪到不透明像素的栅格与其画布矩形(`WorkspacePaintRaster.rect`),不缩回画布分辨率。矩形从图层原矩形量起:未移动的边保留原坐标,扩展或裁剪都保持密度,整数边界取外包框,矩形与整数边界相同时不存储。提交时只读取图层原区域与笔触写过的区域,不复制整张画布。完全擦空的图层与此前相同,变为画布原点的 1×1 透明像素。 - 深度拆分前层保持栅格尺寸:按像素中心从提交像素取样,网格对齐时逐位复制。保留网格的绘画、重建网格的绘画与导入 CMO3 的绘画都按 `LayerSpace` 寻址图块(`RasterPaintCommit` 的切片重映射、`Cmo3ModelImport` 的绘画纹理坐标);连通块拆分在源图层自身的栅格中裁剪部件,部件保持原密度。 纹理命令(`application/WorkspaceTextureEdits`、`WorkspaceTextureOperations`):纹理工作区(纹理集页面与密度热力图、逐层尺寸面板、编辑画布的原始像素预览,见[操作速查](../guide/USER_GUIDE.md#纹理工作区))经 `WorkspaceTexturePort` 提交,与 MCP `layer_set_canvas_rect`、`layer_replace_image`、`layer_set_pixel_density`、`atlas_set_tile`、`atlas_set_budget`、`atlas_pack` 共用同一纯候选,均为后台任务与原子批量成员,查询为 `layer_get_texture`、`atlas_get`、`atlas_check_placement` 与 `atlas_render_page`(见 [MCP 接口](../agent/MCP_AUTHORING.md#纹理与纹理集))。分数矩形写入 umamo 的源图层清单(整数)时取整数边界;按清单解析纹理的网格迁移(`LayerTexture.of`)对分数矩形只是近似(整数矩形精确:图块宽高为栅格尺寸、清单为画布尺寸,比值即密度)。生成输入的补边(`RigGenerationSource.padded`,保留网格、新建网格与 `art_primitive` 部件的纹理覆盖)对高密度图层按原密度补透明,不降采样;`art_primitive`、`canvas_mesh_create` / `canvas_mesh_rebuild` 的 `canvas_uvs` 为画布单位,经 `LayerTexture.of` 映射到高分辨率图块。 ## 性能基线 `CommitPerfTool.baseline`(`PSD2LIVE_TOOLS=1`)在 tml 上搭建带自动骨架、两个摆动(前发、头饰)和一个已烘焙后发模拟的工程,逐阶段测量提交路径,输出 `build/tools/commit-perf/baseline.json` 与 `baseline.md`。数字取自一台 Windows 开发机的单次运行,只用于同机前后对比:不同机器、JIT 预热和并发负载下绝对值可差一倍以上,阶段之间的比例更可靠。阶段值为多次热调用的均值;提交值为经应用层命令边界的墙钟时间(含每次提交后写入历史存储)。原生 Cubism 预览重载需要原生运行时与 GL 上下文,未测;只测了重载前写出运行时文件(“写出”)。 | 场景 | 提交耗时 | 主要构成 | | --- | --- | --- | | 完整重建(重开、历史切换、绘画后的重建;骨架缓存命中) | 约 0.7–1.7 s | 纹理集打包约 0.5 s,其中 PNG 编码约 0.43 s;moc3 打包约 35–50 ms;分析约 45–65 ms;无骨架的基础 Rig 约 20–30 ms;骨架命中约 1–7 ms;重放(含摆动/模拟写回约 1–2 ms)约 3–8 ms;IR 编译约 1–4 ms;`validateBundle` 约 1 ms | | 骨架缓存未命中 | 另加约 5.5–7 s | 骨架烘焙 | | 几何提交(792 顶点网格) | 约 90–180 ms,中位约 150 ms | 快速路径重建约 55–70 ms(moc3 打包约 36–49 ms、重放约 5 ms、IR 约 3 ms);几何安全检查约 30–50 ms;历史存储写入约 34 ms;修订号哈希每次提交 4 次,共约 3–7 ms;桌面另写出运行时文件约 28 ms | | 小图层绘画(70×18 图层上 6×6 像素) | 约 1.4–2.2 s | 候选准备(`RasterPaintCommit.prepare`,自行打包纹理集)约 0.8 s;完整重建约 0.7 s(纹理集与 PNG 约 0.5 s);纹理集与 PNG 一次提交做两遍 | | 换图,同形状(90×158 图层整层换色) | 约 1.3–1.6 s | 与绘画相同:候选准备约 0.75–1.0 s,完整重建约 0.6–0.8 s | | 换图,镜像新形状并重建网格 | 约 1.8–2.4 s | 候选准备约 0.7–0.9 s,完整重建约 1.1–1.2 s | | 无关拓扑编辑(`canvas_topology` 细分右眼虹膜网格) | 约 80–100 ms | 提交走快速路径,不调用骨架;但下一次完整重建(重开、历史切换、绘画)骨架缓存未命中,约 6.2–8.2 s,之后同一文档命中约 1.2 s。原因是缓存键含手工拓扑网格集合与整个基础 Rig | 日志增长(每次追加一条 792 顶点网格的 `canvas_geometry`,条目保存完整顶点数组,约 17 KB): | 追加条数 | 日志条目 | 日志 JSON 字符 | 提交 | 运行时重建 | 重放 | 修订号哈希(单次) | 历史存储写入 | | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | | 1 | 15 | 0.16 M | 约 60 ms | 约 31 ms | 约 6.5 ms | 约 1 ms | 约 85 ms | | 50 | 64 | 1.0 M | 约 90 ms | 约 45 ms | 约 21 ms | 约 5.5 ms | 约 71 ms | | 200 | 214 | 3.6 M | 约 190 ms | 约 95 ms | 约 82 ms | 约 19 ms | 约 165 ms | - 重放、修订号哈希和存储写入都随日志长度线性增长(此为基线;重放检查点之后,追加提交的重放与日志长度无关,见[增量重放](#增量重放))。工作存储每个修订写一份完整快照:200 条时每次提交约增加 7 MB,整个场景序列后存储约 1.2 GB。 - 收窄骨架缓存键之后,无关拓扑编辑后的完整重建骨架命中(0 次重烘);`canvas_geometry` 改存量化增量后,上表中整网格平移的条目从约 17 KB 降到约 12 KB(200 条日志 3.6 M → 2.5 M 字符,重放约 82 → 66 ms),只移动部分顶点的笔刷编辑按移动顶点数缩小。 - 几何提交中 moc3 整模重编与几何安全检查原是主要部分。缓存旁车文本与动作列表、改写 moc3 写出并缩小几何检查范围后,同一场景热态提交约 37 ms(不含历史存储写入约 27 ms 与原生重载):运行时重建约 27 ms(其中 moc3 打包约 20 ms,主要是逐网格关键形降级、静止网格换到画布空间和约 7 MB 的写出;重放约 5–8 ms),几何检查约 4 ms,候选准备与网格规范化约 3 ms; - 绘画和换图由纹理集打包和 PNG 编码主导,且候选准备与重建各做一遍(此为基线)。 - 绘画改为在图层栅格空间提交之后(见上文“逐层尺寸”中的绘画),候选不再复制整张画布:工具改用与绘画会话相同的捕获方式(图层原区域与笔触区域),同一台机器上小图层绘画提交中位约 0.25 s → 约 0.17 s,其中候选准备约 56 ms → 约 1.5 ms,其余为提交内重建。 - 纹理集改为规范布局 + 页面配方缓存 + 条带预览 PNG、绘画候选不再打包之后(见上文“纹理集与绘画”),同一台机器在并发负载下前后对比(两次运行的绝对值都高于上表的空闲机数字):小图层绘画提交中位约 2.5 s → 约 0.35 s(稳定后约 0.3 s),其中候选准备约 1.5 s → 约 0.17 s、提交内重建约 1.2 s → 约 0.2 s;同形状换图约 2.1 s → 约 0.3 s;镜像换图并重建网格约 3.1 s → 约 0.45–0.8 s;带骨架的完整重建(骨架命中)约 1.0–1.6 s → 约 0.25–0.4 s。整页预览条带编码约 20 ms(规范 PNG 约 0.5 s,只在导出时生成);两遍分析合计约 0.2 s → 约 0.03–0.06 s。导出文件与改动前逐字节相同(`ExportGoldenTool`);预览包除纹理页 PNG 编码外不变。 **打开工程**:骨架烘焙缓存只在进程内,冷启动后首次打开带骨架的工程仍要烘焙约 5–7 s。保存时把头部的烘焙写入归档的可选头部缓存 `cache/head/`(格式见[工程格式](PROJECT_FORMAT.md#头部缓存)),打开时按 revision、程序版本、生成器常量与语言核对后种入 `SkeletonRig` 的缓存;重建仍走完整路径,由骨架缓存自身的内容键决定命中,结果与冷重建逐位相同。存储的烘焙只保留读取集内的网格和变形器,读取集外的对象只留 ID 与顺序,命中时取自新的基础 Rig。`OpenPerfTool.profile` 在上述场景(骨架 + 两个摆动 + 烘焙模拟)上保存三种归档:应用现在的写法(各修订的作者态 Rig 与头部缓存,打开时不生成也不重放,见[固化 Rig](MATERIALIZED_RIG.md#10-实现进度))、仅头部缓存(生成与重放,种入骨架烘焙)、两者皆无(生成、重放与骨架烘焙);每次打开前清空作者态 Rig 存储(`MaterializedRigStore.clear()`)与骨架缓存(`SkeletonRig.clearCache()`),各打开 3 次,并核对三者构建出相同的模型。下表是引入作者态 Rig 之前对后两种归档的测量(各打开 3 次,空骨架缓存): | 归档 | 解包与种入 | 头部重建 | 合计(即首次显示) | 骨架烘焙 | | --- | ---: | ---: | ---: | ---: | | 两者皆无(5.5 MB) | 约 0.35–0.4 s | 约 4.7–5.4 s | 约 5.5 s | 每次 1 | | 仅头部缓存(10.9 MB,烘焙条目 6.9 MB,压缩后 5.4 MB) | 约 0.37–0.47 s | 约 0.18–0.25 s | 约 0.7 s | 0 | 保存多约 0.5 s(序列化与压缩烘焙)。打开后的首次显示即完整重建的结果,未另外缓存运行包;带缓存时重建只剩约 0.2 s,先显示旧运行包再替换收益有限。 ## 拆分物化:画元记录 `art_primitive` 此前的拆分(`canvas_source_partition`、`canvas_depth_split`)把原图层留在源图中并软删除,重放时先逐位重新生成原网格(核对几何指纹与父级),再在记录位置把它的绑定复制到部件。这有三个问题:生成规则一变,旧记录就无法重放;纹理集仍打包原图层,CMO3 导出带着它的像素;“恢复全部”会把原图层带回来。现在拆分把原图层**物化**为部件:部件是唯一的绘制对象,原图层不再存在,只能撤销回拆分前。 **拆分即再生成**(`WorkspaceArtPrimitives.materialize`、`RigRegenerationCheckpoint.split`):拆分改变源图(原图层由部件取代),按[固化点](#固化点)的方式合并。G 为当前生成结果,G′ 为部件代替原图层后的生成结果;G 与当前作者态 M 中的原网格都由拆分自身(`SplitParts`:连通块/多边形为 `SourcePartitionJournal.splitParts`,前后分层为 `DepthSplit.splitParts`)切成同样的部件——各取自己那份原网格的静止位置、关键形、通道、混合形、路径、顶点组与 Glue,经同一组顶点来源——所以用户没动过的部件与生成的部件逐位相同,用户对原网格的修改成为部件上的修改,三方合并(`RigRegeneration.merge`)把它们带到 G′ 的部件上(父级变化时经父级空间换算,骨架插入关节行时经画布纹理坐标迁移)。拆分自己新建的 Glue(多边形切点跟随、前后分层的方向 Glue)只在 M 一侧,属于用户。拆分写入 `art_primitive` 记录,紧随其后写 `rig_checkpoint`(合并结果与 G′ 快照),记录本身从不重放;之后的构建从这个固化点开始,不需要基础 Rig。 - 原网格由生成器生成时,记录为版本 2 的**声明**:每个部件的静止画布网格(由 G 中原网格切出的部件经生成变形器在默认姿态下换到画布)、图层、分类与 `fixed_topology`(部件带用户 Glue、顶点组或路径时为真),`authored` 为空对象,`glues` 为空。基础生成据此生成部件(见[版本 2 记录](#版本-2-记录))。之后的脸部设置、站姿、骨架与分类变化经下一次合并同样到达部件。 - 原网格不是生成的(日志创建的网格,例如生成输入冻结后导入的图片)时,记录为版本 1,部件带着全部数据,其图层不进入基础生成,部件在合并中是用户对象。 - 没有版本 1 回退、两遍捕获或守卫:合并不会失败,迁移不干净的地方作为合并问题报告。原网格的拓扑被手工改过时,G 一侧切不出同样的部件,部件按用户拓扑保留(与合并中用户改过拓扑的规则相同,见[固化 Rig · 再生成合并](MATERIALIZED_RIG.md#5-再生成合并))。 **记录**(日志条目,版本 1——现在只为非生成的原网格写出,较早版本的拆分都写它;版本 2 见[下文](#版本-2-记录)): | 字段 | 内容 | | --- | --- | | `op`、`v`、`origin` | `art_primitive`、`1`、`split`(多边形/连通块)或 `depth`(前后分层) | | `texture_source_id` | 部件贴图所在的源(生成模型为 `art-0`) | | `supersedes`、`supersedes_layers` | 在此位置删除的网格 ID 与其源图层 ID | | `replace` | 被取代网格 → 部件网格,用于部件树中的位置;未给 `masks` 时也用于其他网格的遮罩 | | `masks` | 可选,被取代网格作为遮罩时改由哪些部件承担(前后分层只用后层) | | `primitives` | 每个部件一项:`id`、`layer_id`、`source_id`、`source_bounds`(贴图须覆盖的画布矩形)、`neutral_bounds`、名称、父级、部件、混合模式、静态材质与显隐、遮罩、`user_data`;父级空间顶点 `positions`、`triangles`、画布单位纹理坐标 `canvas_uvs`;所用参数定义、几何关键形 `geometry`、通道关键形 `channels`、混合形 `blends`(含限制曲线)、路径 `paths`、顶点组 `vertex_groups` | | `glues` | `replaced`:按模型顺序,每个接触被取代网格的 Glue 对应的替换 Glue;`appended`:追加在最后的 Glue(连通块拆分的切点跟随、前后分层的方向 Glue) | | `depth` | 仅前后分层:`back`、`front` 图层 ID 与 `glue_id`;`DepthSplit.isFrontLayer` 据此让前层绘画保留拓扑 | **重放**(`ArtPrimitiveReplay.replay`,旧版重放,只在记录之后没有固化点的旧日志中进行,见[旧版重放](#旧版重放)):缺失的参数按记录创建;删除 `supersedes` 中的网格,部件在原网格的列表与部件树位置就位(原网格已不存在时加入记录的部件或根);其他网格的遮罩按 `masks`/`replace` 改写;接触被取代网格的 Glue 依次换成 `replaced` 中的对应项(数量与当前不符时,删除全部接触 Glue 后按顺序追加替换项),再追加 `appended`;被取代网格的路径与顶点组删除,部件的路径与顶点组按记录写入。重放不读取被取代网格的几何,也不核对其指纹。纹理坐标存画布单位,重放时经当前纹理集换算。 **拆分时捕获的状态**:`RigEditOverlay.authored` 给出旧格式编辑与整条日志之后、摆动和模拟写回之前的模型;部件取自该模型,因此摆动与模拟生成的轴不写入记录。连通块/多边形部件的顶点、关键形、混合形、路径、顶点组与 Glue 按原分区算法迁移;模拟目标、烘焙偏移与 Glue 角色转到部件(前后分层转到后层),之后由模拟照常写回。 **生成输入**:拆分冻结生成输入(`generationSource`),原图层只留在其中。生成时: - 被取代的生成图层仍参与基础 Rig 生成,排在其替换图层在当前源图中的位置(取替换图层的最高 `order`),因此其他图层的绘制顺序、变形器框架和 ID 与拆分前一致,记录之前的日志照常重放; - 部件所在图层由记录拥有,不进入基础生成;它们的当前像素按 `source_bounds` 补透明覆盖后打包; - 被取代图层在纹理集中只占 1×1 透明占位,供记录之前的日志条目寻址;记录重放后,`BuiltRig.withRigEdits` 从 Rig 的纹理集图块和美术清单中删除它,预览分析也不再列出它,导出的 CMO3 因而没有该图层; - 嘴部被分层时,其生成的嘴唇保留;分层的若是嘴唇本身,则该嘴唇被取代。 **部件之后的编辑**:部件是普通源图层,可绘画、删除/恢复、再拆分(被拆分的部件同样被取代)。修改部件的网格设置时由 `MaterializedMeshRebuild` 追加 `canvas_mesh_rebuild`,迁移关键形与绑定。部件不再随生成规则自动重新生成。 **骨架蒙皮**(版本 1 部件,旧版重放):骨架在日志之前烘焙基础 Rig,而部件要到记录重放才出现,单看基础 Rig 时骨骼绑定的部件并不存在(左右合画后拆分的腿、鞋因此不会被蒙皮,腿骨只剩没有关键形的参数)。所以骨架阶段(`RigBuilder`)先从记录解码骨骼绑定的部件(`ArtPrimitiveReplay.skinnable`:画布单位纹理坐标、不带图块与遮罩),与基础 Rig 一起烘焙,再把蒙皮后的部件及烘焙给它们加的 Glue 从基础 Rig 中取出,存入 `BuiltRig.primitiveSkins`。基础 Rig 仍只含被取代的原网格,记录之前的日志照常重放;记录重放时用蒙皮结果代替自行解码(父级为骨骼变形器,纹理坐标经当前纹理集换算,遮罩取自记录),两端都已就位的 Glue 随后加入。以下部件仍由记录自行解码:父级或关键形参数要由更早的日志条目创建、带路径或顶点组(烘焙不会把它们迁移到新增顶点上)。记录的父级不再由基础生成产生时,解码按[父级不再生成](#父级不再生成)改挂。重放检查点以 `primitiveSkins` 的实例为键的一部分;`RigEditOverlay.applyTo` / `authored` 与模拟烘焙都要传入它(`authored(BuiltRig)`)。 **拥有关系**:部件属于编辑日志节点(`journal` 拥有的 `rig:authored`),不在生成器依赖图中单列;摆动与模拟照常读取它们。被取代网格上原有的 `generated_override` 失去目标后按孤立报告。 **引用被取代的 ID**:文档操作中引用字段(`layer_id`、`target`、`source_id`、`middle_ids`、`meshes` 等)指向被取代的图层或网格时,返回错误并列出取代它的当前 ID(跨多次拆分逐级展开);`source_get_components` 同样。`layer_restore` 不能带回原图层(它不在 `deletedLayerIds` 中)。 **兼容**:旧的 `canvas_source_partition`、`canvas_depth_split` 按原规则重放,第一次编辑时被固化(见[旧版重放](#旧版重放));`LegacySplitReplayTest` 用旧版本保存的工程核对各历史节点重放后的 IR 哈希与旧版本一致,旧工程中的软删除原图层仍可恢复,旧工程上的新拆分按物化规则进行。导入 CMO3 模型的拆分仍使用旧规则(原图层软删除)。 ### 父级不再生成 日志在某条记录之前创建的变形器,重放到该记录时都会再次存在;因此记录的父级若在重放位置缺失,只可能是基础生成不再产生它:拆分之后改了生成设置(启用骨架后不再生成手臂下垂 Warp `DeformArmHang_L/R`,由骨骼接管手臂)。此时记录按下述规则重放,存储的记录不改写;规则由 `VanishedParent` 与各记录的重放实现,结果确定: - **版本 1 部件**(`ArtPrimitiveReplay.rehome`,旧版重放):部件改挂到它所替换的网格(`replace` 中的键)当前所在的父级;该网格本身是更早记录的部件、而骨架烘焙解码时基础 Rig 中还没有它(`skinnable` 的 `earlier`)时,沿那条记录继续向前找它替换的网格。位置按“记录顶点 → 画布纹理坐标”的最小二乘仿射拟合(规则静止网格或刚性框架下精确)还原为默认姿态下的画布位置,再求逆到新父级空间;关键形与混合形的位移经拟合的线性部分换算为画布位移后同样换入。找不到被替换的网格时照旧报错 `Art primitive parent is missing`。 - **`canvas_mesh_rebuild`**(`RasterMeshJournal.replay`):网格已在另一个父级空间,几何指纹无法再核对,改为只核对顶点数(`glue_map` 长度);记录的 `points` 与 `previous_parent_points` 按同样的仿射拟合换到网格当前父级的空间。 - **结构编辑 `bind`(网格)与 `move`(变形器)**(`RigStructureEdits.replay`):目标父级缺失时该项不生效,对象留在原处。新提交的编辑走 `RigStructureEdits.apply`,缺失父级仍报错。 - **`canvas_mesh_create`** 没有被替换的网格可循,父级缺失时照旧报错。 骨骼绑定、但由日志挂到非骨骼变形器下的网格不在烘焙中改挂,而在日志之后蒙皮,见[日志安放的网格](#日志安放的网格)。 ### 日志安放的网格 骨架烘焙在日志之前把骨骼绑定的网格改挂到骨骼的旋转变形器下,位置与关键形都换到骨骼空间。日志若之后自行安放这个网格——结构编辑 `bind` 到不是骨骼的变形器、`warp` 记录把它包进新 Warp——这些记录保留网格的局部坐标(`space=local`、`fit_local`),在骨骼空间的网格上无法正确重放。这类网格由 `SkeletonCanvasSkin.placedByRecords` 从骨架定义与编辑(先旧版 `structureEdits` / `warpEdits`,再按序读日志;网格最后一次 `bind` 到骨骼变形器则不算)确定,按以下规则处理。日志有固化点时,蒙皮阶段另外包括固化点作者态 Rig 中不受其肢体任何骨骼带动的绑定网格(`SkeletonCanvasSkin.placed` / `unskinned`,固化点之后 `bind` 到骨骼变形器的除外),无论它是由记录、再生成合并还是旧版本放到那里的;烘焙只按记录决定,见[固化 Rig · 结构接替与合并忠实性](MATERIALIZED_RIG.md#11-结构接替与合并忠实性)。 - **烘焙**(`SkeletonRig.apply` 的 `canvasSkinned`):与肢体其他网格一样细分关节、与同一肢体的部件焊接(`GlueSkel__` Glue),并计入混合形规划;但不改挂、不写蒙皮关键形,留在生成的父级下。它若改挂本应挂在的骨骼照常保留变形器(不因无网格而剪除或折叠)。因为它在画布上随整条骨骼链运动,规划把链上所有骨骼参数视为相互耦合(不作混合形)。其余网格的路径不变:没有日志安放网格的工程,烘焙与导出逐字节不变(`ExportGoldenTool`)。 - **蒙皮**(生成节点 `skeleton.skin`,日志之后、摆动与模拟之前):骨骼的作用定义在画布空间。某一姿态下,每个顶点按其权重(`SkeletonWeights` / `SkeletonManualWeights`)由骨骼的画布变换(`SkeletonPoseSolver.frames`:有变形器的骨骼读取变形器,其余按父骨骼转动)带动,再与烘焙相同地经关节模板、ARAP 与表面平顺修正(`SkeletonRig.corrected`)。得到的画布位置经网格最终父级链在同一姿态下的逆映射换入父级局部空间,与静止时之差写为网格在骨骼参数上的关键形;混合形类参数(规划认定独立的骨骼、骨架姿态)写为混合形。轴为移动该网格的肢体骨骼、其上方的身体半段与骨架姿态参数,按烘焙相同的容差取最少的键。网格不改挂,日志的安放保持不变,无论其上方有几层用户 Warp。 - **已有关键形**:蒙皮关键形与网格原有关键形相加(共享参数时合并两边的键)。对这些单元的画布编辑由 `GeneratedOverrides.capture` 写为 `skeleton.skin` 的 `generated_override`,重放时三方合并;日志已经给网格的同参数混合形视为用户数据,保持不变。 - **Glue**:与另一蒙皮网格成对的顶点在每个键上取相同的画布目标。另一端由烘焙蒙皮(挂在骨骼下)时取 Rig 求值的位置;两端都由本阶段蒙皮时按 Glue 两侧权重取会合点,并让两者在共享参数上使用相同的键。这些顶点在 ARAP 中作为固定点,网格平滑地过渡到接缝。 - **拓扑**:关节细分发生在日志之前,之后的逐顶点记录针对细分后的网格。若日志中针对该网格的逐顶点记录(`canvas_geometry`、`vertex_group_put`、网格的 `generated_override`)的顶点数等于细分前的顶点数(在不细分该网格的版本中写下),该网格不细分(`SkeletonCanvasSkin.lockedTopology`,与手工拓扑相同),以免这些记录失配。其他由关节骨骼绑定的网格,若这类记录全部按细分前的顶点数写下(骨架之前写的顶点组等),同样不细分;有按细分后顶点数写下的记录时照常细分。 - **代价与缓存**:肢体根骨骼与上方身体半段只把整条肢体刚性带动,求解时先不含它们、再套用其变换,所需求解数只取决于肢体内部关节的键。输出经 `GeneratorReuse` 按读取内容复用;骨架缓存键在存在这类网格时包含其集合。 ### 版本 2 记录 版本 1 把原图层拆分时刻的作者态整体快照进部件:基础生成仍只看见原图层(乘客),看不见部件,生成器写进网格的关键形(眼睛开合、骨架蒙皮等)也冻结在记录里,之后的脸部设置、站姿或骨架变化到不了部件。版本 2 让部件参与基础生成。生成的原网格的拆分都写版本 2,没有开关。现在的版本 2 记录只是声明(`authored` 为空,用户数据在其后的固化点中,见[拆分即再生成](#拆分物化画元记录-art_primitive));较早版本写下的版本 2 记录在 `authored` 中带作者数据,下文的残差、两遍捕获、乘客容错与重放描述的是它们,只在[旧版重放](#旧版重放)中使用。 **已解析图层集**(`core/PrimitiveResolution`):生成图层去掉被 v2 记录取代的图层,加上 v2 部件图层。锚点、脸部 Rig、头部空间、框架、站姿与腿、身体框架、变形器(含成对 Warp、手臂下垂 Warp)、眼白、作为遮罩来源的网格、嘴唇的脸部 Rig 与骨架绑定都只读这个集合;预览分析与之一致。部件像素取当前像素按 `source_bounds` 补透明,网格仍取记录值;分类取 `layerOverrides`(拆分时继承),没有时取记录的 `classification`。带拆分基线(`splitBaselineLayerIds`)的上下文同样计入部件、排除被取代图层;保留变形器的构建(`buildPreservingDeformers`,绘画提交用)也一样。 **乘客**:被取代的图层仍在原位置生成(标签计数决定的 ID 与默认绘制顺序不变),只用自身冻结像素与逐层关键形,读取但不反哺上述聚合阶段,不绑定骨骼;它只为记录之前的日志条目寻址而存在,记录重放时删除。 **停放**:部件在基础 Rig 中按记录的静止画布网格生成(不重新网格化,纹理坐标为画布单位),得到生成关键形、通道、遮罩、部件树位置与骨架蒙皮;UV 绑定后整体移入 `BuiltRig.primitiveSkins`(连同焊接 Glue、生成路径、指向部件的生成遮罩、部件槽、拥有关系、延迟父级与中性边界)。基础 Rig 中指向部件的引用改写为其乘客,因此记录之前看到的是“拆分前”的 Rig。`BuiltRig.resolvedPuppet()` 去掉乘客并放入部件,生成迁移(`RigGenerationMigration.generatedRig`)的 previous / desired 用它比较,脸部设置、分类等变化因而同样到达部件。 **记录字段**(`v: 2`,其余顶层字段同版本 1):每个部件有 `id`、`layer_id`、`source_id`、`source_bounds`、`neutral_bounds`、`name`、`classification`、`mesh{canvas_positions, triangles, canvas_uvs}`(静止画布网格:部件仍在原件的生成父级下时经基础 Rig 的生成变形器换算,日志对父级链的编辑重放时才作用一次;日志改过父级时经作者态父级);`parent`、`part` 为空表示取生成结果,只在日志编辑改过原图层时写入;材质字段只在与生成值不同时写;`masks` 为空表示取生成遮罩;`parameters` 只含用户轴;`geometry_residual`、`channels_residual`;`blends`、`paths`、`vertex_groups` 只含用户数据(不含骨架姿态混合形与生成路径);`fixed_topology`(部件带逐顶点用户数据时为真,骨架不得插入关节行);`frozen_axes`。`glues` 只含用户 Glue(不含 `GlueSkel__` 焊接)。 **残差**:捕获时 A 为原图层作者态,G 为基础生成输出(含骨架),残差 A − G 经分区的顶点来源移到部件顶点上。键中有生成器轴不在默认值的单元写成紧随记录的 `generated_override`(`base` 为候选基础部件的单元,`points` 为 `base` 加残差,`generator` 为拥有该轴的节点),其余写入 `geometry_residual` / `channels_residual`。重放时部件网格 = 停放的生成网格 ⊕ 残差,覆盖照常在全部生成器之后三方合并。基础不为其生成关键形的部件(零网格:唇带的切片、延迟父级的部件)没有生成运动可加回,残差取作者态全部关键形(A − 0)。同一记录的嘴部部件按全部嘴部部件合并的开口生成嘴型,而不是各自的边界,否则半张嘴会各自当作整张嘴张开、在切口处撕开。 **重放**:删除乘客,放置停放的部件(画布纹理坐标经当前纹理集换算,延迟父级换到其父级空间),恢复生成遮罩与部件槽,两端都在时加入焊接,写入生成路径,再加残差与作者层。停放拓扑与记录不同(骨架加入关节行、之后的网格重建)时,逐顶点数据按画布纹理坐标迁移到新网格。 **两遍捕获与守卫**(较早版本):拆分先写基础字段,再由候选基础生成部件、对照它写残差与覆盖;只有候选基础仍生成既有日志引用的全部对象且整条日志能在其上重放时才写版本 2,否则回退版本 1 并带 `v2_fallback {reason, detail}`。现在的拆分以合并代替,没有回退;旧记录上的 `v2_fallback` 只作历史。 **乘客容错**:记录之前的条目若只因其引用的网格全是后续 v2 记录的乘客而失败,按空操作重放并记一条说明(`SupersededEntryNote`:日志序号、`op`、目标、原因);其他失败照常报错。说明随重放检查点保存,从检查点续放也完整,并经 `BuiltRig.supersededEntryNotes` 进入 `workspace_inspect` 的 `quality.overrides`(`SUPERSEDED_ENTRY_SKIPPED`,info,不阻断)。 **再拆分**:部件可再拆分,与拆分普通图层相同:G 与 M 中的部件各自被切开再合并。较早的版本 1 记录取代 v2 部件时,该部件以透明占位和固定网格留在已解析集合中,基础框架不变。 **部件之后的编辑**:绘画只换像素,部件网格保持记录值;`rebuild_mesh` 绘画或逐层网格设置修改在记录之后追加 `canvas_mesh_rebuild`,迁移关键形与绑定。 **拥有关系与缓存**:`DocumentGenerators` 的文档输入 `document:primitives`(v2 记录的固定输入)被足迹、框架、网格与骨架读取;部件的 `keyform:mesh:<部件>@<参数>` 按停放的拥有关系归 `mesh:<图层>` / `rig.meshes`(特征轴)或 `skeleton`(骨骼轴),`GeneratedOverrides.capture` 据此把之后对这些单元的编辑写成覆盖。阶段缓存与绑定缓存的键包含 v2 记录的固定输入摘要。 **兼容**:较早版本写下的版本 1 与带作者数据的版本 2 记录在其后没有固化点时照旧重放(含骨架蒙皮与自行解码),读取时不改写,第一次编辑时被固化(见[旧版重放](#旧版重放));没有 v2 记录的文档走原代码路径(`ExportGoldenTool` 逐字节一致);v1 与 v2 记录可以混用,v2 记录取代 v1 部件时将其删除。声明式记录被重放时(例如在没有固化点的构建中)按生成的部件放置。旧版本的显式升级命令(`source_upgrade_split_records`)已移除。 **已知限制**: - 被拆分嘴部的生成嘴唇仍取自被取代的嘴部(乘客的冻结像素),而不是部件当前像素的合成。拆分时二者一致;之后在部件上绘画,嘴唇纹理不跟随(普通嘴部的嘴唇纹理同样取自网格输入,重建网格前也不跟随),重建部件网格也不会重新生成嘴唇。 - `frozen_axes`:写入端总是写空,因此当前不会出现。基础生成读到非空值时,若生成网格的轴里有被冻结的轴,就把该部件的整张生成网格归零、只保留作者数据;旧版重放遇到非空值直接报错(不支持)。 - 部件网格重建后,针对其生成单元的 `generated_override` 不迁移,形状不符时按孤立报告。 - 嘴型轮廓开启时,嘴部部件的固定网格是原件的轮廓网格,足迹与原件的栅格网格略有不同:由嘴部框定的生成嘴唇静止位置相差约 0.4 px;部件本身的开口形状不含轮廓贡献(约 0.13 px)。 - v2 部件参与框架生成:固定网格的画布位置经父级往返有浮点舍入,未拆分网格的局部坐标与框架控制点可能相差几个 ulp(世界坐标 < 1e-4 px);版本 1 逐位不变。 - 原网格的拓扑被手工改过时,部件按用户拓扑保留为用户对象,生成器之后的变化不再到达这些部件的几何。 **测试**:`PrimitiveResolutionTest`(手写 v2 记录的基础生成)、`ArtPrimitiveRecordV2Test`(旧版记录的编码、残差、重放、乘客容错与检查点续放的说明;编码器在测试中的 `LegacyArtPrimitiveV2`)、`SplitPartsCheckpointTest`(拆分的固化点、成对 Warp 与骨架蒙皮);在 tml 上的拆分效果用 `ArtPrimitiveV2VisualTool` 目视检查(未拆分与拆分在各姿态的差异,与此前的版本 2 记录相同)。 ## 旧版重放 较早版本把一些编辑写成要在基础 Rig 上重放的记录:拆分的 `art_primitive`(版本 1,以及带作者数据的版本 2)、生成迁移(`rig_generation_transition`、`rig_generation_frames`、`rig_generation_scaffold`、`rig_mesh_activation`)、旧的 `canvas_source_partition` / `canvas_depth_split`,以及日志之前的旧格式静态字段。生成的模型不再写它们;导入 CMO3 的模型没有生成结果可合并,仍写迁移记录与旧分区,但写下它们的提交随即在其后写入导入模型的固化点(`WorkspacePreviewBuilder.importedRecordsCheckpointed`),记录只在这次构建中重放,之后的构建、打开与撤销都从固化点开始。因此 `core.legacy` 只在两处运行:没有固化过的旧日志第一次被编辑之前,以及导入模型写下这类记录的那一次提交。 - **只固化一次**:日志在最后一个固化点之后仍有这类记录(没有固化点时也包括旧格式静态字段,`RigEditOverlay.replaysLegacy`)时,延续它的第一次编辑先在原日志末尾、新条目之前写入当前作者态 Rig 的固化点(`WorkspacePreviewBuilder`),生成快照在手时一并保存;这些记录就此重放最后一次,之后的构建都从固化点开始。打开、切换历史和保存不改写日志,旧修订照旧按记录构建。 - **代码**:只在这条路径与导入模型上使用的部分在 `core.legacy`:`ArtPrimitiveReplay`(版本 1 与带作者数据的版本 2 记录的重放、骨架烘焙前解码版本 1 部件、再生成合并时放入版本 1 部件)、`StubTolerance` 与 `SupersededEntryNote`(乘客容错)、`VanishedParent`,以及生成迁移的写入与重放(`RigGenerationMigration`、`RigGenerationJournal`、`RigGenerationFrames`、`RigGenerationScaffold`、`RigMeshActivation`、`RigGenerationResidual`、`RigGenerationTextures`)。`ArtPrimitiveJournal` 保留记录格式与文档查询(取代关系、拥有的图层、贴图覆盖)。判断编辑是否改变生成结果的 `RigGenerationChange` 是合并路径的触发条件,在 `core`;合并(`RigRegenerationCheckpoint`)仍从 `core.legacy` 读取旧迁移记录给变形器起的名字与版本 1 部件,使旧日志的 G 与 G′ 按同样方式放置——这些是兼容读取,不随新功能扩展(`LegacyReplayBoundaryTest` 固定可引用它的文件)。 - **锁定**:`LegacySplitReplayTest` 用旧版本保存的工程核对各历史节点按记录构建的 IR 哈希。 ## 后续 - 分类修改的剩余耗时在分析:生成的模型改为合并后不再走生成迁移,剩下导入 CMO3 模型的迁移与拆分基线对新栅格重复运行 `CharacterAnalyzer.analyze`(逐层分类约 0.5 s/次);可按图层元数据与栅格摘要缓存分类结果。 - 以混合形写回的模拟关键形的覆盖;覆盖报告并入统一质量检验框架后改用其规则注册表与栅栏。 - 通道覆盖的已知限制(目前没有生成器在日志之后键入通道,这条路径不会触发;第一个这样的生成器加入前处理): - 关键形 `set` 写在生成通道轨道的静止格点(键中生成器的参数在默认值)时,`journalOnly` 只为几何编辑剥离生成器在默认值处的轴,通道编辑的键仍带这根轴,日志重放时会在用户自己的轨道上建出这根轴。 - 加入通道覆盖之前的版本把每条覆盖当作顶点或控制点坐标读取,要求数值个数为偶数:标量(1 个)与颜色(3 个)覆盖都会报“Invalid override form”,含通道覆盖的工程在旧版本中重放失败。 - 打开工程时直接读取固化结果:作者态 Rig 的保存与读取、固化点、再生成合并与试运行均已实现([固化 Rig · 实现进度](MATERIALIZED_RIG.md#10-实现进度));导入 CMO3 的模型在导入基线上从固化点重放,不单独由作者态 Rig 构建。 ## 内部造型生成器 可选 `interiorEffect:` 节点在作者日志后、生成结果覆盖前运行。`NativeInteriorEffect` 在 `rigEdits.interiorEffects` 中保存配方、紧凑原生 blend 字段和物理组。现有输入参数只读,生成参数和目标 Mesh 轴有单一所有者;配方修改、增删与开关均参与失效传播。网格、素材或绑定不匹配时保留配方但跳过过期顶点。原生 UI/MCP 共用 `interior_put`,临时试动不进入文档。停用和失效效果的闲置自有参数仅在导出副本中清理。未发布的密集实验不保留兼容;删除通过 `interior_delete` 进入同一重放链路。参见[内部造型](INTERIOR_SURFACE.md)。