Narrow the pose-capture discrepancy to the harness and lock bone posing
This commit is contained in:
1 parent
7a5b902797
commit
1d4f718376
5 files changed
+63
-26
No files matched your search
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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])
|
||||||
@@ -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",
|
||||||
|
|||||||
Reference in new issue
Block a user