Skip to content

feat(render3d): 出帧台读到的骨架事实与位移轨落进造型与动作 - #777

Open
johnnyzhang-eng wants to merge 2 commits into
mainfrom
feat/774-rig-facts
Open

feat(render3d): 出帧台读到的骨架事实与位移轨落进造型与动作#777
johnnyzhang-eng wants to merge 2 commits into
mainfrom
feat/774-rig-facts

Conversation

@johnnyzhang-eng

Copy link
Copy Markdown
Contributor

Summary

出帧台每渲一段动作都从绑骨模型里读出骨架事实、根骨位移轨与可用片段表,此前三样全部算完即丢(sheet.rig / available_clips 在 main 上各有 0 处消费)。本 PR 给它们落点。

本 PR 基于 #771feat/518-render3d-entry,diff 只含本条改动。

落点

  • CharacterOutfit.rig_facts —— 骨数、根骨名、骨名列表、蒙皮网格数、顶点数、可用片段表。每造型一份,与 model_3d_url 同批产出。
  • CharacterAction.root_motion —— 逐帧 (dx, dz),单位「1.0 = 角色总高」。每动作一份

两个字段都可空、纯加法,存量数据原样解析(有用例锁)。不引入迁移机制,schema_sync 巡检能覆盖;真正的迁移机制是 #633

位移轨取哪一份

出帧台从根骨动画轨读出的那份,不取 postprocess.rootmotion 从交付帧像素反推的那份。

后者是在帧已被对齐成原地之后再猜,信息在对齐那一步就损失了;前者是作者意图的精确值,且在三渲二这条路上是白送的。两份不混装在同一字段里——i2v 路线没有骨架,只能用反推的,那条不写进本字段,免得同一个字段装着两种精度的数还分不出来。

两条路必须交回同样的东西

服务端渲:strategy 在 derive 里留下,_finish 取走。

浏览器渲:此前 completeBake 只回传 clipsampleTimesrigInfo 与根骨轨在浏览器里算完就随页面销毁——这是 #714 引入的缺口,本 PR 一并收。现在 BakeStage 在压平根骨 XZ 之前先把位移抽出来存着(压平是为了让帧原地不动,位移本身仍是产物的一部分),随交齐一起回传,服务端存进登记、续跑时带进出参。

有用例锁住「同一造型走哪条路存下来的资产一致」。

撤掉了原方案里的一条硬校验

issue 原方案写了「拿到骨架事实之后,RigInfo docstring 里那条『28 骨 · humanoid 命名 · 无 mixamorig 前缀』才有地方执行,不满足时不静默继续」。

实测推翻(量全部归档绑骨产物):

产物 骨数 mixamorig 前缀
wolf_500k_rigged 24 0
wolf_rigged 27 0
hy_walk_v3 28 0
RobotExpressive 43 0
xed_all 49 49

「28 骨」只有 1/5 命中,而 24 与 27 正是混元自己绑的四足——同一条链路出来的骨数就不是常数;带前缀那个是 Mixamo 绑的,属重定向路线的正常输入。据此当闸会把自家产物与重定向输入一起挡掉。

RigInfo 的 docstring 自称「已确立(不必每次重验)」,重验一次即翻。已改判并写明依据,免得下一个人接着信。这几个数改为记录用,不做拒绝。

顺带更正一处 issue 表述

#192 的子项把这条写成「无处存放就等于每补一个动作都重做一遍前四段,直接抹掉本路线的成本优势」。该表述不成立_produce_action 见到 model_3d_url 就直接走出帧,绑骨只在建资产路径上调,动作生成路径碰不到它——成本复用在 #277model_3d_url 时就解决了。本 PR 的实际收益是「已经算出来的数据不再被丢掉」,不是省钱。已写进 #774 备注。

不包含

Test plan

  • 后端 ruff / export_openapi(openapi.json 已随之更新 +179 行)/ lint-imports / pytest -q1780 passed, 14 skipped
  • 前端 format:check / lint / typecheck / test:coverage / build1213 passed
  • 新增用例:后端 7 条(服务端渲带出两样、i2v 路线恒为 None、落库出参含两键、Redis 往返、空值不写、坏 JSON 当没有),前端桩同步扩到新契约
  • 存量数据(无新字段)仍能解析,有用例锁

命令都跑在最后一次编辑之后。

Closes #774

@vercel

vercel Bot commented Aug 26, 2026

Copy link
Copy Markdown

Deployment failed for project windup with the following error:

Resource is limited - try again in 24 hours (more than 100, code: "api-deployments-free-per-day").

Learn More: https://vercel.com/huyan-s-projects1?upgradeToPro=build-rate-limit

@github-actions

github-actions Bot commented Aug 26, 2026

Copy link
Copy Markdown

✅ 此 PR 已关联 issue,之前的提醒已自动标记为已解决。

@codecov

codecov Bot commented Aug 26, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 80.00000% with 20 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
frontend/src/features/client-bake/stage.ts 0.00% 12 Missing ⚠️
...app/src/windup_app/server/orchestrator/executor.py 41.66% 7 Missing ⚠️
frontend/src/entities/render3d/api.ts 50.00% 0 Missing and 1 partial ⚠️

Impacted file tree graph

@@            Coverage Diff             @@
##             main     #777      +/-   ##
==========================================
- Coverage   86.47%   86.45%   -0.03%     
==========================================
  Files         296      296              
  Lines       22136    22232      +96     
  Branches     3356     3359       +3     
