Narrow the pose-capture discrepancy to the harness and lock bone posing
Full build / Import + headless tests (push) Successful in 45s
Full build / Full desktop build + optional release (push) Skipped

This commit is contained in:
zwitschi committed 2026-11-03 11:24:44 +01:00
1 parent 7a5b902797
commit 1d4f718376
5 files changed
+63 -26

No files matched your search

+6 -2
View File
@@ -11,8 +11,12 @@ Session memory for durable decisions, state, and gotchas. Keep concise, remove o
direct `set_bone_pose_rotation` on HipL/KneeL/Spine/ShoulderL/ElbowL/Head changes rendered direct `set_bone_pose_rotation` on HipL/KneeL/Spine/ShoulderL/ElbowL/Head changes rendered
pixels; a bone *position* offset moves the head; a standalone probe running the same pixels; a bone *position* offset moves the head; a standalone probe running the same
`_animate()` sequence renders the crouch. A staged in-game `film` frame also showed the arms `_animate()` sequence renders the crouch. A staged in-game `film` frame also showed the arms
down, so live venue animation may be affected too. Do not treat the pose sheets as evidence; Standalone SceneTree probes (avatar under `root`, under a `Node3D` stage, and created
a scene test (posed frame differs from rest frame) is the next step. mid-`_process`) all render the seated crouch, so the rig and animation math are fine and the
harness capture path is the outlier. A headless test now locks the proxy→bone contract
(`test_pose_reaches_the_skeleton_bones`, suite 273). Next leads: reproduce the stage inside
`capture_npc_review.gd` (known-good captures) or capture via `SubViewport` +
`RenderingServer.force_draw()`.
- **3D review baseline (2026-11-03):** `docs/3d-model-review.md` records the first skill-guided - **3D review baseline (2026-11-03):** `docs/3d-model-review.md` records the first skill-guided
measurements: 46/46 assets clean, library 59,012 tris, characters 4,482–5,474 tris (68–69% of measurements: 46/46 assets clean, library 59,012 tris, characters 4,482–5,474 tris (68–69% of
the 8,000 caps), `npc_base` only 1,204/2,488, 24-NPC 6.94 ms, and authored texel density the 8,000 caps), `npc_base` only 1,204/2,488, 24-NPC 6.94 ms, and authored texel density
+6 -4
View File
@@ -64,10 +64,12 @@ except the rebuilt assets themselves.
frames still show the rest pose; direct bone writes deform the mesh, so this is a staging/ frames still show the rest pose; direct bone writes deform the mesh, so this is a staging/
capture discrepancy that may also affect live venue frames. See capture discrepancy that may also affect live venue frames. See
`docs/3d-model-review.md` §"Pose evidence is blocked". `docs/3d-model-review.md` §"Pose evidence is blocked".
- [ ] Diagnose the pose-capture discrepancy: compare the harness staging with the working - [ ] Finish the pose-capture diagnosis. Done so far: the pose reaches proxies and skeleton
standalone probe (SceneTree lifetime, node parents, first-frame skin bind, capture timing) bones (new headless test, 273/273), direct bone rotations deform the rendered mesh, and
and add a scene test asserting a posed avatar renders differently from its rest frame. three standalone probes render the seated crouch — only `capture_npc_pose.gd` saves the
Do not trust pose sheets or claim rig deformation until that test passes. rest pose. Remaining leads: reproduce the stage inside `capture_npc_review.gd` (known to
render) or capture via `SubViewport` + `RenderingServer.force_draw()`, then add a scene
test that a posed avatar renders differently from its rest frame.
- [ ] UV/texel density: measure the 256² slot maps; decide whether atlas, UDIM or a resolution - [ ] UV/texel density: measure the 256² slot maps; decide whether atlas, UDIM or a resolution
change is justified. Do not change `CHAR_MAX_TRIS`/`CHAR_MAX_VERTS` without re-running the change is justified. Do not change `CHAR_MAX_TRIS`/`CHAR_MAX_VERTS` without re-running the
budget smoke and the Web/Compatibility path. budget smoke and the Web/Compatibility path.
+19 -18
View File
@@ -165,26 +165,27 @@ for presence, not re-reviewed for art quality.
## Pose evidence is blocked (2026-11-03) ## Pose evidence is blocked (2026-11-03)
The weights/pose column cannot be filled yet. A pose harness was added The weights/pose column cannot be filled yet. The pose logic is verified; the *capture* is not.
(`src/test/capture_npc_pose.gd`, staged in `tmp/model-pose*`), and its output does not match the `src/test/capture_npc_pose.gd` stages idle / walk-contact / walk-pass / seated for all eight
state it reports: variants (full-body + shoulder views) and its output does not match the state it reports.
- `_animate()` applies the seated pose (logged `rig_y=-0.339`, `leg0=-1.357`) and | Check | Result |
`Skeleton3D.get_bone_pose_rotation()` returns the rotated quaternion at capture time, yet the | --- | --- |
saved frame shows the rest pose. | Pose reaches proxies | Seated pose logs `rig_y=-0.339`, `leg0=-1.357` |
- Ruled out: missing skeleton (one `Skeleton3D`, mesh bound to it, `same=true`), hidden mesh | Pose reaches skeleton bones | `Skeleton3D.get_bone_pose_rotation(HipL/KneeL)` differs from rest; new headless test `test_pose_reaches_the_skeleton_bones` locks it (suite 273/273) |
(hiding `npc_base` removes the body), pose not applied (proxies and bone poses verified), | Idle restores rest | Same test asserts the hip returns to its rest orientation |
and unbound skinning: writing a single bone rotation directly (`HipL`, `KneeL`, `Spine`, | Skinning bound | One `Skeleton3D`, mesh bound to it (`same=true`); hiding `npc_base` removes the body |
`ShoulderL`, `ElbowL`, `Head`) changes the rendered pixels, and a bone *position* offset moves | Bone rotations deform the mesh | Direct writes change rendered pixels: Spine 1,719 / HipL 1,052 / KneeL 810 / ElbowL 745 / Head 671 / ShoulderL 640 sampled pixels |
the head. A standalone probe that calls the same `_animate()` sequence renders the crouch. | Bone translation deforms the mesh | A +0.5 m Head offset moves the head in frame |
- Also observed: a staged in-game `film` pose frame showed the arms down, so this may affect the | Standalone probes render the pose | Three variants (avatar under `root`, under a `Node3D` stage, created mid-`_process`) all render the seated crouch (`posed` vs rest diff ≈ 2,200–2,360 samples) |
live game rather than only the harness. That is why it is a P0/P1 candidate rather than a QA | The harness renders the pose | **No.** All 8 variants × 4 poses save the rest pose while the logged state and bone poses are posed |
tooling nit.
Next step is a focused diagnosis: compare the harness/venue staging with the standalone probe The cause is therefore in the harness's capture path, not in the rig or the animation math. Two
(SceneTree lifetime, node parents, first-frame skin bind, capture timing) and add a scene test leads are left and both are cheap: reproduce the stage inside a harness that is known to render
that asserts a posed avatar renders differently from its rest frame. Do not mark the pose/weight correctly (`capture_npc_review.gd` already produces 96 valid frames), or capture through a
scorecard row until that test exists. `SubViewport` with `RenderingServer.force_draw()` so the frame cannot lag the state. Until a
scene test asserts that a posed avatar renders differently from its rest frame, do not treat pose
sheets as evidence and do not claim rig deformation in the review.
## Ranked candidate gaps ## Ranked candidate gaps
-2
View File
@@ -261,8 +261,6 @@ func _shoot() -> void:
while FileAccess.file_exists(path): while FileAccess.file_exists(path):
path = dir.path_join("%s-v%d.png" % [base, index]) path = dir.path_join("%s-v%d.png" % [base, index])
index += 1 index += 1
var hip_bone: int = int(_avatar._bone_indices.get("HipL", -1))
var knee_bone: int = int(_avatar._bone_indices.get("KneeL", -1))
var img: Image = root.get_texture().get_image() var img: Image = root.get_texture().get_image()
var err := img.save_png(path) var err := img.save_png(path)
print("%s -> err=%d" % [base, err]) print("%s -> err=%d" % [base, err])
+32
View File
@@ -147,6 +147,38 @@ func test_skinned_model_loads_and_keeps_the_head_contract() -> bool:
avatar.free() avatar.free()
return a and b and c return a and b and c
## The pose harness captures frames, but a regression here is cheap to catch:
## the pose logic must reach the skeleton bones, not only the proxy pivots.
func test_pose_reaches_the_skeleton_bones() -> bool:
var avatar := _avatar(Rules.NpcType.TOURIST, 55)
var sk := avatar._skeleton
var hip: int = int(avatar._bone_indices["HipL"])
var knee: int = int(avatar._bone_indices["KneeL"])
var rest_hip := sk.get_bone_rest(hip).basis.get_rotation_quaternion()
var rest_knee := sk.get_bone_rest(knee).basis.get_rotation_quaternion()
avatar.arrived = true
avatar.npc.intent = "sit"
for _i in 45:
avatar._animate(1.0 / 60.0)
var sit_hip := sk.get_bone_pose_rotation(hip)
var sit_knee := sk.get_bone_pose_rotation(knee)
var a := TestUtils.check(not sit_hip.is_equal_approx(rest_hip),
"seated pose rotates the HipL bone away from rest")
var b := TestUtils.check(not sit_knee.is_equal_approx(rest_knee),
"seated pose rotates the KneeL bone away from rest")
var c := TestUtils.check(avatar._rig.position.y < -0.25,
"seated pose lowers the rig (%.3f)" % avatar._rig.position.y)
# Returning to idle must restore the rest orientation, not leave the sit pose.
avatar.arrived = false
avatar.npc.intent = "roam"
avatar.velocity = Vector3.ZERO
for _i in 120:
avatar._animate(1.0 / 60.0)
var d := TestUtils.check(sk.get_bone_pose_rotation(hip).is_equal_approx(rest_hip),
"idle restores the HipL rest orientation")
avatar.free()
return a and b and c and d
func test_character_selection_is_deterministic_and_complete() -> bool: func test_character_selection_is_deterministic_and_complete() -> bool:
var expected := { var expected := {
Rules.NpcType.TOURIST: "npc_casual", Rules.NpcType.TOURIST: "npc_casual",