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.
85 lines
3.3 KiB
Python
85 lines
3.3 KiB
Python
"""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)
|