diff --git a/clothing/skirt_gait_spike/NOTES.md b/clothing/skirt_gait_spike/NOTES.md new file mode 100644 index 0000000..e5877f7 --- /dev/null +++ b/clothing/skirt_gait_spike/NOTES.md @@ -0,0 +1,152 @@ +# Baked-skirt-gait spike — design note + +**Status: SPIKE, not production.** Ozan 2026-08-20: prove that an offline cloth +sim of a skirt against the run cycle can be baked onto the `skirt_*` strand ring +and shipped as a layered animation clip. Script: `bake_skirt_gait.py` (this dir). + +## What it does + +1. Loads `Regular_Female_SkirtRig24.glb` (24 strands × 5 segments) and the long + tapa skirt `Female_Tapa_Skirt_Regular.gltf`, re-pointing the garment's skin at + the body rig. +2. Loads `Jog_Fwd_Loop` from `UAL1.glb` — this is the game's actual `run` clip + (`PlayerController.cs`: UAL1 `Jog_Fwd_Loop` → alias `run`). The pack's other + 119 actions and its mannequin are discarded in-memory. A CYCLES fmodifier + loops it; scene fps is set to 30 *before* import so seconds map to frames. +3. Cloth-sims the garment against the animated body, reusing + `build_pugu_tifi.py`'s idioms (pin group at the waistband, body as collider + 8 mm outer, tension/compression 8, shear 4, bending 0.06, 6 mm contact). + One addition the drape-only pipeline didn't need: **the waistband pin group is + pelvis-weighted and the Armature modifier sits above Cloth**, so the pinned + edge *rides the animated hip* instead of being nailed to world space. + 20 settle frames + 3 looped cycles; the last cycle is captured. +4. Bakes cloth → bones (mapping below), blends the loop seam, writes a + skirt-bones-only action, renders sim-vs-bake proof frames, and exports one GLB + containing one animation with **only `skirt_*` rotation tracks**. + +## The cloth→bone mapping + +- **Rest-pose binning.** Each garment vertex is assigned once, from its rest + position, to a (strand, boundary-knot) cell: strand by azimuth around the + pelvis axis, knot by nearest of the strand's 6 boundary heights (5 segment + heads + hem tail). Same conventions as `skirt_garment_weights.py` — azimuth + and ring heights read off the rig, never bone-name order. +- **Per-frame targets.** For each simulated frame, a cell's world-space centroid + becomes the target position of that strand's boundary knot (in armature + space). ~12 verts/cell on this garment; empty cells fall back to "hang + straight down from the parent joint". +- **Chain fit, rotation-only.** Per strand, per frame: the root head stays glued + to the pelvis-carried rest position (the waistband is pinned there — the cloth + top *is* pelvis motion). Each segment takes the minimal rotation from its + parent-propagated rest orientation that aims head→tail at its target centroid; + twist is inherited down the chain so strands don't spin. The pose-space basis + quaternion comes out analytically + (`basis = (parent_pose @ parent_rest⁻¹ @ rest)⁻¹ @ M_desired`) — no + constraints, no `bake_action`, no per-bone depsgraph updates. +- **No bone scale, no bone translation.** Bone lengths are fixed; cloth + compression shows up as segment curl, never stretch. This keeps the clip + compatible with anything that assumes rigid strand segments (the spring rig's + collision particles included). +- **Loop seam.** Quat sign continuity is enforced per bone, then the last 6 + frames of the cycle are slerp-blended toward frame 0. The game wraps last + key → first (`LoopMode.Linear`), so the seam is smooth by construction. + +## Runtime integration options (for the game side to pick) + +1. **Override per gait (recommended starting point).** When the locomotion state + machine enters run/walk, `SkirtSpringRig` stops simulating and the skeleton + plays the baked `skirt_*` clip on the same bones, synced to (or just + cross-faded with) the body clip. Cheapest possible runtime cost (zero sim), + deterministic silhouette, and the clip loops with the gait. Cost: a crossfade + pop unless blended; no reaction to collisions while overridden. +2. **Additive/under-layer.** Keep the spring sim running but feed the baked pose + as its rest/target (or as a low-weight animated base under the springs). + Keeps collision response, regains baked flair. More machinery: spring sim + needs a "goal pose" input it doesn't currently have. +3. **Replace the sim during gaits only** (hybrid of 1): sim for idle/dance (low + amplitude, collision matters), baked for run/walk/sprint (high amplitude, + where the sim visibly struggles). This matches how the industry uses baked + cloth and is option 1 with a state gate. + +Not evaluated: retargeting one baked clip across bodies. The bake is +body-specific (ring radii are measured per body; the cloth is fitted per body) — +per-body bakes are the honest answer, same doctrine as the garment generator. + +## What breaks / changes with the 16×4 male ring + +Nothing structural — strand/segment counts are read off the rig at runtime, so +the same script bakes 16×4. Three real differences: + +- **Fewer, longer segments** (4 vs 5, non-uniform fracs) → each segment spans + more cloth column; the centroid fit smooths out more fold detail. Expect + slightly stiffer-looking hem on the male ring. +- **Wider strand spacing** (16 vs 24 around the ring) → each strand owns a wider + azimuth wedge; lateral fold corners between strands get more visible. The + `AZ_SPREAD` raised-cosine weighting that fixed piecewise-linear hem folds in + `skirt_garment_weights.py` is the analogous fix on the *weights* side; the + bake itself doesn't care. +- The male long tapa sits on different bodies (Superhero's quads) — the cloth + sim, not the bake, owns that problem. + +## Verified / not verified + +**Verified (run 5, `run5.log`, `renders/`):** the sim holds together at run +amplitude (20 settle + 3 cycles, waistband riding the pelvis, legs pushing the +cloth — `sim_f01`/`sim_f15` show two genuinely different phases, cloth flaring +and lifting over the advancing thigh), and the bake tracks the sim's gross +motion phase-for-phase (`baked_f01` vs `sim_f01` is the same silhouette; the +skirt visibly rides and flares through the cycle, `baked_f01..f28` × 3 views). +No gross leg poke-through in any rendered phase. Exported +`out/SkirtJogFwd.glb`: one animation named `SkirtJogFwd`, 120 channels, +**rotation-only, all `skirt_*` targets**, 0.933 s (one gait cycle), 225 KB. + +**Bake fidelity:** the 24×5 rotation-only centroid fit smooths fine folds — the +baked skirt is lumpier than the raw sim and the hem reads slightly ragged at +peak lift (`baked_f14`). Crumple near the front waistband is the *sim's* own +behavior at the pinned edge, not a bake artifact (visible in `sim_f01` too). + +**Not verified:** in-game playback (the game side has no loader for this yet — +that is integration, out of spike scope); gait transitions; the 16×4 male ring +(same code path, never executed); walk/sprint clips; other garments. + +As-built lessons from the spike runs (full log: `run5.log`): + +- **The shipped garment GLTF has per-face vertices** (flat-shaded; every quad is + its own island). A cloth sim on it shreds into confetti — the script + `remove_doubles`-welds the imported mesh before anything else touches it. +- **Pin per island, not per mesh.** The rope ring's back-rise sits ~5 cm above + the tapa tube's top row; one pin line off the mesh top leaves the tube + unpinned and the whole skirt falls to the floor. Pinning is computed per + connected island (rope island fully pinned, tube top band pinned). +- **Pinned verts must be pelvis-only.** The garment's inherited pelvis/thigh + blend would drag the pinned waistband with the stride; the script zeroes all + other groups on pinned verts. +- **glTF export with `export_force_sampling=True` (the default) exports TRS for + all 185 bones**, not just the animated ones — 555 tracks including the body. + `export_force_sampling=False` + `ACTIVE_ACTIONS` yields exactly 120 tracks, + all `skirt_*` rotations. The exporter also won't name the animation on this + path; the script renames it in the GLB JSON post-export. +- **Blender 5.1 has no `Action.fcurves`** — slotted actions only + (`slots.new` → `layers.new` → `strips.new(type="KEYFRAME")` → + `channelbags.new(slot=...)` → `bag.fcurves.new(..., group_name=...)`), and the + object must bind `action_slot` (same for NLA strips). +- **Never `scene.frame_set` backward into a cached cloth range** to grab a + still — it re-evaluates stale state (run 4 produced two byte-identical + "different phase" sim renders). Sim stills are shot inside the capture loop. + +## Open problems / unknowns + +- **Sync contract.** The clip is one gait cycle keyed 1:1 with the body clip's + cycle. If the game ever plays `run` at a scaled speed, the skirt layer must + scale identically or the cloth will fight the legs. +- **Gait transitions.** Run→walk→idle crossfades of the skirt layer are + unproven; a naive crossfade of two phase-locked clips can momentarily halve + the flare amplitude. +- **Collision deafness during override.** With the spring sim off, nothing + pushes the skirt off world geometry (stairs, sitting). The bake only knows + the legs. +- **Multi-garment.** One clip per (body, garment, gait) — the tapa long/short + and piupiu each need their own bake. Matrix is small today; worth a driver + config if it grows. +- **Wind/idle sway.** Baked clips are dead when the character stands still; + the spring sim (or a third, subtle idle bake) still owns that. diff --git a/clothing/skirt_gait_spike/bake_skirt_gait.py b/clothing/skirt_gait_spike/bake_skirt_gait.py new file mode 100644 index 0000000..c350137 --- /dev/null +++ b/clothing/skirt_gait_spike/bake_skirt_gait.py @@ -0,0 +1,626 @@ +#!/usr/bin/env python3 +"""bake_skirt_gait.py — SPIKE: bake a cloth-simulated skirt onto the skirt_* ring. + +Headless Blender. Simulates the long tapa skirt against a SkirtRig body running +the game's own run cycle (UAL1 Jog_Fwd_Loop == the game's "run" clip), then bakes +the simulated cloth motion onto the skirt_* strand bones and exports ONE looped +cycle as a glTF animation containing ONLY skirt_* rotation tracks — so the game +can layer it over the body clip instead of (or under) the runtime spring sim. + + /Applications/Blender.app/Contents/MacOS/Blender --background \ + --python clothing/skirt_gait_spike/bake_skirt_gait.py -- \ + [--clip Jog_Fwd_Loop] [--cycles 3] [--out clothing/skirt_gait_spike/out] + +Stages: import -> bin -> cloth-sim -> fit chains -> skirt-only action -> renders +(sim A/B + baked turntable) -> export GLB -> verify tracks. See NOTES.md. + +Bake mapping (cloth -> bones), in one paragraph: every garment vertex is binned +ONCE from its REST position into (strand, boundary) cells — strand by azimuth +around the pelvis axis, boundary by nearest ring-knot height, the same +conventions as skirt_garment_weights.py. Per simulated frame each cell's world +centroid becomes the target point for that strand's boundary knot. Bone pose is +then solved analytically down each chain: root head stays glued to the pelvis +(the waistband is pinned there), each segment takes the minimal rotation from +its parent-propagated rest orientation that aims head->tail at its target +centroid, twist carried from the parent so strands don't spin. Rotation-only: +bone lengths never stretch, cloth compression shows up as curl, not scale. +""" + +import argparse +import json +import math +import os +import struct +import sys + +import bpy +from mathutils import Matrix, Vector + +ARGS = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else [] +HERE = os.path.dirname(os.path.abspath(__file__)) +ANIM_ROOT = os.path.dirname(os.path.dirname(HERE)) +GAME = os.environ.get("ARIKI_GAME_ROOT") or os.path.join( + os.path.dirname(ANIM_ROOT), "ariki-game") + +ap = argparse.ArgumentParser() +ap.add_argument("--body", default=os.path.join( + GAME, "assets/quaternius/derived-bodies/Regular_Female_SkirtRig24.glb")) +ap.add_argument("--garment", default=os.path.join( + GAME, "assets/quaternius/outfits/chamorro/Female_Tapa_Skirt_Regular.gltf")) +ap.add_argument("--anim-pack", default=os.path.join( + GAME, "assets/quaternius/anim/UAL1.glb")) +ap.add_argument("--clip", default="Jog_Fwd_Loop") +ap.add_argument("--cycles", type=int, default=3) +ap.add_argument("--settle", type=int, default=20) +ap.add_argument("--fps", type=int, default=30) +ap.add_argument("--out", default=os.path.join(HERE, "out")) +ap.add_argument("--renders", default=os.path.join(HERE, "renders")) +ap.add_argument("--clip-name", default=None, + help="exported animation name (default derived from --clip)") +ap.add_argument("--skip-renders", action="store_true") +ARGS = ap.parse_args(ARGS) + +FPS = ARGS.fps +PIN_BAND = 0.04 # top 4 cm of the garment is the pinned waistband +SEAM_BLEND = 6 # frames at the cycle tail blended toward frame 0 (loop seam) + + +def log(msg): + print(f"[skirt_gait] {msg}", flush=True) + + +def find_armature(): + for o in bpy.data.objects: + if o.type == "ARMATURE": + return o + return None + + +def iter_fcurves(act): + """Blender 5.x: actions are slotted/layered; there is no Action.fcurves.""" + for layer in act.layers: + for strip in layer.strips: + for bag in strip.channelbags: + yield from bag.fcurves + + +def action_frame_range(act): + lo, hi = 1e9, -1e9 + for fc in iter_fcurves(act): + for kp in fc.keyframe_points: + lo = min(lo, kp.co.x) + hi = max(hi, kp.co.x) + return (int(round(lo)), int(round(hi))) + + +def set_active_action(rig, act): + ad = rig.animation_data_create() + ad.action = act + try: + if hasattr(ad, "action_slot") and len(act.slots): + ad.action_slot = act.slots[0] + except Exception as e: + log(f" action_slot bind skipped: {e}") + + +def push_nla(rig, act, name): + ad = rig.animation_data_create() + tr = ad.nla_tracks.new() + tr.name = name + st = tr.strips.new(name, 1, act) + try: + if hasattr(st, "action_slot") and len(act.slots): + st.action_slot = act.slots[0] + except Exception as e: + log(f" nla slot bind skipped: {e}") + return tr + + +def new_keyframe_action(name, rig): + """Create a slotted action bound to `rig`; return (action, fcurve-new).""" + act = bpy.data.actions.new(name) + slot = act.slots.new("OBJECT", name) + layer = act.layers.new("Layer") + strip = layer.strips.new(type="KEYFRAME") + bag = strip.channelbags.new(slot=slot) + ad = rig.animation_data_create() + ad.action = act + ad.action_slot = slot + + def fc_new(data_path, index, action_group): + return bag.fcurves.new(data_path=data_path, index=index, + group_name=action_group) + return act, fc_new + + +def smooth01(x): + x = max(0.0, min(1.0, x)) + return x * x * (3 - 2 * x) + + +# ───────────────────────────────────────────────────────────────────────────── +log(f"body = {ARGS.body}") +log(f"garment = {ARGS.garment}") +log(f"anim = {ARGS.anim_pack} :: {ARGS.clip}") +os.makedirs(ARGS.out, exist_ok=True) +os.makedirs(ARGS.renders, exist_ok=True) + +bpy.ops.wm.read_factory_settings(use_empty=True) +scene = bpy.context.scene +scene.render.fps = FPS # BEFORE the anim import, so times map to frames + +# ── import body ────────────────────────────────────────────────────────────── +bpy.ops.import_scene.gltf(filepath=ARGS.body) +rig = find_armature() +meshes = [o for o in bpy.data.objects if o.type == "MESH"] +body = max(meshes, key=lambda m: len(m.data.vertices)) +log(f"body: rig={rig.name} bones={len(rig.data.bones)} " + f"main mesh={body.name} ({len(body.data.vertices)} verts)") + +# ── ring geometry (armature space, Blender Z-up) ───────────────────────────── +pelvis_bone = rig.data.bones["pelvis"] +pelvis_head = pelvis_bone.head_local.copy() + +strands = {} +for b in rig.data.bones: + if not b.name.startswith("skirt_"): + continue + parts = b.name.split("_") + if len(parts) < 2 or not parts[1].isdigit(): + continue + strands.setdefault(int(parts[1]), []).append(b.name) +for k in strands: + def depth(nm): + d, b = 0, rig.data.bones[nm] + while b.parent and b.parent.name.startswith("skirt_"): + d += 1 + b = b.parent + return d + strands[k] = sorted(strands[k], key=depth) + +K = sorted(strands) +N_SEG = len(strands[K[0]]) +log(f"ring: {len(K)} strands x {N_SEG} segments; strand 0 chain: {strands[K[0]]}") + +knots = {} +azimuths = {} +for k in K: + chain = strands[k] + zs = [rig.data.bones[nm].head_local.z for nm in chain] + zs.append(rig.data.bones[chain[-1]].tail_local.z) + knots[k] = zs + h = rig.data.bones[chain[0]].head_local + azimuths[k] = math.atan2(h.y - pelvis_head.y, h.x - pelvis_head.x) +ring_top = knots[K[0]][0] +log(f"ring top z={ring_top:.3f} hem z={knots[K[0]][-1]:.3f} " + f"knots: {[f'{z:.3f}' for z in knots[K[0]]]}") + +# ── import garment, retarget its skin to the body rig ───────────────────────── +pre = set(bpy.data.objects) +bpy.ops.import_scene.gltf(filepath=ARGS.garment) +new_objs = [o for o in bpy.data.objects if o not in pre] +garment = next(o for o in new_objs if o.type == "MESH") +gar_arm = next((o for o in new_objs if o.type == "ARMATURE"), None) +log(f"garment: {garment.name} ({len(garment.data.vertices)} verts), " + f"carrier armature: {gar_arm.name if gar_arm else None}") +for m in garment.modifiers: + if m.type == "ARMATURE": + m.object = rig +if not any(m.type == "ARMATURE" for m in garment.modifiers): + garment.modifiers.new("Armature", "ARMATURE").object = rig +if gar_arm: + bpy.data.objects.remove(gar_arm, do_unlink=True) +garment.parent = rig + +# WELD: the exported garment has per-face verts (flat-shaded islands of ONE +# quad each — run 3: the cloth sim shredded it into confetti and per-island +# pinning meant nothing). remove_doubles reconnects the tube and the rope as +# two real islands. Weights are (re)assigned after this, nothing to preserve. +import bmesh +bm = bmesh.new() +bm.from_mesh(garment.data) +before = len(bm.verts) +bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=1e-4) +bm.to_mesh(garment.data) +bm.free() +garment.data.update() +log(f"welded garment: {before} -> {len(garment.data.vertices)} verts") + +# Garment rest verts in ARMATURE space (no action assigned yet -> rest pose). +g2r = rig.matrix_world.inverted() @ garment.matrix_world +rest_arm = [g2r @ v.co for v in garment.data.vertices] +top_z = max(p.z for p in rest_arm) +log(f"garment rest z span: {top_z:.3f} .. {min(p.z for p in rest_arm):.3f}") + +# ── bin verts: (strand by azimuth, boundary by nearest knot) — rest positions ── +def nearest_strand(az): + return min(K, key=lambda k: abs((az - azimuths[k] + math.pi) + % (2 * math.pi) - math.pi)) + + +# Pin per ISLAND, not per mesh: the garment is the tapa tube PLUS the separate +# rope ring, and the rope's back-rise rides ~5 cm above the tube's top row. A +# single pin line off the mesh top leaves the tube's top row unpinned and the +# whole skirt falls to the floor (run 2 — sim renders showed only the rope). +adj = [set() for _ in rest_arm] +for e in garment.data.edges: + adj[e.vertices[0]].add(e.vertices[1]) + adj[e.vertices[1]].add(e.vertices[0]) +island_of = [-1] * len(rest_arm) +islands = [] +for i in range(len(rest_arm)): + if island_of[i] >= 0: + continue + idx = len(islands) + stack, members = [i], [] + island_of[i] = idx + while stack: + v = stack.pop() + members.append(v) + for w in adj[v]: + if island_of[w] < 0: + island_of[w] = idx + stack.append(w) + islands.append(members) +pin_line_by_island = {idx: max(rest_arm[v].z for v in members) - PIN_BAND + for idx, members in enumerate(islands)} +log(f"garment islands: {[len(m) for m in islands]} verts, " + f"pin lines: {[f'{z:.3f}' for z in pin_line_by_island.values()]}") + +bins = {} +n_pinned = 0 +pin_idxs = [] +for i, p in enumerate(rest_arm): + az = math.atan2(p.y - pelvis_head.y, p.x - pelvis_head.x) + k = nearest_strand(az) + j = min(range(N_SEG + 1), key=lambda jj: abs(p.z - knots[k][jj])) + if j == 0: + j = 1 # above-ring waistband verts inform the first boundary + bins.setdefault((k, j), []).append(i) + if p.z >= pin_line_by_island[island_of[i]]: + n_pinned += 1 + pin_idxs.append(i) +empty = [(k, j) for k in K for j in range(1, N_SEG + 1) if not bins.get((k, j))] +log(f"bins: {len(bins)} cells, {len(empty)} empty; waistband pin verts: {n_pinned}") + +# ── import the anim pack, keep only the wanted clip ────────────────────────── +pre_actions = set(bpy.data.actions) +bpy.ops.import_scene.gltf(filepath=ARGS.anim_pack) +pack_objs = [o for o in bpy.data.objects if o not in pre and o is not garment] +pack_actions = [a for a in bpy.data.actions if a not in pre_actions] +run_act = next((a for a in pack_actions if a.name == ARGS.clip), None) +if run_act is None: + cand = [a.name for a in pack_actions if ARGS.clip.split("_")[0] in a.name] + raise SystemExit(f"[skirt_gait] FATAL: clip '{ARGS.clip}' not found; " + f"similar: {cand[:10]}") +run_act.use_fake_user = True +f0, f1 = action_frame_range(run_act) +CYCLE = f1 - f0 +log(f"clip '{run_act.name}': frames {f0}..{f1} -> cycle {CYCLE}f @ {FPS}fps " + f"({CYCLE / FPS:.3f}s)") +for fc in iter_fcurves(run_act): + if not any(m.type == "CYCLES" for m in fc.modifiers): + fc.modifiers.new("CYCLES") +for o in pack_objs: + bpy.data.objects.remove(o, do_unlink=True) +for a in pack_actions: + if a is not run_act: + bpy.data.actions.remove(a) + +set_active_action(rig, run_act) +scene.frame_start = 1 +sim_end = ARGS.settle + ARGS.cycles * CYCLE +scene.frame_end = sim_end +log(f"sim: settle {ARGS.settle}f + {ARGS.cycles} cycles = frames 1..{sim_end}") + +# ── cloth setup (build_pugu_tifi.py idioms) ─────────────────────────────────── +# Waistband: pinned, and pelvis-weighted so the pin FOLLOWS the animated hip +# (Armature modifier sits above Cloth; pinned verts track the deformed input). +pin_vg = garment.vertex_groups.get("ClothPin") or garment.vertex_groups.new(name="ClothPin") +pelvis_vg = garment.vertex_groups.get("pelvis") or garment.vertex_groups.new(name="pelvis") +# Pinned verts must ride the pelvis ALONE: zero their other weights first, or +# the inherited thigh blend drags the waistband with the stride. +for vg0 in garment.vertex_groups: + if vg0.name not in ("ClothPin", "pelvis"): + vg0.add(pin_idxs, 0.0, "REPLACE") +pin_vg.add(pin_idxs, 1.0, "REPLACE") +pelvis_vg.add(pin_idxs, 1.0, "REPLACE") +arm_mod = next(m for m in garment.modifiers if m.type == "ARMATURE") +bpy.context.view_layer.objects.active = garment +while garment.modifiers.find(arm_mod.name) > 0: + bpy.ops.object.modifier_move_up(modifier=arm_mod.name) + +body.modifiers.new("Collision", "COLLISION") +body.collision.thickness_outer = 0.008 + +cloth = garment.modifiers.new("Cloth", "CLOTH") +cs = cloth.settings +cs.vertex_group_mass = "ClothPin" +cs.quality = 8 +cs.mass = 0.25 +cs.tension_stiffness = 8.0 +cs.compression_stiffness = 8.0 +cs.shear_stiffness = 4.0 +cs.bending_stiffness = 0.06 +cloth.collision_settings.collision_quality = 4 +cloth.collision_settings.distance_min = 0.006 +cloth.collision_settings.use_self_collision = False + +# ── render rig (set up BEFORE the sim: sim stills are shot DURING the capture +# loop — jumping scene.frame_set BACKWARD into a cached cloth range re-evaluates +# stale state and produced two byte-identical "different phase" stills in run 4) +cam = None +if not ARGS.skip_renders: + cam_data = bpy.data.cameras.new("Cam") + cam = bpy.data.objects.new("Cam", cam_data) + scene.collection.objects.link(cam) + sun_data = bpy.data.lights.new("Sun", "SUN") + sun_data.energy = 3.0 + sun = bpy.data.objects.new("Sun", sun_data) + sun.rotation_euler = (math.radians(50), 0, math.radians(30)) + scene.collection.objects.link(sun) + gnd = bpy.data.meshes.new("Ground") + gnd.from_pydata([(-3, -3, 0), (3, -3, 0), (3, 3, 0), (-3, 3, 0)], [], + [(0, 1, 2, 3)]) + gnd_obj = bpy.data.objects.new("Ground", gnd) + scene.collection.objects.link(gnd_obj) + for eng in ("BLENDER_EEVEE", "BLENDER_EEVEE_NEXT", "BLENDER_WORKBENCH"): + try: + scene.render.engine = eng + break + except Exception: + continue + log(f"render engine: {scene.render.engine}") + scene.render.resolution_x = 640 + scene.render.resolution_y = 760 + world = bpy.data.worlds.new("W") + world.use_nodes = True + world.node_tree.nodes["Background"].inputs[0].default_value = (0.7, 0.7, 0.75, 1) + world.node_tree.nodes["Background"].inputs[1].default_value = 0.6 + scene.world = world + +VIEWS = {"front45": math.radians(30), "side": math.radians(120), + "back45": math.radians(240)} + + +def aim(cam_obj, az): + r, h = 2.6, 1.15 + cam_obj.location = (r * math.cos(az), r * math.sin(az), h) + look = Vector((0, 0, 0.80)) - cam_obj.location + cam_obj.rotation_euler = look.to_track_quat("-Z", "Y").to_euler() + + +def render(tag, f, view): + scene.frame_set(f) + aim(cam, VIEWS[view]) + scene.camera = cam + scene.render.filepath = os.path.join( + ARGS.renders, f"{tag}_f{f - cap0 + 1:02d}_{view}.png") + bpy.ops.render.render(write_still=True) + + +# ── simulate, capturing the LAST cycle ──────────────────────────────────────── +cap0 = sim_end - CYCLE + 1 +frames = list(range(cap0, sim_end + 1)) +SIM_SHOTS = {cap0, cap0 + CYCLE // 2} +log(f"simulating frames 1..{sim_end}; capture {cap0}..{sim_end}") +r2a = rig.matrix_world.inverted() +pelvis_pb = rig.pose.bones["pelvis"] +pelvis_rest_inv = pelvis_bone.matrix_local.inverted() + +targets = [] # [frame][(k,j)] -> Vector (armature space) +pelvis_delta = [] # [frame] -> Matrix (pelvis rest->pose, armature space) +for f in range(1, sim_end + 1): + scene.frame_set(f) + if f < cap0: + continue + deps = bpy.context.evaluated_depsgraph_get() + ev = garment.evaluated_get(deps) + me = ev.to_mesh() + mw = ev.matrix_world + acc = {} + for (k, j), idxs in bins.items(): + s = Vector((0, 0, 0)) + for i in idxs: + s += mw @ me.vertices[i].co + acc[(k, j)] = r2a @ (s / len(idxs)) + ev.to_mesh_clear() + targets.append(acc) + pelvis_delta.append(pelvis_pb.matrix @ pelvis_rest_inv) + if cam is not None and f in SIM_SHOTS: + render("sim", f, "front45") + render("sim", f, "side") + scene.frame_set(f) # render() re-set the frame; stay in sync +log("sim done") + +# ── fit strand chains to the cloth targets (analytic, rotation-only) ───────── +def fit_frame(ti): + acc, d_pel = targets[ti], pelvis_delta[ti] + out = {} + for k in K: + chain = strands[k] + d_par_rot = d_pel.to_3x3() # inherited orientation delta + root_rest = rig.data.bones[chain[0]].matrix_local + parent_chain = d_pel @ root_rest # parent_pose @ parent_rest^-1 @ rest + H = parent_chain.to_translation() # root head carried by pelvis + for s, nm in enumerate(chain): + bone = rig.data.bones[nm] + T = acc.get((k, s + 1)) + if T is None: # empty cell: hang straight down + T = H + d_par_rot @ Vector((0, 0, -(knots[k][s] - knots[k][s + 1]))) + want = T - H + if want.length < 1e-6: + want = d_par_rot @ Vector((0, 0, -1)) + want.normalize() + rest_rot = bone.matrix_local.to_3x3() + O = d_par_rot @ rest_rot + q = (O @ Vector((0, 1, 0))).rotation_difference(want) + R = q.to_matrix() @ O + M = Matrix.Translation(H) @ R.to_4x4() + basis = (parent_chain.inverted() @ M).to_quaternion() + out[nm] = basis + H = H + R @ Vector((0, bone.length, 0)) + d_par_rot = R @ rest_rot.inverted() + parent_chain = M + return out + + +log("fitting bone chains to cloth targets...") +pose_seq = [fit_frame(ti) for ti in range(len(frames))] + +for nm in pose_seq[0]: # quaternion sign continuity + for ti in range(1, len(pose_seq)): + if pose_seq[ti][nm].dot(pose_seq[ti - 1][nm]) < 0: + pose_seq[ti][nm].negate() +for ti in range(len(frames) - SEAM_BLEND, len(frames)): # loop seam blend + w = smooth01((ti - (len(frames) - SEAM_BLEND - 1)) / SEAM_BLEND) + for nm in pose_seq[ti]: + pose_seq[ti][nm] = pose_seq[ti][nm].slerp(pose_seq[0][nm], w) + +# ── write the skirt-only action ─────────────────────────────────────────────── +clip_name = ARGS.clip_name or "Skirt" + "".join( + w.title() for w in ARGS.clip.split("_") if w.lower() != "loop") +log(f"writing action '{clip_name}': {len(pose_seq[0])} bones x {len(frames)} frames") +rig.animation_data_clear() +skirt_act, fc_new = new_keyframe_action(clip_name, rig) +for pb in rig.pose.bones: + if pb.name in pose_seq[0]: + pb.rotation_mode = "QUATERNION" +curves = {} +for nm in pose_seq[0]: + dp = f'pose.bones["{nm}"].rotation_quaternion' + curves[nm] = [fc_new(data_path=dp, index=ch, action_group=nm) for ch in range(4)] +for ti in range(len(frames)): + for nm, fcs in curves.items(): + q = pose_seq[ti][nm] + for ch in range(4): + fcs[ch].keyframe_points.insert(ti + 1, q[ch], options={"FAST"}) +for fcs in curves.values(): + for fc in fcs: + fc.update() + +# ── renders of the BAKED result (the eyeball gate; sim stills were shot +# during the capture loop above) ───────────────────────────────────────────── +if not ARGS.skip_renders: + # ── switch the garment from cloth-sim to SKIRT-RING SKINNING ── + garment.modifiers.remove(cloth) + body.modifiers.remove(next(m for m in body.modifiers if m.type == "COLLISION")) + if "ClothPin" in garment.vertex_groups: + garment.vertex_groups.remove(garment.vertex_groups["ClothPin"]) + # classic 2-strand x 2-segment weights (skirt_garment_weights.py conventions) + garment.vertex_groups.clear() + vg = {nm: garment.vertex_groups.new(name=nm) + for k in K for nm in strands[k]} + vg["pelvis"] = garment.vertex_groups.new(name="pelvis") + BAND = 0.02 + for i, p in enumerate(rest_arm): + if p.z >= ring_top - BAND: + vg["pelvis"].add([i], 1.0, "REPLACE") + continue + az = math.atan2(p.y - pelvis_head.y, p.x - pelvis_head.x) + ks = sorted(K, key=lambda k: abs((az - azimuths[k] + math.pi) + % (2 * math.pi) - math.pi))[:2] + d = abs((azimuths[ks[1]] - azimuths[ks[0]] + math.pi) % (2 * math.pi) - math.pi) + off = (az - azimuths[ks[0]] + math.pi) % (2 * math.pi) - math.pi + frac = min(1.0, max(0.0, abs(off) / d)) if d > 1e-9 else 0.0 + # height -> continuous segment position over the strand's knots + zs = knots[ks[0]] + fseg = float(N_SEG - 1) + for sgi in range(N_SEG - 1): + if p.z > zs[sgi + 1]: + h = zs[sgi] - zs[sgi + 1] + fseg = sgi + (0.0 if h < 1e-9 + else max(0.0, min(1.0, (zs[sgi] - p.z) / h))) + break + i0 = min(N_SEG - 1, int(fseg)) + i1 = min(N_SEG - 1, i0 + 1) + fs = fseg - i0 + for kk, azw in ((ks[0], 1.0 - frac), (ks[1], frac)): + if azw <= 0.0: + continue + for si, sw in ((i0, 1.0 - fs), (i1, fs)): + if sw > 0.0: + vg[strands[kk][si]].add([i], azw * sw, "REPLACE") + # body runs the gait; skirt ring runs the bake (disjoint channels, NLA) + set_active_action(rig, run_act) + push_nla(rig, skirt_act, "skirt_bake") + scene.frame_start = 1 + scene.frame_end = CYCLE + cap0 = 1 # render tag math: actions now keyed at 1..CYCLE + phases = [1, CYCLE // 4, CYCLE // 2, 3 * CYCLE // 4, CYCLE - 2, CYCLE] + for f in phases: + for view in VIEWS: + render("baked", f, view) + log("baked renders done") + +# ── export the skirt-only clip GLB (armature only, active action) ──────────── +# Same scene — a file reload would purge skirt_act with the rest of bpy.data. +# Save the work blend first (scratch/debug), then strip it down to rig+action. +bpy.context.preferences.filepaths.save_version = 0 # working-files rule: no .blend1 +bpy.ops.wm.save_as_mainfile(filepath=os.path.join(ARGS.out, "work.blend")) +ad = rig.animation_data +for tr in list(ad.nla_tracks): + ad.nla_tracks.remove(tr) +set_active_action(rig, skirt_act) +if run_act is not None: + bpy.data.actions.remove(run_act) +for o in [o for o in bpy.data.objects if o is not rig]: + bpy.data.objects.remove(o, do_unlink=True) +for pb in rig.pose.bones: + pb.rotation_mode = "QUATERNION" +scene.frame_start = 1 +scene.frame_end = CYCLE +rig2 = rig +glb_out = os.path.join(ARGS.out, f"{clip_name}.glb") +bpy.ops.object.select_all(action="DESELECT") +rig2.select_set(True) +try: + bpy.ops.export_scene.gltf( + filepath=glb_out, export_format="GLB", use_selection=True, + export_animations=True, export_animation_mode="ACTIVE_ACTIONS", + # sampling exports TRS for EVERY bone (run 3: 555 tracks incl. body); + # direct fcurve export keeps it to the skirt_* rotation tracks only. + export_force_sampling=False, + export_skins=False, export_yup=True) +except TypeError as e: + log(f"export mode fallback: {e}") + bpy.ops.export_scene.gltf( + filepath=glb_out, export_format="GLB", use_selection=True, + export_animations=True, export_skins=False, export_yup=True) +log(f"EXPORTED {glb_out} ({os.path.getsize(glb_out)} bytes)") + +# ── verify the GLB: skirt-only tracks, right duration ───────────────────────── +with open(glb_out, "rb") as fh: + blob = fh.read() +clen, _ = struct.unpack_from(" '{clip_name}'") +log("VERIFY OK: skirt-only tracks") +log("DONE") diff --git a/clothing/skirt_gait_spike/out/SkirtJogFwd.glb b/clothing/skirt_gait_spike/out/SkirtJogFwd.glb new file mode 100644 index 0000000..17e388f --- /dev/null +++ b/clothing/skirt_gait_spike/out/SkirtJogFwd.glb @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d59f56741550120d6ecb12a87a30dd3a010c7038514bdbdf81a9d607ccaf646f +size 229576 diff --git a/clothing/skirt_gait_spike/out/work.blend b/clothing/skirt_gait_spike/out/work.blend new file mode 100644 index 0000000..685ce98 --- /dev/null +++ b/clothing/skirt_gait_spike/out/work.blend @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c57cfc22269782f08c71e9755100e3d4cc5fcf23077dd28981bea6a747b86905 +size 24226769 diff --git a/clothing/skirt_gait_spike/renders/baked_f01_back45.png b/clothing/skirt_gait_spike/renders/baked_f01_back45.png new file mode 100644 index 0000000..dbc5e0e Binary files /dev/null and b/clothing/skirt_gait_spike/renders/baked_f01_back45.png differ diff --git a/clothing/skirt_gait_spike/renders/baked_f01_front45.png b/clothing/skirt_gait_spike/renders/baked_f01_front45.png new file mode 100644 index 0000000..e9f6368 Binary files /dev/null and b/clothing/skirt_gait_spike/renders/baked_f01_front45.png differ diff --git a/clothing/skirt_gait_spike/renders/baked_f01_side.png b/clothing/skirt_gait_spike/renders/baked_f01_side.png new file mode 100644 index 0000000..bcaf161 Binary files /dev/null and b/clothing/skirt_gait_spike/renders/baked_f01_side.png differ diff --git a/clothing/skirt_gait_spike/renders/baked_f07_back45.png b/clothing/skirt_gait_spike/renders/baked_f07_back45.png new file mode 100644 index 0000000..51d2253 Binary files /dev/null and b/clothing/skirt_gait_spike/renders/baked_f07_back45.png differ diff --git a/clothing/skirt_gait_spike/renders/baked_f07_front45.png b/clothing/skirt_gait_spike/renders/baked_f07_front45.png new file mode 100644 index 0000000..0d03e5d Binary files /dev/null and b/clothing/skirt_gait_spike/renders/baked_f07_front45.png differ diff --git a/clothing/skirt_gait_spike/renders/baked_f07_side.png b/clothing/skirt_gait_spike/renders/baked_f07_side.png new file mode 100644 index 0000000..79232fd Binary files /dev/null and b/clothing/skirt_gait_spike/renders/baked_f07_side.png differ diff --git a/clothing/skirt_gait_spike/renders/baked_f14_back45.png b/clothing/skirt_gait_spike/renders/baked_f14_back45.png new file mode 100644 index 0000000..b1f85e1 Binary files /dev/null and b/clothing/skirt_gait_spike/renders/baked_f14_back45.png differ diff --git a/clothing/skirt_gait_spike/renders/baked_f14_front45.png b/clothing/skirt_gait_spike/renders/baked_f14_front45.png new file mode 100644 index 0000000..8de7e9e Binary files /dev/null and b/clothing/skirt_gait_spike/renders/baked_f14_front45.png differ diff --git a/clothing/skirt_gait_spike/renders/baked_f14_side.png b/clothing/skirt_gait_spike/renders/baked_f14_side.png new file mode 100644 index 0000000..fcfe759 Binary files /dev/null and b/clothing/skirt_gait_spike/renders/baked_f14_side.png differ diff --git a/clothing/skirt_gait_spike/renders/baked_f21_back45.png b/clothing/skirt_gait_spike/renders/baked_f21_back45.png new file mode 100644 index 0000000..d2024fb Binary files /dev/null and b/clothing/skirt_gait_spike/renders/baked_f21_back45.png differ diff --git a/clothing/skirt_gait_spike/renders/baked_f21_front45.png b/clothing/skirt_gait_spike/renders/baked_f21_front45.png new file mode 100644 index 0000000..72fd1b4 Binary files /dev/null and b/clothing/skirt_gait_spike/renders/baked_f21_front45.png differ diff --git a/clothing/skirt_gait_spike/renders/baked_f21_side.png b/clothing/skirt_gait_spike/renders/baked_f21_side.png new file mode 100644 index 0000000..6366aff Binary files /dev/null and b/clothing/skirt_gait_spike/renders/baked_f21_side.png differ diff --git a/clothing/skirt_gait_spike/renders/baked_f26_back45.png b/clothing/skirt_gait_spike/renders/baked_f26_back45.png new file mode 100644 index 0000000..3150d06 Binary files /dev/null and b/clothing/skirt_gait_spike/renders/baked_f26_back45.png differ diff --git a/clothing/skirt_gait_spike/renders/baked_f26_front45.png b/clothing/skirt_gait_spike/renders/baked_f26_front45.png new file mode 100644 index 0000000..9edf9e8 Binary files /dev/null and b/clothing/skirt_gait_spike/renders/baked_f26_front45.png differ diff --git a/clothing/skirt_gait_spike/renders/baked_f26_side.png b/clothing/skirt_gait_spike/renders/baked_f26_side.png new file mode 100644 index 0000000..4e839d2 Binary files /dev/null and b/clothing/skirt_gait_spike/renders/baked_f26_side.png differ diff --git a/clothing/skirt_gait_spike/renders/baked_f28_back45.png b/clothing/skirt_gait_spike/renders/baked_f28_back45.png new file mode 100644 index 0000000..a2ca627 Binary files /dev/null and b/clothing/skirt_gait_spike/renders/baked_f28_back45.png differ diff --git a/clothing/skirt_gait_spike/renders/baked_f28_front45.png b/clothing/skirt_gait_spike/renders/baked_f28_front45.png new file mode 100644 index 0000000..53e2a1f Binary files /dev/null and b/clothing/skirt_gait_spike/renders/baked_f28_front45.png differ diff --git a/clothing/skirt_gait_spike/renders/baked_f28_side.png b/clothing/skirt_gait_spike/renders/baked_f28_side.png new file mode 100644 index 0000000..694c740 Binary files /dev/null and b/clothing/skirt_gait_spike/renders/baked_f28_side.png differ diff --git a/clothing/skirt_gait_spike/renders/sim_f01_front45.png b/clothing/skirt_gait_spike/renders/sim_f01_front45.png new file mode 100644 index 0000000..265890c Binary files /dev/null and b/clothing/skirt_gait_spike/renders/sim_f01_front45.png differ diff --git a/clothing/skirt_gait_spike/renders/sim_f01_side.png b/clothing/skirt_gait_spike/renders/sim_f01_side.png new file mode 100644 index 0000000..db0b9bd Binary files /dev/null and b/clothing/skirt_gait_spike/renders/sim_f01_side.png differ diff --git a/clothing/skirt_gait_spike/renders/sim_f15_front45.png b/clothing/skirt_gait_spike/renders/sim_f15_front45.png new file mode 100644 index 0000000..b1248b6 Binary files /dev/null and b/clothing/skirt_gait_spike/renders/sim_f15_front45.png differ diff --git a/clothing/skirt_gait_spike/renders/sim_f15_side.png b/clothing/skirt_gait_spike/renders/sim_f15_side.png new file mode 100644 index 0000000..737afd3 Binary files /dev/null and b/clothing/skirt_gait_spike/renders/sim_f15_side.png differ diff --git a/clothing/skirt_gait_spike/run5.log b/clothing/skirt_gait_spike/run5.log new file mode 100644 index 0000000..ef6b782 --- /dev/null +++ b/clothing/skirt_gait_spike/run5.log @@ -0,0 +1,81 @@ +ℹ️ blendkit: Verbose is enabled [14:15:34.159, addon_updater.py:150] +ℹ️ blendkit: Read in JSON settings from file [14:15:34.160, addon_updater.py:150] +ℹ️ blendkit: Ports reordered so first port is now 62485 (previous index was 0) [14:15:34.162, client_lib.py:156] +BlenderMCP: cannot start server in background mode (blender -b) - commands would never execute +BlenderMCP: run Blender with a GUI, or use a virtual display: xvfb-run -a blender +BlenderMCP addon registered +Created history step 74546992-0996-484d-9ad9-72034ae322a5 +ℹ️ blendkit: Created new search history step [14:15:34.166, search.py:2698] +ℹ️ blendkit: No temporarily enabled addons to clean up [14:15:34.166, download.py:419] +[skirt_gait] body = /Users/behcetozanbozkurt/Documents/tinqs-ltd/ariki-game/assets/quaternius/derived-bodies/Regular_Female_SkirtRig24.glb +[skirt_gait] garment = /Users/behcetozanbozkurt/Documents/tinqs-ltd/ariki-game/assets/quaternius/outfits/chamorro/Female_Tapa_Skirt_Regular.gltf +[skirt_gait] anim = /Users/behcetozanbozkurt/Documents/tinqs-ltd/ariki-game/assets/quaternius/anim/UAL1.glb :: Jog_Fwd_Loop +ℹ️ blendkit: Unregistering Blendkit add-on [14:15:34.174, __init__.py:2973] +ℹ️ blendkit: No temporarily enabled addons to clean up [14:15:34.174, download.py:419] +BlenderMCP server stopped +BlenderMCP addon unregistered +14:15:34 | INFO: Data are loaded, start creating Blender stuff +14:15:34 | INFO: Blender create Mesh node Face +14:15:34 | INFO: Blender create Mesh node Face.001 +14:15:34 | INFO: Blender create Mesh node Female_Regular.001 +14:15:34 | INFO: glTF import finished in 0.07s +[skirt_gait] body: rig=Armature bones=185 main mesh=Female_Regular (7310 verts) +[skirt_gait] ring: 24 strands x 5 segments; strand 0 chain: ['skirt_00', 'skirt_00_01', 'skirt_00_02', 'skirt_00_03', 'skirt_00_04'] +[skirt_gait] ring top z=0.952 hem z=0.523 knots: ['0.952', '0.899', '0.829', '0.733', '0.628', '0.523'] +14:15:34 | INFO: Data are loaded, start creating Blender stuff +14:15:34 | INFO: Blender create Mesh node Female_Tapa_Skirt_Regular +14:15:34 | INFO: glTF import finished in 0.04s +[skirt_gait] garment: Female_Tapa_Skirt_Regular (2304 verts), carrier armature: Armature.001 +[skirt_gait] welded garment: 2304 -> 612 verts +[skirt_gait] garment rest z span: 1.119 .. 0.403 +[skirt_gait] garment islands: [396, 216] verts, pin lines: ['1.025', '1.079'] +[skirt_gait] bins: 120 cells, 0 empty; waistband pin verts: 184 +14:15:34 | INFO: Data are loaded, start creating Blender stuff +14:15:35 | INFO: Blender create Mesh node Mannequin +14:15:37 | INFO: glTF import finished in 2.48s +[skirt_gait] clip 'Jog_Fwd_Loop': frames 0..28 -> cycle 28f @ 30fps (0.933s) +[skirt_gait] sim: settle 20f + 3 cycles = frames 1..104 +[skirt_gait] render engine: BLENDER_EEVEE +/Users/behcetozanbozkurt/Documents/tinqs-ltd/animation/clothing/skirt_gait_spike/bake_skirt_gait.py:377: DeprecationWarning: 'World.use_nodes' is expected to be removed in Blender 6.0 + world.use_nodes = True +[skirt_gait] simulating frames 1..104; capture 77..104 +00:08.729 render | Saved: '/Users/behcetozanbozkurt/Documents/tinqs-ltd/animation/clothing/skirt_gait_spike/renders/sim_f01_front45.png' +00:09.112 render | Saved: '/Users/behcetozanbozkurt/Documents/tinqs-ltd/animation/clothing/skirt_gait_spike/renders/sim_f01_side.png' +00:10.202 render | Saved: '/Users/behcetozanbozkurt/Documents/tinqs-ltd/animation/clothing/skirt_gait_spike/renders/sim_f15_front45.png' +00:10.606 render | Saved: '/Users/behcetozanbozkurt/Documents/tinqs-ltd/animation/clothing/skirt_gait_spike/renders/sim_f15_side.png' +[skirt_gait] sim done +[skirt_gait] fitting bone chains to cloth targets... +[skirt_gait] writing action 'SkirtJogFwd': 120 bones x 28 frames +00:11.817 render | Saved: '/Users/behcetozanbozkurt/Documents/tinqs-ltd/animation/clothing/skirt_gait_spike/renders/baked_f01_front45.png' +00:12.209 render | Saved: '/Users/behcetozanbozkurt/Documents/tinqs-ltd/animation/clothing/skirt_gait_spike/renders/baked_f01_side.png' +00:12.622 render | Saved: '/Users/behcetozanbozkurt/Documents/tinqs-ltd/animation/clothing/skirt_gait_spike/renders/baked_f01_back45.png' +00:13.031 render | Saved: '/Users/behcetozanbozkurt/Documents/tinqs-ltd/animation/clothing/skirt_gait_spike/renders/baked_f07_front45.png' +00:13.427 render | Saved: '/Users/behcetozanbozkurt/Documents/tinqs-ltd/animation/clothing/skirt_gait_spike/renders/baked_f07_side.png' +00:13.830 render | Saved: '/Users/behcetozanbozkurt/Documents/tinqs-ltd/animation/clothing/skirt_gait_spike/renders/baked_f07_back45.png' +00:14.231 render | Saved: '/Users/behcetozanbozkurt/Documents/tinqs-ltd/animation/clothing/skirt_gait_spike/renders/baked_f14_front45.png' +00:14.627 render | Saved: '/Users/behcetozanbozkurt/Documents/tinqs-ltd/animation/clothing/skirt_gait_spike/renders/baked_f14_side.png' +00:15.023 render | Saved: '/Users/behcetozanbozkurt/Documents/tinqs-ltd/animation/clothing/skirt_gait_spike/renders/baked_f14_back45.png' +00:15.416 render | Saved: '/Users/behcetozanbozkurt/Documents/tinqs-ltd/animation/clothing/skirt_gait_spike/renders/baked_f21_front45.png' +00:15.804 render | Saved: '/Users/behcetozanbozkurt/Documents/tinqs-ltd/animation/clothing/skirt_gait_spike/renders/baked_f21_side.png' +00:16.211 render | Saved: '/Users/behcetozanbozkurt/Documents/tinqs-ltd/animation/clothing/skirt_gait_spike/renders/baked_f21_back45.png' +00:16.612 render | Saved: '/Users/behcetozanbozkurt/Documents/tinqs-ltd/animation/clothing/skirt_gait_spike/renders/baked_f26_front45.png' +00:17.011 render | Saved: '/Users/behcetozanbozkurt/Documents/tinqs-ltd/animation/clothing/skirt_gait_spike/renders/baked_f26_side.png' +00:17.415 render | Saved: '/Users/behcetozanbozkurt/Documents/tinqs-ltd/animation/clothing/skirt_gait_spike/renders/baked_f26_back45.png' +00:17.822 render | Saved: '/Users/behcetozanbozkurt/Documents/tinqs-ltd/animation/clothing/skirt_gait_spike/renders/baked_f28_front45.png' +00:18.215 render | Saved: '/Users/behcetozanbozkurt/Documents/tinqs-ltd/animation/clothing/skirt_gait_spike/renders/baked_f28_side.png' +00:18.611 render | Saved: '/Users/behcetozanbozkurt/Documents/tinqs-ltd/animation/clothing/skirt_gait_spike/renders/baked_f28_back45.png' +[skirt_gait] baked renders done +Info: Saved as "work.blend" +INFO Draco mesh compression is available, use library at /Applications/Blender.app/Contents/Resources/5.1/scripts/addons_core/io_scene_gltf2/libextern_draco.dylib +14:15:52 | INFO: Starting glTF 2.0 export +14:15:52 | INFO: Finished glTF 2.0 export in 0.15310096740722656 s + +[skirt_gait] EXPORTED /Users/behcetozanbozkurt/Documents/tinqs-ltd/animation/clothing/skirt_gait_spike/out/SkirtJogFwd.glb (229572 bytes) +[skirt_gait] verify: animations=['Animation'] tracks=120 dur=0.933s non-skirt targets=[] +[skirt_gait] renamed animation -> 'SkirtJogFwd' +[skirt_gait] VERIFY OK: skirt-only tracks +[skirt_gait] DONE +Blender 5.1.2 (hash ec6e62d40fa9 built 2026-05-19 01:30:33) + +Blender quit +exit=0