- Quake-style air-strafe accelerate (real speed gain from strafing; external
speed from dash/rockets never clamped)
- Wall run: keeps entry momentum (incl. upward carry), accelerates along the
wall instead of hard-setting velocity, gravity fades in over the run
- Slide: slope physics (gravity projected along floor accelerates downhill),
flat-ground decel instead of multiplicative friction, entry boost, direct
slide->wallrun and slide->dash transitions
- Dash: cooldown 10s -> 2s, per-player (was a static shared across instances),
momentum fully kept on exit, FOV punch event
- Grapple: taut pendulum with active rope control (W reels in, S pays out),
release keeps swing energy
- Wall climb: carries upward momentum in, jump-off kick, shared vault helper
- Crouch capsule: single owner in the machine, smoothly lerped, ceiling check;
camera eye height follows via crouch factor (no more head snapping)
- Landing: machine emits 'land' events scaled by impact; camera dip + pitched
down thud; footsteps get pitch variation
- Camera rig: event-driven (land dip, dash FOV kick, vault pitch impulse),
slide roll tilt
- Model: Jump vs Fall split by vertical velocity, Land one-shot on heavy
landings, wall-run body lean (synced to remotes via synced_wall_side)
Co-Authored-By: Claude Fable 5 <[email protected]>
Godot import artifacts that pair with already-committed assets:
- miku_test_Image_0..3.png: textures extracted from miku_test.glb on import
- .uid files for spawn_smoke_test.gd and audio_manager.gd
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Layer a procedural SkeletonModifier3D (ShooterPoseModifier) on top of the base
clip so the character reads like a movement-shooter avatar:
- Directional lean: banks into the movement direction (right/left/back) and
blends smoothly for diagonals, driven by velocity relative to facing.
- Slide: leans the torso back and pitches the head up to look forward, instead
of the base clip's forward-torso/legs-out "spine break".
- Airborne: plays a composed Jump pose (weapon ready) rather than a flailing
fall.
- Weapon hold: the base clip already keeps the arms down (weapon at the hip);
on ADS both arms lift and swing in toward centre-front to aim. ADS is read
from the active weapon and synced (synced_is_ads) so remote players raise
their weapons too.
The controller feeds movement direction + ADS to SkinnedPlayerModel.set_locomotion()
each frame (works for local and remote via synced velocity/rotation). All
rotations are authored in skeleton space (fwd=+Z, up=+Y, right=-X) and converted
per-bone; tuning constants are at the top of ShooterPoseModifier.
Verified by rendering the poses (idle/strafe/back/slide/ads) in a real Godot
viewport. Smoke test 30/30.
Co-Authored-By: Claude Fable 5 <[email protected]>
The model animated but deformed wrong: arms crossed the torso and foot heels
stretched to the model's center-bottom. Two root causes, both fixed:
1. Pose mismatch. Sketchfab models are modelled in an A-pose (arms angled
down) but the Quaternius library rests in a T-pose. Binding the A-pose mesh
to T-pose arm bones misweighted the arms, and the library clips (which
rotate arms ~70° down from the T rest) then shoved the already-drooped arms
across the body.
Fix: autorig.fit_arms_to_mesh() reorients each arm bone chain along the
model's real arm direction; merge_animations retargets every clip from the
library's T-pose rest onto the fitted rest via world-space constraint bake
(Copy Rotation/Location + nla.bake), so world motion is reproduced
regardless of the rest difference.
2. Feet-to-center stretch. The library's deform-flagged `root` bone runs up
the body centre-line; nearest-bone weighting assigned inner-foot/heel verts
to it, stretching them to center when animated.
Fix: exclude root/control/mechanism bones from weighting (_NON_DEFORM_HINTS).
Also upgraded nearest-bone weighting from rigid (1 bone/vert) to a smooth
inverse-distance blend across the nearest 4 bones, so joints deform cleanly.
Bone-heat still fails on Miku's joined hair/clothing mesh, so this is the
primary path.
Verified by rendering Idle/Walk/Run poses (Blender): natural stance, arms at
sides swinging correctly, feet planted, proper running stride. Smoke test
still 30/30.
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]>
Verified the full chain on a real Sketchfab model (Miku UID -> rigged ->
13 clips -> loads and animates in-engine, 24/24 smoke checks pass).
Fixes found while running it for real:
- sketchfab_import: read .sketchfab_token as UTF-16/BOM too (PowerShell's
`echo >` writes UTF-16LE, which crashed the UTF-8 reader)
- autorig: rig to the animation library's OWN skeleton when _library.glb
is present (zero retargeting); robust binding — fall back to rigid
nearest-bone weights when Blender bone-heat fails on complex meshes
(Miku's joined hair/clothing gave 0 weighted verts); export as
parent_type=ARMATURE with no modifier (Blender 5.x only emits a glTF
skin in that exact form — a lingering Armature modifier gave skins:0);
export_apply=False (applying modifiers baked away the skin); flatten
the Sketchfab empty hierarchy before binding
- merge_animations: support a single multi-clip library GLB via
LIBRARY_CLIP_MAP; version-safe fcurve access for Blender 4.4+/5.x
slotted actions (Action.fcurves was removed)
Assets/tooling:
- bundle assets/characters/animations/_library.glb — CC0 Quaternius
Universal Animation Library (13 game clips), + .gdignore so Godot
skips the pipeline-input folder
- requirements.txt (stdlib-only; documents Blender-python separation)
- pipeline docs updated for the bundled library + swap instructions
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]>