背景
本路线指「3D 建模 → 绑骨 → 套动作 → 渲染出 2D 序列帧」(下文简称渲染路线 ,与既有的逐帧路线 并列)。该路线已跑通并产出一套完整角色资源,但这套产物在当前后端契约里没有落点 。
当前基线(#64 / #70 已合并):
Character
├── reference_image_url
└── character_data (JSONB)
└── outfits[] {id, name, description, preview_url}
└── actions[] {id, type, name, loop, fps, frame_count}
└── frames[] {index, image_url, duration_ms}
这套结构围绕逐帧路线建立,渲染路线的产物有三类数据无处可放:
引擎播放必需的逐帧几何量 (地面锚、位移轨、时长、图集)——缺了引擎播得出来但播不对;
3D 源资产 (模型、骨架规范、武器挂点、渲染参数)——缺了每补一个动作都要把前四段重做一遍;
产物来源 (任务生成 vs 外部导入)——存量资产由人工产出,且部分 provider 尚未接入,契约必须能表达「该资产不由某个 task 产出」。
本 Issue 只做加法 :新增字段一律可选,新增枚举一律追加成员,既有字段语义与既有调用方均不受影响。
本路线的真实形态
全链路可编程,无人工步骤。除第 1 段外,3D 相关各段均由腾讯云混元生3D
(ai3d.tencentcloudapi.com · Version 2025-05-13)提供接口:
段
实现
状态
1 母版 T-pose 图
图像模型 i2i(有 prompt 门禁)
已跑通
2 图生 3D
SubmitHunyuanTo3DProJob / QueryHunyuanTo3DProJob
接口可用,适配器待接入 ;存量产物按外部导入处理
3 减面 + 导出
无头 Blender(本地)
已跑通。云端接口有 ≤60MB 上限,高模必须本地先减面
4 自动绑骨蒙皮
SubmitAutoRiggingJob / DescribeAutoRiggingJob
已实测跑通,全自动
5 动作片段
同上接口的 MotionType 参数(48 个预设动作)
已实测跑通,全自动
6 3D→2D 截帧
three.js + 无头浏览器,确定性取样(本地)
已跑通
7 剪影门禁 + 图集打包
Python(本地)
已跑通
云端同族接口另有 SubmitHunyuanTo3DMotionJob(文生动作)、SubmitReduceFaceJob(智能拓扑)、
SubmitHunyuanTo3DUVJob(UV 展开)、Convert3DFormat(格式转换)可选用。
第 4/5 段于 2026-08-03 实测跑通 :5 次调用全部成功,单次 40–60 秒;异步提交 + 轮询
(JobId 有效期 24h,Status 取 RUN / DONE / FAIL)。原先依赖第三方网页版人工操作的断点
就此消除。实测结论见本 Issue 评论区,其中四点直接影响本契约的字段设计
(骨架命名取值、位移数据来源、clip_ref 需带区间、输入端硬约束)。
第 2 段的 provider 适配器本期仍留桩(NotImplementedError),契约按已接入的形态设计。
输入端硬约束(写进契约的依据)
绑骨接口对输入有三条约束,违反不会报错,而是产出错误结果 :
格式 FBX / GLB,≤60MB 。生产模型实测 87MB,必须先本地减面 → model_3d.triangles 作为门禁值的依据。
须 A-Pose 或 T-Pose。
不得包含人体以外的组件(武器、配件) 。实测送入带武器的模型后,武器被错误绑权重、动画中乱甩。
我们的武器走刚体挂件(绑完骨挂到手骨、不进网格)天然合规 —— 这是 sockets 必须入契约、
而不能把武器烘进网格的直接依据。
实测同时验证了 model_3d.skeleton_convention 与 sockets 的必要性 :更换绑骨方案确实改变了骨架命名
(无命名空间前缀、骨数不同),靠这两个字段即可定位受影响范围——本次实测据此判定挂点参数可复用、无需重标定。
目标
让渲染路线与逐帧路线的产物共用同一套播放契约 ,且消费端按契约播放即正确:不脚陷地、不打滑、不二段跳。
一、统一单位口径(先定这个,否则字段无法自洽)
所有长度、位移、速度一律以「角色总高 = 1.0」为单位,不使用像素。
角度单位:度(deg),欧拉角按 XYZ 顺序。
二、提议字段
A. 动作层(character_data.outfits[].actions[])——两条路线通用
这一组不是渲染路线专属,逐帧路线同样需要,因此加在通用层,而非 3D 子对象里。
字段
类型
语义与必要性
anchor_y
float
地面锚点,相对帧高的比例 [0,1] 。绘制时 y = 地面线 − anchor_y × 当前帧高。各动作最深脚点不同,是渲染产物的几何事实,无法从 frames[] 推出。实测:五个动作统一取 idle 的锚,走路时脚陷地约 2.5% 帧高。归一化后满分辨率与图集共用一个值 。
root_motion
list[[float, float]] | None
逐帧位移 (dx, dy),相对首帧,y 向上为正,单位为角色总高。长度 == 帧数。承接 #63 的论证:位移烘进像素则消费端无法控制悬空时长,抽成轨道后可由物理驱动。
move_speed_h_per_s
float | None
建议施加的移动速度 (身高/秒)。与 root_motion 是语义差异而非数值差异:前者是产品建议值,后者是片段实测值,二者允许不同——跳跃片段自带约 2.66 身高的前冲,但产品选择播放时不额外推进,否则叠加成两倍。实测 walk 1.137、run 3.023。
duration_s
float
一轮精确时长。既有 fps 是导出量而非权威:idle 只取真循环周期 2.05s / 16 帧(等效 7.8fps),整段 10 秒采 16 帧会退化成幻灯片。既有 fps 不动 ,本字段为权威口径,冲突时以本字段为准。
atlas
{file, cols, rows, scale} | None
图集产物。scale 相对 render_profile.frame_size,单格尺寸由二者推导,不重复存 。只给散帧 URL 会让每个接入方各自重排一遍。
derivation
"image_frames" | "render_3d"
该动作由哪条路线派生。放在动作层而非角色层 :同一角色的不同动作会走不同路线(连续位移动作走渲染路线,需要单帧可编辑的离散姿势走逐帧路线),角色层只提供默认值。枚举值需与 #53 的分流命名保持一致。
clip_ref
str | None
来源动作片段 ID,用于溯源与素材授权追踪。
主动交出的可删候选 :move_speed_h_per_s 数值上可由 root_motion 与 duration_s 推导,保留它的唯一理由是上表所述语义差异。若评审认为「建议值应由消费端决定」,可删,代价是每个接入方各自实现一遍跳跃前冲的取舍。
B. 角色层(character_data)——渲染路线特有
model_3d: {
url str 模型文件地址
format "glb" | "fbx"
triangles int 三角面数
has_skin bool 是否已绑骨
skeleton_convention str 骨架命名规范,枚举收敛:mixamorig / plain_humanoid
normalized {height: 1.0, feet_y: 0.0, centered: true}
source "generated" | "imported"
}
render_profile: {
view "side" | "three_quarter"
faces "right" 帧朝向,见口径三
material str 渲染材质模式,如 "cel"
frame_size [int, int] 满分辨率帧格,所有动作共用
}
sockets: {
<挂点名>: {
bone str 挂载骨骼名,如 "mixamorig:RightHand"
position [f, f, f] 骨骼局部空间坐标,单位=角色总高
offset_along_axis float 挂件沿自身 +Y 的补偿量
rotation_euler_deg [f, f, f] XYZ 顺序,单位度
}
}
必要性逐项:
model_3d :本路线的一切从它派生——补动作、换武器、改渲染都要回到模型;不存则每次重跑前四段(每段都是分钟级异步任务,且第 2 段按次计费)。triangles 单列是因为它是门禁值 :绑骨服务对输入有面数与文件体积约束(实测 ≤60MB),高模必须先本地减面才能提交。skeleton_convention 决定挂点与动作复用如何按骨名寻址,也是更换绑骨方案时的分叉点——实测已发生过一次 :更换方案后骨名去掉了命名空间前缀、骨数由 49 变 28,靠该字段即可判定挂点参数可直接复用。建议取值按枚举收敛(如 mixamorig / plain_humanoid),而非自由字符串。normalized 是所有位移数值的基准,不写死则单位口径失去锚。
render_profile :补新动作时必须复用同一套参数,否则新旧动作构图对不上。现有实现特意一次性烤完所有片段来保证共用构图,这个隐式约束必须落到数据上才可复现。
sockets :武器走刚体挂件不蒙皮,因此一套骨 + 一套动作可组合出 N 把武器 × M 种配件,换武器无需重绑骨、重出动作 ——这是渲染路线相对逐帧路线的主要杠杆。position 与 offset_along_axis 分开的理由:前者修正「手骨原点在腕、挂件应在掌心」,后者修正「挂件原点在握把顶端、直接挂等于握在最上端」,两个偏移来源不同,合并成一个数就无法在换武器时只改其一。
C. 跨端口径(落 README / MODULES.md,不留聊天记录)
单位 :见第一节。
帧朝向 faces = "right" ,朝左由消费端水平翻转;但不得用镜像帧代替更换渲染视角 ——角色左右不对称(如单侧腕带),镜像会把不对称细节翻到另一侧。
跳跃的竖直运动已烘在帧里 ,消费端不得再叠加重力,否则跳两次。抽离位移时只剥水平分量,竖直分量属姿态不属位移。
D. MediaCategory 新增
追加 model-3d、sprite-atlas。该枚举注释已写明「放在 common 而非 media 模块,新增文件用途时无需修改 media 代码」,此处按既定方式扩展。
E. 生成任务
GenerationType 追加 character_model_3d:3D 生成是独立产物、独立耗时量级、独立失败模式,与出帧不是同一件事。provider 适配器本期留桩 ,契约先立。
CharacterActionInput 不拆成两个类型 ,追加判别字段 derivation 与各自的可选参数块(model_3d_ref / clip_ref 对渲染路线;既有 reference_image_urls / num_frames 对逐帧路线)。理由:对调用方而言「生成一个动作」仍是同一件事,拆成两个入口会把路线选择泄漏到 API 表面。
资产统一带 source: "generated" | "imported":存量资产与用户自带模型均非 task 产出,契约必须能表达这一点。
三、改后完整结构与示例
Character
├── reference_image_url
└── character_data (JSONB)
├── version: 2 ← 1 → 2
├── model_3d {...} ← 新增(渲染路线)
├── render_profile {...} ← 新增(渲染路线)
├── sockets {...} ← 新增(渲染路线)
└── outfits[] {id, name, description, preview_url}
└── actions[] {id, type, name, loop, fps, frame_count,
anchor_y, ← 新增
duration_s, ← 新增
move_speed_h_per_s, ← 新增
root_motion, ← 新增
atlas, ← 新增
derivation, ← 新增
clip_ref} ← 新增
├── frames[] {index, image_url, duration_ms} ← 不变(单层时仍走这里)
└── layers[] {id, role, frames[]} ← 新增(多图层,见第八节)
示例(数值取自已产出的真实资源;root_motion 与 layers[].frames 为节选,仅示意结构形状):
{
"version" : 2 ,
"model_3d" : {
"url" : " <object-key>" ,
"format" : " glb" ,
"triangles" : 45000 ,
"has_skin" : true ,
"skeleton_convention" : " mixamorig" ,
"normalized" : { "height" : 1.0 , "feet_y" : 0.0 , "centered" : true },
"source" : " imported"
},
"render_profile" : {
"view" : " side" ,
"faces" : " right" ,
"material" : " cel" ,
"frame_size" : [1107 , 924 ]
},
"sockets" : {
"hand_r" : {
"bone" : " mixamorig:RightHand" ,
"position" : [0.0072 , 0.0451 , 0.0030 ],
"offset_along_axis" : 0.066 ,
"rotation_euler_deg" : [-20 , 0 , 0 ]
}
},
"outfits" : [{
"id" : " default" , "name" : " 默认造型" ,
"actions" : [{
"id" : " walk" , "type" : " walk" , "name" : " 行走" ,
"loop" : true , "fps" : 15.5 , "frame_count" : 16 ,
"duration_s" : 1.0333 ,
"anchor_y" : 0.843 ,
"move_speed_h_per_s" : 1.137 ,
"root_motion" : [[0.0 , 0.0 ], [0.073 , 0.0 ]],
"atlas" : { "file" : " <object-key>" , "cols" : 4 , "rows" : 4 , "scale" : 0.5 },
"derivation" : " render_3d" ,
"clip_ref" : " <clip-id>" ,
"frames" : [{ "index" : 0 , "image_url" : " <object-key>" , "duration_ms" : 64 }],
"layers" : [
{ "id" : " bare" , "role" : " base" , "frames" : [] },
{ "id" : " armed" , "role" : " composed" , "frames" : [] },
{ "id" : " sword" , "role" : " overlay" , "frames" : [] }
]
}]
}]
}
四、版本与兼容
character_data.version 由 1 升为 2。新增字段全部可选,version 1 的存量数据无需迁移 (JSONB 软 schema,读出即为缺省)。消费端遇到缺失字段的退化行为如下,需前后端共同确认:
缺失字段
退化行为
anchor_y
退回按帧底边贴地(即 anchor_y = 1.0),与当前行为一致
root_motion
不施加位移轨
move_speed_h_per_s
不驱动位移,动画原地播放
duration_s
回落到 frame_count / fps
atlas
回落到逐帧 frames[].image_url
derivation
视为 "image_frames"
五、验收标准
以已产出的那套角色资源作为 golden fixture:
无损往返 :现有打包 manifest 的全部字段可由本契约表达,写入后读出可无损还原(逐字段 diff 通过,以单测形式固化在后端)。
播放正确性 (前端 playtest 播 5 个动作):
脚不陷地(anchor_y 生效;对照组统一取单一锚点将陷地约 2.5% 帧高)
走 / 跑不打滑(按 move_speed_h_per_s 驱动位移)
跳跃只跳一次(不叠加重力)
不破坏既有路线 :逐帧路线的既有前端骨架无需改动即可继续工作。
兼容 :一条 version 1 的存量数据按第四节退化行为可正常播放。
六、MVP 假设(四要素)
命题 :以上字段足以让消费端正确播放渲染路线的产物,且不污染逐帧路线的通用契约。
验证方式 :用已产出的真实资源跑「写入 → 读出 → 前端播放」全链路。
通过标准 :第五节四条验收全过。
失败退路 :字段不足 → 在动作层继续加法;若发现 3D 特有数据污染了通用层 → 将渲染路线特有块下沉为 action.render_3d 子对象,通用层只保留 A 组。
七、实施拆分(对应 PR)
PR
内容
规模
1
character_data Pydantic 模型加字段 + MediaCategory 追加成员 + 单测
小,纯加法
2
GenerationType / CharacterActionInput 追加判别字段 + provider 桩
小
3
golden fixture 往返测试 + 跨端口径写入 README / MODULES.md
小
八、决策:一个动作的三套帧如何表达
渲染路线的同一动作会同时产出三套帧,共用同一帧格、像素级可直接叠加:bare(空手)、armed(角色 + 武器)、sword(仅武器,角色隐藏 )。第三套是「武器单独出现」这类特效的前提——武器若烘死在 armed 里就只能整体淡入。
备选
做法
问题
① 三套 = 三个 outfit
现有结构不动即可装下
语义错位:sword 那套没有角色,不是一种「造型」,会让高频操作对象「造型」的含义漂移
② frames[] 加 layers
帧级表达图层
粒度过细:三套帧整段共存,不随帧变化
③ 动作层加 layers[] ,每层一组共帧格的帧
语义准确:同一动作的多个图层
需要前端配合改渲染逻辑(叠加播放)
选择 ③ :三套帧共用帧格、可像素级叠加,本质是「同一动作的多个图层」而非三个造型;塞进 outfit 会让高频操作对象「造型」的语义漂移。字段形如 action.layers[]: {id, role, frames[]},role 取 base / composed / overlay,未提供 layers 时以既有 frames[] 为单层,行为不变。此项影响前端渲染逻辑(需支持叠加播放),实现前请前端确认。
九、不采用清单
不为 3D 资产单独建角色表 :跟随既有「角色相关数据统一存 JSONB、不另行建表」的设计(Character 模型注释已明确)。本 Issue 因此不涉及任何数据库迁移 。
不在本 Issue 定义工作流节点状态机 (含长耗时异步任务的状态流转):归 feat:工作流节点编排 #79 ,本 Issue 只声明该需求存在。
不定义任务推送事件 :归 feat: 生成任务 SSE 推送替代前端轮询 #78 。
不引入迁移工具 :本 Issue 只动 JSONB 与枚举。跨角色复用的动作片段库是否独立建表、是否需要迁移工具,另开 Issue。
不预留像素化相关字段 :该能力尚未实现,先占字段等于先立无法自证的契约。
十、与既有 Issue 的关系
十一、自查(评审常见追问预答)
每个字段能自证必要性吗? 逐条见第二节,并主动标出一个可删候选(move_speed_h_per_s)及删除代价。
只给这些够吗? A 组是「播放正确」的最小集,第五节验收即为其充分性检验;不够则按失败退路加法。
前后一致吗? 单位在第一节统一;枚举沿用既有 StrEnum 小写下划线风格;derivation 与 ai_engine 空骨架:ports 契约 + DerivationStrategy 分流 + 串联 #53 命名对齐;新增枚举成员不改既有成员语义。
固定在正确的抽象层级吗? A 组两条路线通用 → 动作层;B 组仅渲染路线需要 → 角色层;工作流状态机 → 不在本层(归 feat:工作流节点编排 #79 )。
命名照顾高频操作对象吗? 高频对象是「动作」与「造型」,因此三套帧不塞进 outfit(见第八节)。
暂不实现的能力留位了吗? 3D 生成 provider 留桩但契约立好;外部导入资产用 source: "imported" 走同一条路。
背景
本路线指「3D 建模 → 绑骨 → 套动作 → 渲染出 2D 序列帧」(下文简称渲染路线,与既有的逐帧路线并列)。该路线已跑通并产出一套完整角色资源,但这套产物在当前后端契约里没有落点。
当前基线(#64 / #70 已合并):
这套结构围绕逐帧路线建立,渲染路线的产物有三类数据无处可放:
本 Issue 只做加法:新增字段一律可选,新增枚举一律追加成员,既有字段语义与既有调用方均不受影响。
本路线的真实形态
全链路可编程,无人工步骤。除第 1 段外,3D 相关各段均由腾讯云混元生3D
(
ai3d.tencentcloudapi.com· Version2025-05-13)提供接口:SubmitHunyuanTo3DProJob/QueryHunyuanTo3DProJobSubmitAutoRiggingJob/DescribeAutoRiggingJobMotionType参数(48 个预设动作)云端同族接口另有
SubmitHunyuanTo3DMotionJob(文生动作)、SubmitReduceFaceJob(智能拓扑)、SubmitHunyuanTo3DUVJob(UV 展开)、Convert3DFormat(格式转换)可选用。第 4/5 段于 2026-08-03 实测跑通:5 次调用全部成功,单次 40–60 秒;异步提交 + 轮询
(JobId 有效期 24h,Status 取
RUN/DONE/FAIL)。原先依赖第三方网页版人工操作的断点就此消除。实测结论见本 Issue 评论区,其中四点直接影响本契约的字段设计
(骨架命名取值、位移数据来源、
clip_ref需带区间、输入端硬约束)。第 2 段的 provider 适配器本期仍留桩(
NotImplementedError),契约按已接入的形态设计。输入端硬约束(写进契约的依据)
绑骨接口对输入有三条约束,违反不会报错,而是产出错误结果:
model_3d.triangles作为门禁值的依据。我们的武器走刚体挂件(绑完骨挂到手骨、不进网格)天然合规 —— 这是
sockets必须入契约、而不能把武器烘进网格的直接依据。
实测同时验证了
model_3d.skeleton_convention与sockets的必要性:更换绑骨方案确实改变了骨架命名(无命名空间前缀、骨数不同),靠这两个字段即可定位受影响范围——本次实测据此判定挂点参数可复用、无需重标定。
目标
让渲染路线与逐帧路线的产物共用同一套播放契约,且消费端按契约播放即正确:不脚陷地、不打滑、不二段跳。
一、统一单位口径(先定这个,否则字段无法自洽)
所有长度、位移、速度一律以「角色总高 = 1.0」为单位,不使用像素。
屏幕像素 = 数值 × 角色当前屏幕高度。root_motion。契约补充:asset / playtest 增加 root_motion 与逐帧 durations(Refs #62 #35) #63 的论证成立,仅单位改为身高单位。角度单位:度(
deg),欧拉角按XYZ顺序。二、提议字段
A. 动作层(
character_data.outfits[].actions[])——两条路线通用这一组不是渲染路线专属,逐帧路线同样需要,因此加在通用层,而非 3D 子对象里。
anchor_yfloaty = 地面线 − anchor_y × 当前帧高。各动作最深脚点不同,是渲染产物的几何事实,无法从frames[]推出。实测:五个动作统一取 idle 的锚,走路时脚陷地约 2.5% 帧高。归一化后满分辨率与图集共用一个值。root_motionlist[[float, float]] | None(dx, dy),相对首帧,y 向上为正,单位为角色总高。长度 == 帧数。承接 #63 的论证:位移烘进像素则消费端无法控制悬空时长,抽成轨道后可由物理驱动。move_speed_h_per_sfloat | Noneroot_motion是语义差异而非数值差异:前者是产品建议值,后者是片段实测值,二者允许不同——跳跃片段自带约 2.66 身高的前冲,但产品选择播放时不额外推进,否则叠加成两倍。实测 walk 1.137、run 3.023。duration_sfloatfps是导出量而非权威:idle 只取真循环周期 2.05s / 16 帧(等效 7.8fps),整段 10 秒采 16 帧会退化成幻灯片。既有fps不动,本字段为权威口径,冲突时以本字段为准。atlas{file, cols, rows, scale} | Nonescale相对render_profile.frame_size,单格尺寸由二者推导,不重复存。只给散帧 URL 会让每个接入方各自重排一遍。derivation"image_frames" | "render_3d"clip_refstr | None主动交出的可删候选:
move_speed_h_per_s数值上可由root_motion与duration_s推导,保留它的唯一理由是上表所述语义差异。若评审认为「建议值应由消费端决定」,可删,代价是每个接入方各自实现一遍跳跃前冲的取舍。B. 角色层(
character_data)——渲染路线特有必要性逐项:
model_3d:本路线的一切从它派生——补动作、换武器、改渲染都要回到模型;不存则每次重跑前四段(每段都是分钟级异步任务,且第 2 段按次计费)。triangles单列是因为它是门禁值:绑骨服务对输入有面数与文件体积约束(实测 ≤60MB),高模必须先本地减面才能提交。skeleton_convention决定挂点与动作复用如何按骨名寻址,也是更换绑骨方案时的分叉点——实测已发生过一次:更换方案后骨名去掉了命名空间前缀、骨数由 49 变 28,靠该字段即可判定挂点参数可直接复用。建议取值按枚举收敛(如mixamorig/plain_humanoid),而非自由字符串。normalized是所有位移数值的基准,不写死则单位口径失去锚。render_profile:补新动作时必须复用同一套参数,否则新旧动作构图对不上。现有实现特意一次性烤完所有片段来保证共用构图,这个隐式约束必须落到数据上才可复现。sockets:武器走刚体挂件不蒙皮,因此一套骨 + 一套动作可组合出 N 把武器 × M 种配件,换武器无需重绑骨、重出动作——这是渲染路线相对逐帧路线的主要杠杆。position与offset_along_axis分开的理由:前者修正「手骨原点在腕、挂件应在掌心」,后者修正「挂件原点在握把顶端、直接挂等于握在最上端」,两个偏移来源不同,合并成一个数就无法在换武器时只改其一。C. 跨端口径(落 README / MODULES.md,不留聊天记录)
faces = "right",朝左由消费端水平翻转;但不得用镜像帧代替更换渲染视角——角色左右不对称(如单侧腕带),镜像会把不对称细节翻到另一侧。D.
MediaCategory新增追加
model-3d、sprite-atlas。该枚举注释已写明「放在 common 而非 media 模块,新增文件用途时无需修改 media 代码」,此处按既定方式扩展。E. 生成任务
GenerationType追加character_model_3d:3D 生成是独立产物、独立耗时量级、独立失败模式,与出帧不是同一件事。provider 适配器本期留桩,契约先立。CharacterActionInput不拆成两个类型,追加判别字段derivation与各自的可选参数块(model_3d_ref/clip_ref对渲染路线;既有reference_image_urls/num_frames对逐帧路线)。理由:对调用方而言「生成一个动作」仍是同一件事,拆成两个入口会把路线选择泄漏到 API 表面。source: "generated" | "imported":存量资产与用户自带模型均非 task 产出,契约必须能表达这一点。三、改后完整结构与示例
示例(数值取自已产出的真实资源;
root_motion与layers[].frames为节选,仅示意结构形状):{ "version": 2, "model_3d": { "url": "<object-key>", "format": "glb", "triangles": 45000, "has_skin": true, "skeleton_convention": "mixamorig", "normalized": { "height": 1.0, "feet_y": 0.0, "centered": true }, "source": "imported" }, "render_profile": { "view": "side", "faces": "right", "material": "cel", "frame_size": [1107, 924] }, "sockets": { "hand_r": { "bone": "mixamorig:RightHand", "position": [0.0072, 0.0451, 0.0030], "offset_along_axis": 0.066, "rotation_euler_deg": [-20, 0, 0] } }, "outfits": [{ "id": "default", "name": "默认造型", "actions": [{ "id": "walk", "type": "walk", "name": "行走", "loop": true, "fps": 15.5, "frame_count": 16, "duration_s": 1.0333, "anchor_y": 0.843, "move_speed_h_per_s": 1.137, "root_motion": [[0.0, 0.0], [0.073, 0.0]], "atlas": { "file": "<object-key>", "cols": 4, "rows": 4, "scale": 0.5 }, "derivation": "render_3d", "clip_ref": "<clip-id>", "frames": [{ "index": 0, "image_url": "<object-key>", "duration_ms": 64 }], "layers": [ { "id": "bare", "role": "base", "frames": [] }, { "id": "armed", "role": "composed", "frames": [] }, { "id": "sword", "role": "overlay", "frames": [] } ] }] }] }四、版本与兼容
character_data.version由1升为2。新增字段全部可选,version 1 的存量数据无需迁移(JSONB 软 schema,读出即为缺省)。消费端遇到缺失字段的退化行为如下,需前后端共同确认:anchor_yanchor_y = 1.0),与当前行为一致root_motionmove_speed_h_per_sduration_sframe_count / fpsatlasframes[].image_urlderivation"image_frames"五、验收标准
以已产出的那套角色资源作为 golden fixture:
anchor_y生效;对照组统一取单一锚点将陷地约 2.5% 帧高)move_speed_h_per_s驱动位移)六、MVP 假设(四要素)
action.render_3d子对象,通用层只保留 A 组。七、实施拆分(对应 PR)
character_dataPydantic 模型加字段 +MediaCategory追加成员 + 单测GenerationType/CharacterActionInput追加判别字段 + provider 桩八、决策:一个动作的三套帧如何表达
渲染路线的同一动作会同时产出三套帧,共用同一帧格、像素级可直接叠加:
bare(空手)、armed(角色 + 武器)、sword(仅武器,角色隐藏)。第三套是「武器单独出现」这类特效的前提——武器若烘死在armed里就只能整体淡入。outfitsword那套没有角色,不是一种「造型」,会让高频操作对象「造型」的含义漂移frames[]加layerslayers[],每层一组共帧格的帧选择 ③:三套帧共用帧格、可像素级叠加,本质是「同一动作的多个图层」而非三个造型;塞进
outfit会让高频操作对象「造型」的语义漂移。字段形如action.layers[]: {id, role, frames[]},role取base/composed/overlay,未提供layers时以既有frames[]为单层,行为不变。此项影响前端渲染逻辑(需支持叠加播放),实现前请前端确认。九、不采用清单
Character模型注释已明确)。本 Issue 因此不涉及任何数据库迁移。十、与既有 Issue 的关系
root_motion与逐帧durations锚在asset/playtest域,而该模块已在 feat:module api skeletons #64 重构中移除,落点已失效。本 Issue 收编 契约补充:asset / playtest 增加 root_motion 与逐帧 durations(Refs #62 #35) #63 的论证并对齐到当前 character / generation 模块(单位改为身高单位,见第一节)。建议 契约补充:asset / playtest 增加 root_motion 与逐帧 durations(Refs #62 #35) #63 关闭并指向本 Issue,避免两份契约同时流通。derivation是该 Issue 中路线分流在数据层的落点,枚举命名需与之保持一致。source/clip_ref的溯源信息。十一、自查(评审常见追问预答)
move_speed_h_per_s)及删除代价。StrEnum小写下划线风格;derivation与 ai_engine 空骨架:ports 契约 + DerivationStrategy 分流 + 串联 #53 命名对齐;新增枚举成员不改既有成员语义。outfit(见第八节)。source: "imported"走同一条路。