==========================================
+ Hits        19142    19220      +78     
- Misses       2055     2072      +17     
- Partials      939      940       +1     
Flag Coverage Δ
backend 91.43% <91.66%> (+<0.01%) ⬆️
frontend 80.44% <18.75%> (-0.09%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
...e/src/windup_ai_engine/impl/character_generator.py 86.57% <100.00%> (+0.18%) ⬆️
...s/ai_engine/src/windup_ai_engine/ports/__init__.py 100.00% <100.00%> (ø)
...i_engine/src/windup_ai_engine/strategy/concrete.py 90.78% <100.00%> (+0.98%) ⬆️
...kages/app/src/windup_app/server/character/model.py 100.00% <100.00%> (ø)
.../src/windup_app/server/orchestrator/client_bake.py 94.11% <100.00%> (+0.98%) ⬆️
...es/app/src/windup_app/server/orchestrator/model.py 97.88% <100.00%> (+0.02%) ⬆️
...pp/src/windup_app/server/orchestrator/task_repo.py 93.33% <ø> (ø)
...nd/packages/app/src/windup_app/web/api/render3d.py 96.15% <100.00%> (+0.22%) ⬆️
frontend/src/features/client-bake/index.ts 96.96% <100.00%> (+0.09%) ⬆️
frontend/src/entities/render3d/api.ts 95.12% <50.00%> (-1.13%) ⬇️
... and 2 more
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@fennoai fennoai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

审阅了后端生成结果、浏览器出帧回传、任务反序列化及前端动作结果映射。当前实现有两处会让新字段在真实流程中不可靠:一处在任务查询/断线恢复时丢失,另一处在并发三渲二任务间串值。

验证:已按固定提交范围检查完整 diff;本环境无法执行后端测试(pytest/uv 不可用)或前端类型检查(tsc 不可执行)。

# 出帧台读到的骨架事实与根骨位移轨(#774)。三渲二独有,i2v 路线恒为 None。
# 此前这两样每渲一段都算一遍、算完即丢。骨架事实是**每造型一次性**的,
# 随第一个动作带上来即可;位移轨是每动作一份。
if generated.rig is not None:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Preserve rig metadata through task deserialization

这里把 rig_factsroot_motion 写入了动作结果 JSON,但 task_repo._deserialize_result() 重建 CharacterActionOutput 时没有读取这两个键;因此通过任务查询接口或断线重连拿到的已完成任务会静默丢掉新字段(实时事件与持久化读取的结果也会不一致)。前端的 mapActionResult 同样只映射 frames/geometry,所以这些数据最终无法到达动作结果消费者。请把字段加入动作输出模型及两条反序列化/映射路径,确保结果持久化后仍完整。

# 出帧台读到的骨架事实与根骨位移轨此前算完即丢(#774)。留在实例上由
# CharacterGenerator._finish 取走 —— derive 的返回类型是帧列表,是两个入口
# (服务端渲 / 浏览器渲)共用的形状,不为这一条改它。
self._last_rig = RigFacts(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Avoid shared mutable metadata between concurrent renders

ActionTaskExecutor 是进程级单例,缓存的 CharacterGenerator/RenderFrameStrategy 会被多个任务线程共享;本 PR 将每次渲染的结果写入 _last_rig_last_root_motion,再由调用方稍后从 strategy 读取。两个三渲二任务可在 A 的 derive() 写入后、A 的 _finish() 读取前交错,导致 A 的动作帧携带 B 模型的骨架事实和位移轨(反之亦然),并将错误元数据持久化。请让派生结果与帧一起按调用返回,或以请求局部状态传递,不能依赖共享 strategy 实例字段。

@johnnyzhang-eng
johnnyzhang-eng changed the base branch from feat/518-render3d-entry to main August 27, 2026 10:01
此前每渲一段都算一遍、算完即丢。骨架事实每造型一份,位移轨每动作一份,
取根骨动画轨而非交付帧像素反推。服务端渲与浏览器渲两条路交回同一批数。

Closes #774
三条都属于"任务照常 COMPLETED、帧数时长成色全对、零报错,只是产出的东西是错的"。
两位审查者(FennoAI + 内部审查)独立指出了其中两条。

① **位移轨恒为 None。** bake_driver.mjs 交回 meta 时已按片段名拆过一层
   (root_motion: rootMotion[clip] ?? null),sprite.py 原样透传 —— _root_motion_of
   再 .get(clip) 一次恒得 None。服务端渲那条 100% 走这里。
   顺带修脏数据守卫:原来只校验 x 且解包先于守卫,长度不对照样 ValueError、z 是脏值
   直接抛;守卫命中时又只丢那一条,让位移轨比帧数短、索引静默错位。改成整条作废。

② **并发串味。** ActionTaskExecutor 是进程级单例,strategy 被所有任务线程共用而
   action 并发默认 8:A 的 derive 写完、A 的 _finish 读之前,B 的 derive 会覆盖它。
   改用 ContextVar 按请求隔离,取完即清。不改 derive 的返回形状 —— 那是两个入口共用的。

③ **落库丢字段。** rig_facts / root_motion 只写进结果 JSON,没进 CharacterActionOutput
   也没进 _deserialize_result:查询接口与断线重连拿到的已完成任务缺这两样,而实时事件
   那条路有,两条路给出不同结果。同一个坑 geometry 已经踩过一次,注释就在它旁边。

配套修正测试:那个桩用的是 {"walk": {...}},比生产多包一层 —— ① 那个真 bug 就是这么
合进来的。改成 bake_driver 实际交回的形状。

另外 rebase 到 main(#771 已合)时解冲突,顺手清掉两处重复的 return 与一处把 strategy
当 render_spec 位置实参传的写法。
@vercel

vercel Bot commented Aug 27, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
windup Ignored Ignored Preview Aug 27, 2026 10:10am

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(render3d): 出帧台读出的骨架事实与位移轨没有落点,算完即丢

1 participant