Two fidelity gaps closed:
- Reload/throw/shoot/hit used to REPLACE the whole-body clip, so reloading
mid-slide snapped the character upright on standing legs. The model now
runs a runtime AnimationTree (clips -> Transition -> TimeScale -> OneShot)
whose one-shot layer is FILTERED to upper-body bones: the arms play the
action while the legs keep sliding/running/falling. Land stays full-body.
The rifle hold releases exactly while the one-shot node reports active.
- Grapple no longer plays the library's horizontal-swim clip. The zip pose
is procedural: Fall as the airborne base, the spine pivots to fly along
the line to the actual anchor point (capped so overhead shots don't fold
the body), the head sights the anchor, legs trail behind, the FREE left
hand reaches up the rope — and the right hand keeps holding the rifle.
The anchor point feeds in from the controller for local and remote
players (synced_grapple_point).
- anim_capture: deterministic open-ground teleport + two new regression
shots (reload-while-sliding, grapple zip toward a real anchor).
Co-Authored-By: Claude Fable 5 <[email protected]>
- Third-person camera chain is now HeadPivot > OrbitPivot > ShoulderOffset
(0.55m right, 0.12m up) > SpringArm(2.6m, 14 deg) — the character sits
slightly left of screen centre over-the-shoulder instead of dead centre
behind. Aim is unchanged (still the first-person camera).
- Hold Alt in third person to orbit the camera freely around the character
with the mouse (yaw wraps, pitch clamped -69..+29 deg); the character
keeps facing and aiming where it was. Release Alt and the camera springs
back behind the shoulder.
- debug/orbit_capture.gd: screenshots the OTS framing, a driven orbit, and
the spring-back for visual regression.
Co-Authored-By: Claude Fable 5 <[email protected]>
The model animated its skeleton but rendered a permanent T-pose: the mesh
wasn't skin-bound. Root cause was in autorig's binding, introduced while
chasing an earlier "skins:0" export:
- The earlier "skins:0" was actually caused by bone-heat weighting failing
(0 weighted verts → empty skin gets dropped), NOT by the Armature modifier.
- The "fix" then stripped the Armature modifier and used parent_type=ARMATURE.
That makes the glTF exporter emit a skin OBJECT but with no node.skin
reference and no per-vertex JOINTS/WEIGHTS — an orphan skin. The mesh then
renders its bind pose (T-pose) forever while the skeleton animates unseen.
Fix: keep the standard ARMATURE_AUTO result (Armature modifier + vertex
groups). With the nearest-bone fallback ensuring real weights, the exporter
now writes a COMPLETE skin (verified: meshnode.skin=0, JOINTS/WEIGHTS present).
Also:
- skinned_player_model: _ensure_meshes_bound() re-binds any skinned mesh whose
skeleton NodePath doesn't resolve at load — graceful degradation instead of
a silent T-pose for imperfect GLBs.
- smoke test now asserts the spawned player's mesh is bound to its skeleton,
so this class of bug fails the test instead of shipping. 30/30 pass.
- regenerated miku_test.glb with the corrected pipeline.
Co-Authored-By: Claude Fable 5 <[email protected]>
The first-person head-shrink (SkeletonModifier3D) left the camera looking at
the inside of the neck/torso/hair. Replaced it: the owner's model now renders
shadows-only in first person (clean FPS view, still fully animated and visible
to others and in shadows).
Added a third-person toggle (V / toggle_camera_view) so you can actually see
your own animated model — the easiest way to verify a new skin's animations:
- over-the-shoulder SpringArm3D camera (wall-collision aware) on the local
player; press V to swap, press again to return
- firing still uses the first-person camera, so aim is unchanged
- reveals the owner's model via set_owner_visible() on both SkinnedPlayerModel
and HumanoidModel; hides the first-person weapon viewmodel while in TP
- death forces first person so the toggle can't fight the death cam
Smoke test extended to cover the toggle (camera boom created, becomes current,
round-trips) — 28/28 checks pass for both color and GLB skins.
Co-Authored-By: Claude Fable 5 <[email protected]>
Character pipeline (tools/):
- sketchfab_import.py: search/download via Sketchfab Download API with
license/attribution tracking (SKETCHFAB_API_TOKEN)
- autorig.py: headless Blender auto-rig — fits a Mixamo-named skeleton
to unrigged humanoids, binds automatic weights
- merge_animations.py: merges the shared Mixamo-skeleton animation
library onto any rigged character, strips root motion, canonical names
- pipeline.py: one command chains download -> rig -> animate -> register
In-game skin system:
- SkinManager autoload reads skins.json (auto-written by the pipeline)
- SkinnedPlayerModel rewritten: canonical clips with fallback chains,
blend times, speed-scaled locomotion, weapon bone attachment
- First-person: full animated body for the owner, head hidden via
SkeletonModifier3D; third-person: full model for other players
- Skin selector in main menu; skin id synced in multiplayer
- Fixed GLBLoader crash (GLTFDocument.get_animation_count doesn't exist)
Multiplayer sync overhaul:
- Movement is now client-authoritative: the owning peer simulates
locally (no input round-trip), server keeps health/kills/death
- Remote players interpolate synced_position/velocity with
extrapolation and snap-on-teleport
- Knockback/impulses routed to the simulating peer
Audio:
- AudioManager autoload: SFX/Weapons/Footsteps/UI/Music buses, pooled
3D players, variation + pitch randomization, auto-registration from
assets/sounds
Docs: 3D_ASSET_PIPELINE.md rewritten end-to-end, new ASSET_SOURCES.md
(non-procedural animation/map/sound sources) and SOUND_DESIGN.md.
Verified with debug/spawn_smoke_test.gd (headless: 24/24 checks pass).
Co-Authored-By: Claude Fable 5 <[email protected]>