Lane E, Sprint 13: no wind clip — the pipeline can't supply one, and the lean was never a clip
Gate 2.4 asked for lean/stagger/brace clips staged by wind band. The answer,
measured twice, is that the asset shouldn't exist.
1. The lean cannot be a clip. _rotOnly drops Hips.quaternion from every clip at
load (player.js:53, applied :239). Parsed the shipped pack to confirm rather
than trust the read: 17 clips, 195 channels each, 64 survive. A lean baked
into a clip's hips is discarded. And stripping it is right — the wind's
bearing swings (the change; site_02's venturi) and a canned clip has one
fixed lean axis, so only the sim can aim it. Same trick as the knockdown and
climbY. The lean is D's root pitch; thresholds proposed off Beaufort 7
("inconvenience felt when walking against the wind" = 13.9 m/s), which lands
storm_01 never leaning, storm_03 gusting through the line, storm_02 in gale.
2. Mixamo has no wind animations. Searched the real API, not assumed: shiver
returns zero, brace returns eleven ledge-hang clips, wind returns Catwalk
Turn. The factory header already recorded this in Sprint 3.
Rendered the two real candidates on a skinned rig, twice each — as authored and
as the game shows them. Pushing is a squat shoving a couch. Leaning is a man
waiting for a bus. Both rejected. Both would have passed every check we own:
they load, they're metre-scale, their nodes survive. Dims can't answer "is this
the right shape" — the bike rule, catching a whole feature this time.
Brace and stagger already ship (TakeCover, StumbleBack, Reaction). D should wire
the root lean first; I'll hand-author a WindPosture additive into the gap we can
both see, if one remains. Spine rotation survives _rotOnly, so it would land.
Evidence, harness and repro in tools/character/windclip_candidates/, plus the
four pipeline traps I hit in order so the next lane doesn't.
selftest 335/0/0, unchanged — no asset added, no shipped file touched.
This commit is contained in:
parent
d5c16534c1
commit
91bc391633
52
THREADS.md
52
THREADS.md
@ -4627,3 +4627,55 @@ anchors are your GLB), but the tooling is now waiting, not TODO.
|
||||
Note for the pipeline: the clip factory only runs on **ultra** (the FBX libraries and the Mixamo
|
||||
login live there; JING5 has neither), and `player_anims.glb` is committed per the copies rule, so
|
||||
the game never needs that box. Nothing here changes that.
|
||||
|
||||
[E] 2026-07-18 — 🌬 **VERDICT: I'm shipping NO wind clip, and that's the deliverable.** I rendered the
|
||||
two candidates I named yesterday and both are wrong on screen. Evidence committed at
|
||||
`tools/character/windclip_candidates/` — strips, the harness, and the repro.
|
||||
· **`Pushing` — rejected.** It is not a lean. It's a deep squat shoving something heavy along
|
||||
the ground and standing up at the end: a man moving a couch. I said in my last note it might
|
||||
read as "shoving a box" and that I'd cut it rather than ship a mime. It does, so it's cut.
|
||||
· **`Leaning` — rejected.** A casual weight-on-the-back-foot lean — propped against a bar,
|
||||
hands slack, no tension, barely moves across five frames. A man waiting for a bus. Nothing to
|
||||
do with 32 m/s.
|
||||
· `Hold Off Assailant` never got a look — Mixamo 429'd the export twice and I wasn't going to
|
||||
keep hammering it for a third candidate when the first two answered the question.
|
||||
**Both would have passed every check we own.** They load, they're metre-scale, their nodes
|
||||
survive the export. Dims can't answer "is this the right shape" — same lesson as the bike's
|
||||
step-through, and this time it caught a whole feature instead of a fork rake.
|
||||
|
||||
[E] 2026-07-18 — 🌬 **LANE D — so here's what I think you should actually build, and it's less than
|
||||
the sprint asked for.** SPRINT13 gate 2.4 asks for lean/stagger/brace clips staged by band. Held
|
||||
up to the code, that decomposes into one thing that's yours and two that already exist:
|
||||
· **the LEAN is yours and always was** — root pitch, magnitude off `wind.speedAt(player.pos, t)`,
|
||||
aimed at `wind.dirAt(t)`, thresholds as posted (calm <5.5 / breeze 5.5-13.9 / gale 13.9+ m/s,
|
||||
Beaufort 7 = "inconvenience felt when walking against the wind"). No asset needed or possible:
|
||||
`_rotOnly` drops `Hips.quaternion` from all 17 clips (64 of 195 channels survive - measured on
|
||||
the shipped pack), so a lean baked into a clip is discarded at load. The wind's bearing swings
|
||||
and a clip's lean axis can't. Third instance of the knockdown/climbY pattern.
|
||||
· **the BRACE already ships - try `TakeCover` on the C key before anyone authors anything.** It's
|
||||
in the pack already, retargeted and working: hunched, arms up, planted. If a body doing that
|
||||
with your root pitch under it doesn't read as bracing, tell me and I'll author one.
|
||||
· **the STAGGER already ships too** - `StumbleBack` and `Reaction`. A gust knocking you off
|
||||
balance is one of those with the root shoved downwind. Sim trigger, not asset.
|
||||
**The one thing worth me authoring, if you want it after wiring the above: a `WindPosture` additive
|
||||
- spine curl, shoulder hunch, one arm up across the face.** Not a lean; a lean is your root. This
|
||||
is the tension the root can't express. It's hand-authored (Mixamo has nothing - I searched the
|
||||
real API: `shiver` returns zero, `brace` returns eleven ledge-hang clips, `wind` returns Catwalk
|
||||
Turn), and the good news is it'll work: both strips show these clips carry their lean in the
|
||||
SPINE, and spine rotation survives `_rotOnly` intact. So a hand-authored posture lands.
|
||||
**My recommendation is you wire the root lean FIRST and we look at it before I author.** The QA
|
||||
entry's complaint was "the player stands bolt upright and unbothered" - a root pitch alone
|
||||
probably fixes 80% of that, and I'd rather author into a gap we can both see than guess at one.
|
||||
Yours to call; I'm not precious about it.
|
||||
|
||||
[E] 2026-07-18 — 🌬 **pipeline notes, so the next lane doesn't re-lose the afternoon I lost.** All in
|
||||
the candidates README, the short version here: `fetch.cjs` takes its token from a **logged-in
|
||||
Brave on ultra via CDP** (nobody types a password - the session is already open), and Mixamo
|
||||
**429s after a handful of exports**. `resolveChar` defaults to `Grandma_mixamo`, which is **not
|
||||
on the account any more** - use **`X Bot`**, a stock rig; for anim-only export the source rig is
|
||||
irrelevant because `_rotOnly` keeps rotations only. Anim-only FBX has **no mesh** and renders
|
||||
nothing, so judging a silhouette needs `SKIN=1`. And do NOT preview by retargeting onto
|
||||
`Hum_M_1_mixamo.fbx`: its armature carries FBX's 0.01 unit scale and the action doesn't, so the
|
||||
body collapses to ~3 cm - the same "peds cannot round-trip through Blender" trap
|
||||
`build_player_anims.py` already documents. I hit all four of those in order.
|
||||
selftest **335/0/0**, unchanged - I added no asset and touched no shipped file.
|
||||
|
||||
82
tools/character/windclip_candidates/README.md
Normal file
82
tools/character/windclip_candidates/README.md
Normal file
@ -0,0 +1,82 @@
|
||||
# Wind clips — the Mixamo candidates, and why they're rejected
|
||||
|
||||
Lane E, Sprint 13 gate 2.4 ("wind clips for the player — lean / stagger / brace,
|
||||
staged by wind band, through the Mixamo pipeline").
|
||||
|
||||
**Outcome: no clip shipped. The Mixamo pipeline cannot supply a wind lean, and the
|
||||
lean was never going to be a clip in the first place.** Both halves of that are
|
||||
measured, and the evidence is in this folder. Full argument in THREADS [E] 2026-07-17/18.
|
||||
|
||||
## 1. Mixamo has no wind animations
|
||||
|
||||
Searched the real API (`fetch.cjs search`, on ultra), not assumed:
|
||||
|
||||
| term | what comes back |
|
||||
|---|---|
|
||||
| `brace` | eleven **Braced Hang** ledge-climbing clips |
|
||||
| `wind` | Standing Idle 04, Catwalk Walk Turn 180 |
|
||||
| `shiver` | nothing, 0 results |
|
||||
| `stagger` | three walks (Swagger Walk, Walking, Walking) |
|
||||
| `lean` | Leaning On A Wall, Leaning, One Shoulder Lean |
|
||||
| `push` | Pushing, Push Up, Button Pushing |
|
||||
|
||||
The factory's own header already recorded half of this in Sprint 3 — *"Hammering /
|
||||
Sweeping Floor / **Bracing** don't exist on Mixamo — skipped."* This is the case
|
||||
the asset-pipeline skill names directly: *truly bespoke clips don't exist on
|
||||
Mixamo — hand-author, don't keep searching.*
|
||||
|
||||
## 2. The two real candidates, rendered and rejected
|
||||
|
||||
`skinshot.py` films a skinned Mixamo FBX twice: **as authored**, and **as the game
|
||||
shows it** (Hips rotation cleared, which is what `player.js:_rotOnly` does at load).
|
||||
The second row is the honest one.
|
||||
|
||||
- **`rejected_Pushing.png`** — not a wind lean. A deep squat shoving something heavy
|
||||
along the ground, standing up at the end. It reads as *moving a couch*. The
|
||||
forward angle I hoped to borrow is a squat, not a lean.
|
||||
- **`rejected_Leaning.png`** — a casual weight-on-the-back-foot lean, the "propped
|
||||
against a bar" pose. Hands slack, no tension, near-identical across all five
|
||||
frames. A person waiting for a bus, not one in a gale.
|
||||
|
||||
Neither is close enough to fix by retiming. Shipping either would have passed every
|
||||
check we have — they load, they're metre-scale, their nodes survive — and been wrong
|
||||
on screen. That's the bike rule: **the verify can't answer "is this the right shape."**
|
||||
|
||||
## 3. The useful technical result
|
||||
|
||||
In both strips the raw and stripped rows are **near-identical**, which is worth
|
||||
knowing: these clips carry their lean in the **spine**, and spine rotation survives
|
||||
`_rotOnly` intact. So a hand-authored posture (spine curl, shoulder hunch, arm up)
|
||||
*would* survive the loader. What cannot survive is a lean authored into the **hips** —
|
||||
`_rotOnly` drops `Hips.quaternion` from every clip (`player.js:53`, applied `:239`).
|
||||
Measured on the shipped pack: 17 clips, 195 channels each, **64 survive**.
|
||||
|
||||
Which is why the lean belongs in `player.sim.js` as a root pitch aimed at
|
||||
`wind.dirAt(t)` — the wind's bearing swings (the change; site_02's venturi) and a
|
||||
canned clip has one fixed lean axis. Same trick as the knockdown and `climbY`.
|
||||
|
||||
## Reproducing
|
||||
|
||||
```sh
|
||||
# on ultra (the FBX libraries and the Mixamo login live there; JING5 has neither)
|
||||
mkdir -p /tmp/shadeswind && cd /tmp/shadeswind
|
||||
cp ~/Documents/mixamo-fetch/fetch.cjs .
|
||||
printf 'Pushing\nLeaning\n' > wishlist.txt
|
||||
SKIN=1 node fetch.cjs "X Bot" # SKIN=1: needs a mesh to judge a silhouette
|
||||
/Applications/Blender.app/Contents/MacOS/Blender -b --factory-startup \
|
||||
-P skinshot.py -- /tmp/shadeswind/out/Pushing.fbx Pushing
|
||||
```
|
||||
|
||||
Notes for whoever next reaches for this pipeline:
|
||||
- `fetch.cjs` reads its token from a **logged-in Brave on ultra via CDP** (port 9222).
|
||||
Nobody types a password; the session is already there. It 429s after a handful of
|
||||
exports — space them out.
|
||||
- `resolveChar` defaults to `Grandma_mixamo`, which is not currently uploaded to the
|
||||
account. **`X Bot`** is a Mixamo stock rig and works; for anim-only export the source
|
||||
rig is irrelevant anyway, since `_rotOnly` keeps rotations only.
|
||||
- Anim-only FBX (`SKIN` unset) has **no mesh** and renders nothing. Judging a
|
||||
silhouette needs `SKIN=1`.
|
||||
- Don't retarget onto `Hum_M_1_mixamo.fbx` to preview: its armature carries FBX's
|
||||
0.01 unit scale and the clip's action doesn't, so the body collapses to ~3 cm.
|
||||
That's the same "peds cannot round-trip through Blender" trap `build_player_anims.py`
|
||||
documents.
|
||||
BIN
tools/character/windclip_candidates/rejected_Leaning.png
Normal file
BIN
tools/character/windclip_candidates/rejected_Leaning.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 234 KiB |
BIN
tools/character/windclip_candidates/rejected_Pushing.png
Normal file
BIN
tools/character/windclip_candidates/rejected_Pushing.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 315 KiB |
84
tools/character/windclip_candidates/skinshot.py
Normal file
84
tools/character/windclip_candidates/skinshot.py
Normal file
@ -0,0 +1,84 @@
|
||||
"""Filmstrip a SKINNED Mixamo FBX, twice: as-authored, and as the game shows it
|
||||
(Hips rotation cleared — player.js:_rotOnly).
|
||||
|
||||
blender -b --factory-startup -P skinshot.py -- <skinned.fbx> <label>
|
||||
|
||||
Skinned export carries its own mesh at its own scale, so there's no cross-rig
|
||||
retarget and no unit mismatch — the reason the Hum_M_1 route collapsed.
|
||||
|
||||
Camera is fitted to the MEASURED bbox of the animated body rather than guessed,
|
||||
because guessing framing is how the last three renders came out flat grey.
|
||||
"""
|
||||
import bpy, sys, os, math
|
||||
from mathutils import Vector
|
||||
|
||||
argv = sys.argv[sys.argv.index("--") + 1:]
|
||||
SRC, LABEL = argv[0], argv[1]
|
||||
|
||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||
bpy.ops.import_scene.fbx(filepath=SRC)
|
||||
|
||||
arm = next(o for o in bpy.data.objects if o.type == 'ARMATURE')
|
||||
meshes = [o for o in bpy.data.objects if o.type == 'MESH']
|
||||
act = arm.animation_data.action if arm.animation_data else None
|
||||
hips = next((b for b in arm.pose.bones if b.name.lower().endswith("hips")), None)
|
||||
f0, f1 = (int(act.frame_range[0]), int(act.frame_range[1])) if act else (1, 1)
|
||||
print(f" arm={arm.name} meshes={len(meshes)} act={act.name if act else None} frames={f0}..{f1} hips={hips.name if hips else None}")
|
||||
|
||||
sc = bpy.context.scene
|
||||
N = 5
|
||||
frames = [int(f0 + (f1 - f0) * i / (N - 1)) for i in range(N)] if f1 > f0 else [f0] * N
|
||||
|
||||
|
||||
def world_pts(frame):
|
||||
sc.frame_set(frame)
|
||||
bpy.context.view_layer.update()
|
||||
dg = bpy.context.evaluated_depsgraph_get()
|
||||
pts = []
|
||||
for m in meshes:
|
||||
ev = m.evaluated_get(dg)
|
||||
pts += [ev.matrix_world @ Vector(c) for c in ev.bound_box]
|
||||
return pts
|
||||
|
||||
|
||||
# --- measure the whole clip, then frame it once so all tiles share a scale.
|
||||
allp = []
|
||||
for fr in frames:
|
||||
allp += world_pts(fr)
|
||||
minx, maxx = min(p.x for p in allp), max(p.x for p in allp)
|
||||
miny, maxy = min(p.y for p in allp), max(p.y for p in allp)
|
||||
minz, maxz = min(p.z for p in allp), max(p.z for p in allp)
|
||||
cx, cy, cz = (minx + maxx) / 2, (miny + maxy) / 2, (minz + maxz) / 2
|
||||
span = max(maxz - minz, maxy - miny, 0.1)
|
||||
print(f" clip bbox X[{minx:.2f},{maxx:.2f}] Y[{miny:.2f},{maxy:.2f}] Z[{minz:.2f},{maxz:.2f}] span={span:.2f}")
|
||||
|
||||
cam_d = bpy.data.cameras.new("cam"); cam_d.type = 'ORTHO'
|
||||
cam_d.ortho_scale = span * 1.5
|
||||
cam = bpy.data.objects.new("cam", cam_d); bpy.context.collection.objects.link(cam)
|
||||
cam.rotation_mode = 'XYZ'
|
||||
cam.location = (cx + span * 6, cy, cz)
|
||||
cam.rotation_euler = (Vector((cx, cy, cz)) - cam.location).to_track_quat('-Z', 'Y').to_euler()
|
||||
sc.camera = cam
|
||||
print(f" cam {tuple(round(v,2) for v in cam.location)} ortho={cam_d.ortho_scale:.2f}")
|
||||
|
||||
sc.render.engine = 'BLENDER_WORKBENCH'
|
||||
sc.render.resolution_x, sc.render.resolution_y = 300, 460
|
||||
sc.render.image_settings.file_format = 'PNG'
|
||||
sc.display.shading.light = 'FLAT'
|
||||
sc.display.shading.color_type = 'SINGLE'
|
||||
sc.display.shading.single_color = (0.85, 0.32, 0.28)
|
||||
if sc.world is None:
|
||||
sc.world = bpy.data.worlds.new("W")
|
||||
sc.world.color = (0.42, 0.45, 0.48)
|
||||
|
||||
for mode in ("raw", "stripped"):
|
||||
for i, fr in enumerate(frames):
|
||||
sc.frame_set(fr)
|
||||
if mode == "stripped" and hips:
|
||||
hips.rotation_mode = 'QUATERNION'
|
||||
hips.rotation_quaternion = (1, 0, 0, 0)
|
||||
bpy.context.view_layer.update()
|
||||
sc.render.filepath = f"/tmp/shadeswind/tile_{LABEL}_{mode}_{i}.png"
|
||||
bpy.ops.render.render(write_still=True)
|
||||
|
||||
print("DONE " + LABEL)
|
||||
Loading…
Reference in New Issue
Block a user