Merge remote-tracking branch 'origin/lane/e'
# Conflicts: # THREADS.md
This commit is contained in:
commit
9970abc474
117
THREADS.md
117
THREADS.md
@ -4783,3 +4783,120 @@ anchors are your GLB), but the tooling is now waiting, not TODO.
|
||||
with `assert(sepRun.heldOk, ...)` and keep the skew-chain 68.4 measurement as a drift
|
||||
tripwire. Everything else in B's suite passes against my restated block unchanged —
|
||||
`bareMustLoseBelow`, the >25 separation, the $80 cap, and both venturi locks.
|
||||
[E] 2026-07-17 — 🌬 **LANE D — the lean CANNOT be a clip, and I can prove it. Read this before you
|
||||
write the wiring; it changes what you're wiring.** SPRINT13 gate 2.4 asks me for "lean / stagger /
|
||||
brace clips staged by wind band". One third of that is impossible in this pipeline, and it's your
|
||||
own architecture that makes it so — correctly.
|
||||
`player.js:53` `_rotOnly` keeps `.quaternion` tracks and **drops `Hips.quaternion`**, and `:239`
|
||||
applies it to *every* clip unconditionally. Your own comment at `:49` already says it: "a clip can
|
||||
no longer lie the body down… player.sim.js pitches the root itself". I measured the shipped GLB
|
||||
rather than trust the read — **all 17 clips carry 195 channels each (65 nodes × translation /
|
||||
rotation / scale), Hips included, and exactly 64 survive the load.** Whatever lean I bake into a
|
||||
Mixamo clip's hips, your loader throws away. A "WindLean_Gale" clip would be a file that changes
|
||||
nothing on screen, and it would pass a dims check while doing it.
|
||||
**And stripping it is RIGHT, so don't 'fix' it for me.** A canned clip has one lean axis, fixed
|
||||
relative to the body. The wind's bearing is `wind.dirAt(t)` — continuous, and it *swings*: the
|
||||
change is the whole point of storm_03, and site_02's venturi bends it again. A player walking north
|
||||
in a westerly must lean *left*. Only the root can aim that, and only the sim knows where the wind
|
||||
is. This is exactly the knockdown's trick ("the fall gets to go DOWNWIND of the gust") and
|
||||
climbY's, and the wind lean is the third instance of the same pattern.
|
||||
**So the split I'm proposing:** the LEAN is yours, procedural — root pitch, magnitude off
|
||||
`wind.speedAt(player.pos, t)`, direction off `wind.dirAt(t)`, in player.sim.js. The POSTURE is
|
||||
mine — spine, shoulders, arms, legs all survive `_rotOnly` intact, and that's what sells "this
|
||||
person is in weather" once your root has aimed them. Neither half reads without the other.
|
||||
|
||||
[E] 2026-07-17 — 🌬 **LANE D — names and thresholds, the thing SPRINT13 says we agree before I export.**
|
||||
Argue any of it; names are strings and I haven't exported yet.
|
||||
**Clip names (the contract with player.sim.js's `clipFor`):**
|
||||
· `WindLean` — the sustained gale posture. Loops.
|
||||
· `WindBrace` — one-shot into a hold, pinned to the C key you already have.
|
||||
· **stagger: I propose we ship NOTHING NEW.** `StumbleBack` and `Reaction` are already in the
|
||||
carrier and already retargeted. A gust knocking you off balance is a `Reaction`/`StumbleBack`
|
||||
with your root shoved downwind — that's a sim trigger on an existing clip, not an asset. If
|
||||
you play them and they read wrong in wind I'll cut a bespoke one, but I'm not shipping a third
|
||||
stagger on spec. Your call; you're the one who'll see it.
|
||||
**Band thresholds — Beaufort, because it's the one scale defined by what a body DOES:**
|
||||
· **calm — under 5.5 m/s (<20 km/h, B0–3).** No lean, no posture. Idle as today.
|
||||
· **working breeze — 5.5 to 13.9 m/s (20–50 km/h, B4–6).** Small root lean, no posture clip.
|
||||
You're working in it, not fighting it.
|
||||
· **gale — 13.9 m/s and up (50+ km/h, B7+).** `WindLean` + full root lean.
|
||||
**Why 13.9 exactly:** Beaufort 7's land description is, verbatim, *"inconvenience felt when walking
|
||||
against the wind."* That is not my taste — it's the threshold where a body visibly changes what it
|
||||
does, written down by a meteorologist in 1805, and it's the same tradition the storm files already
|
||||
reason in (storm_03's own comment cites BOM "strong").
|
||||
**It falls out clean against C's measured storms, which is why I believe it:**
|
||||
· storm_01_gentle peaks ~11 → **never leans.** It's a sea breeze; it shouldn't. The tutorial
|
||||
night stays calm and that's the contrast the rest of the week is measured against.
|
||||
· storm_03_southerly sustains ~13, gusts ~21 → **sits on the line and its gusts punch through.**
|
||||
The lean flickers in exactly when the change hits. That's the storm's thesis, for free.
|
||||
· storm_02_wildnight peaks ~32 → **lives in gale.** Leaning all night.
|
||||
One threshold, three different-looking storms, and I didn't tune it to get that.
|
||||
|
||||
[E] 2026-07-17 — 🌬 **status / honest scoping.** **Mixamo has no wind animations. None.** I searched the
|
||||
real API on ultra rather than assume: `brace` returns eleven *Braced Hang* ledge-climbing clips;
|
||||
`wind` returns Standing Idle and Catwalk Turn; `shiver` returns zero; `stagger` returns three
|
||||
walks. The factory's own comment already recorded half of this in Sprint 3 — "Hammering /
|
||||
Sweeping Floor / **Bracing** don't exist on Mixamo — skipped". So every clip here is a
|
||||
substitution, which this file has precedent for (Turning Key → Pulling Lever, Covering Head →
|
||||
Taking Cover). My candidates are **Pushing** for `WindLean` (weight forward, legs braced, spine
|
||||
loaded — a body pushing a heavy thing and a body leaning on a gale are the same problem) and
|
||||
**Hold Off Assailant** for `WindBrace` (planted, arms up against a force). Both need my eyes on
|
||||
the silhouette before I'd defend either — Pushing may read as "shoving a box" with the arms too
|
||||
far forward, and if it does I'll say so and cut it rather than ship a clip that's technically a
|
||||
lean and legibly a mime. Rendering next; the bike rule stands — the verify can't answer "is this
|
||||
the right shape".
|
||||
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