From 60d1d674f9a941ac2adee6691aa23e3979a8a50d Mon Sep 17 00:00:00 2001 From: firejune Date: Wed, 21 Oct 2026 00:43:05 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20step=209c=20=E2=80=94=20the=20side=20lo?= =?UTF-8?q?cks=20and=20the=20skirt=20answer=20the=20world,=20through=20the?= =?UTF-8?q?=20producer's=20cloth=20recipe=20(#12)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The walkthrough's build declared no constraints: the hair and the skirt moved only where the idle or the hello keyed them. Step 9c writes a `cloth` block (bottomwear: 3 chains x 3 links with a swing; front hair: 1 chain x 3 links, edge strength 100), runs `rig-parts propose --cloth --write-config`, and rebuilds with `--loop --lead-in 1`: - the recipe drafts 27 physics + 3 transform constraints (the two side locks -> lockt0_/lock1t0_, the fringe, skirt0_..2_ + skirt_swing under hip); bones 70 -> 46; their idle keys are removed; the bun (back hair above the shoulders) is not reached and keeps its key - hello.ts gains --no-skirt-keys: in 9c the skirt is left to physics (without physics the keys are still needed: thigh_l|bottomwear 48 px) - two corrections the draft needs on this example, shown as commands: the bun track's amplitude 0.8 -> 1 (CHECK_CHAIN_LAG reads neck > head_ctl > bun_ctl as one chain once the siblings' keys are gone) and lock edge strength 100 (default 50 ghosts 122 px at the head turn) - bars on idle and hello with physics on: all 15 seam pairs within gap <= 25 / ghost <= 50 (closest face|hair_front 7/47), pass-through 0/81 and 0/97, ANIMATIONS_DO_NOT_FOLD PASS, check PASS, build PASS, sleeve_r0 4.00 deg; CLOTH_SETTLES WARN (skirt trees settle in 1.8-2.0 s, hair does not) reported as is - README as written reproduces from a clean folder on parts 1.49.1 (published mid-task) byte-identical to the 1.48.0 build measured; check-adapters runs 9c and asserts the constraints Closes #12 Co-Authored-By: Roa <342598975+fj-roa[bot]@users.noreply.github.com> --- README.md | 64 +++++++++++++++++- docs/REFERENCE.md | 138 +++++++++++++++++++++++++++++++++++++- scripts/check-adapters.ts | 13 +++- scripts/hello.ts | 20 +++--- 4 files changed, 222 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index f763faf..9747bbf 100644 --- a/README.md +++ b/README.md @@ -221,6 +221,67 @@ painting the upper arm and its puff sleeve are one image under a sleeveless top, swinging it out, as step 9's wave does, opens a gap at the armhole. The comment at the top of `hello.ts` says why each limit is where it is. +### 9c. Let the hair and the skirt answer + +So far the hair and the skirt move only where a key moves them. The parts package has a +recipe that hands them to physics instead: a `cloth` block in the config names a recipe per +See-through layer tag, `propose --cloth` drafts chains with a physics constraint on each +link, and the build carries them. Then what moves under them, her breath, her head and the +hello's weight shift, moves them, and no key does. + +```sh +bun ../getting-started/scripts/hello.ts out/rig/rig.json --no-skirt-keys +cat > work/cloth.json <<'EOF' +{ + "cloth": { + "bottomwear": { "chains": 3, "links": 3, "wind": [1, 6], "gravity": [2, 12], "translate": 0.2, + "swing": { "scales": [1.2, 1, 1.2] } }, + "front hair": { "chains": 1, "links": 3, "wind": [1, 6], "gravity": [2, 12], "translate": 0.2, + "strength": { "trunk": 100, "branch": 100, "edge": 100 } } + } +} +EOF +bun ../getting-started/scripts/take.ts work/cloth.json config.json cloth +bunx rig-parts propose --parts work/rig --source inputs/painting.png --out work --keypoints keypoints.json --cloth config.json --write-config config.cloth.json +mv config.cloth.json config.json +bun -e 'const f = "config.json", c = await Bun.file(f).json(); c.motion.tracks.find((t) => t.bone === "bun").amp = 1; await Bun.write(f, JSON.stringify(c, null, 2) + "\n")' +bunx rig-parts build --config config.json --source inputs/painting.png --full inputs/layers/full --head inputs/layers/head --out out --loop --lead-in 1 +``` + +About two and a half minutes. The first line writes the hello again without its three skirt +keys (`hello.ts` then prints `13 tracks of 97 keys` and `skirt left to physics`): step 9b's +settle turns the skirt's top links by hand to follow her hip, and from here physics does +that. `bottomwear` is the skirt's layer; `front hair` is the hair around the face, the +fringe and the two locks that hang beside her cheeks, all one layer. +`propose` prints what the recipe drafted: `3 tree(s) on one shared swing, skirt_swing` for the +skirt, one tree of three links on each side lock (`lockt0_` and `lock1t0_`), and the fringe's +three short chains on physics too. Then a line beginning `wrote config.cloth.json: +config.json's text with` names what it replaced — the old hair and skirt chains removed and +the new ones inserted, `meshes.hair_front`, `meshes.bottomwear`, their idle tracks removed, +`constraints` — and ends `every other byte as read`: everything else in your config, +`motion.animations_from` included, is as it was, and `mv` makes the result your config. It holds 30 constraints: 27 +physics constraints, one on every link, and 3 that turn each skirt tree's first link with +the swing hung from the hip. + +Two lines are corrections to the draft. The `strength` line makes the side locks as stiff as +the fringe's middle: at the recipe's default the locks trail her head's turn in the hello +far enough to pull off her cheek by more than this guide allows (the figures are in +[docs/REFERENCE.md](docs/REFERENCE.md#step-9c-the-hair-and-the-skirt)). The `bun -e` line raises one number. The +recipe took the keys off the hair around the face, which leaves the back hair's bun the +head's only keyed child, so `check` reads neck, head and bun as one chain, and wants each to +swing at least as far as the one above. The draft gives the bun 0.8 degrees under the head's +0.9; without the line the build stops at `CHECK_CHAIN_LAG`. + +From the build you should see `loop physics: 27 physics constraint(s) left out of the loop's +frames, reported, not judged`, four `WARN chain` lines from `CLOTH_SETTLES` for the skirt, +`check: PASS; 11 of 12 bar(s) measured, 1 skipped (STRESS_OFF_CHAIN); 5 warning(s) +(SEAM_OVER_MOTION, CLOTH_SETTLES, not a bar)`, a `lead-in:` line ending `max |d| 0, 0 px over +40 — the jump at the wrap`, and `build: PASS`. Physics starts at rest when an animation +starts, and the skirt hangs into place within 2 seconds. Then it rests, because the idle +never moves her hip; `CLOTH_SETTLES` says so, and it is a warning, not a failure. The hello's +weight shift moves the skirt again. `--lead-in 1` plays the idle once before +`out/idle.gif` is cut from it, so the loop starts after the skirt has settled. + ### 10. Play it in your browser ```sh @@ -231,7 +292,8 @@ Open ; the menu switches animations. Ctrl+C stops the server. That's the whole chain. The page is diff --git a/docs/REFERENCE.md b/docs/REFERENCE.md index 21b55da..1625a12 100644 --- a/docs/REFERENCE.md +++ b/docs/REFERENCE.md @@ -10,7 +10,7 @@ quoted from. Three words recur: ## Tested with -Recorded 2026-10-13; *Try it first* re-run twice on 2026-10-20, the second time with step 9b. Everything marked *measured* on this +Recorded 2026-10-13; *Try it first* re-run twice on 2026-10-20, the second time with step 9b, and once more on 2026-10-21 with step 9c. Everything marked *measured* on this page was run on this machine with these versions. | | | @@ -20,8 +20,8 @@ page was run on this machine with these versions. | Bun | 1.4.2 | | uv | 0.12.17, with the Python 3.12.14 it chose | | Browser | Chromium 152 (the built-in browser of the Claude desktop app), meshes drawn through WebGL (2026-10-13) | -| `@rig-c/parts` | 1.48.0 (2026-10-20, with step 9b; 1.41.1 earlier that day; 1.16.0 on 2026-10-13) | -| `rig-c` | 2.64.1 (2026-10-20, with step 9b; 2.61.1 earlier that day; 2.48.0 on 2026-10-13) | +| `@rig-c/parts` | 1.49.1 (2026-10-21, with step 9c; step 9c's readings were taken on 1.48.0's build, byte-identical) · 1.48.0 (2026-10-20, with step 9b; 1.41.1 earlier that day; 1.16.0 on 2026-10-13) | +| `rig-c` | 2.64.1 (2026-10-20 and 2026-10-21, with steps 9b and 9c; 2.61.1 earlier that day; 2.48.0 on 2026-10-13) | | `@rig-c/pose` | 0.37.0 (2026-10-20, with step 9b; 0.35.0 earlier that day; 0.1.1 on 2026-10-13) | | `@rig-c/play` | 1.12.0 (2026-10-20, both runs; 1.9.1 on 2026-10-13) | | Spine editor | Spine 4.3.26 Professional (launcher 4.3.08), driven from the command line only; see *Use what you made* | @@ -54,6 +54,16 @@ and `skeleton.model.json`). Step 10 was checked by fetching the page and both mo `serve.ts` (HTTP 200), and the `&under=idle` address was opened once in headless Chromium: `idle` on track 0, `hello` on track 1, both looping, the page's message line empty. +On 2026-10-21, with step 9c added, every `sh` block was extracted and run verbatim in order +from an empty folder once more (run 6), `git clone` again replaced by a copy of the checkout; +`bun add` installed parts 1.49.1, published that day, with rig-c 2.64.1, pose 0.37.0 and play +1.12.0. Every expected line on the README held. Step 9c's outputs (`hello.anim.json`, +`config.json`, the build's `skeleton.json`, `skeleton.model.json` and `skeleton.png`, and +`out/idle.gif`) were byte-identical to the scratch run on parts 1.48.0 whose build the +readings under *Step 9c* were taken on. One line's wording differs between the two: 1.49.1's +`wrote config.cloth.json:` names each chain and track it removed and inserted, where 1.48.0 +named the sections. Step 10 was checked by fetching the page and both models (HTTP 200). + *Try it first* (the CPU-only half) was run end to end twice on the machine above: once command by command while drafting, and once as a smoke, every `sh` block of the finished page extracted and run verbatim in order from an empty folder with an empty Bun cache. The @@ -91,6 +101,7 @@ Two runs; where they differ, both are given. The README's step numbers are the l | — look | `rigc inspect`, `rigc render` (under *Where to go next*) | 1.9 + 2.3 s | 2.8 s | not run | not run | | 9 wave | `wave.ts`, `clean`, map heredoc, `rig-pose retarget --build` | 0.09 + 6.0 s | 5.7 s | 5.2 s | 6.1 s | | 9b hello | `hello.ts`, the `animations_from` line, `take.ts`, `rig-parts build --loop` | — | — | — | 80.6 s | +| 9c cloth | `hello.ts --no-skirt-keys`, the `cloth` heredoc, `take.ts`, `propose --cloth --write-config`, `mv`, the `bun -e` line, `rig-parts build --loop --lead-in 1` | — | — | — | — (run 6: 153.5 s, under load) | | 10 play | `serve.ts` | played in Chromium 152; meshes on WebGL | played; 37 requests, last at 242 ms | page and model HTTP 200 | page and both models HTTP 200 | Run 2 total: **176 s**. Run 3 total: **166 s** wall clock, first-time downloads included. Run @@ -98,6 +109,10 @@ Run 2 total: **176 s**. Run 3 total: **166 s** wall clock, first-time downloads smoke took 580 s, but the machine's load average was 75–87 while it ran (other jobs), so its times are not given. The download in step 2 is 41 MB. +Run 6 (clean, 2026-10-21, caches warm, the machine's load average 13–16 from other jobs, so +an upper bound): **495.5 s** in all — steps 1 to 10 took 1.3, 5.3, 4.3, 8.5, 63.7, 4.3, 8.6, +101.5, 16.6, 122.7 (9b), 153.5 (9c) and 2.7 s. + Disk after run 2: `my-rig` 79 MB (`node_modules` 37, `inputs` 19, `work` 4.3, `out` 16, `wave-build` 1.6) and Bun's download cache 38 MB — about 120 MB in all. Bun reported `7 packages installed`. @@ -198,6 +213,122 @@ frames for both thighs (0 of 81 under the idle). `sleeve_r0` against `chest`: `&under=idle` plays it — `rigc render` reads one animation at a time. The idle adds its breath on `chest_ctl` and its sway on the left sleeve beneath the hello's own keys. +### Step 9c, the hair and the skirt + +The recipe is the parts package's *Recipe: cloth and hair that answer the world* (stated by +the parts [AUTHORING][authoring]): the config's `cloth` block holds one recipe per +See-through tag, `propose --cloth` drafts it, and `--write-config` writes the config back +"with what the recipe drafts spliced in", replacing `bones` and `constraints` whole and +keeping every other byte, `motion.animations_from` among them. On this painting the locks +beside the face are in the `front hair` layer, with the fringe (the plan's `hair_front` +part, `head:front hair`); the back hair's `bun` is a single bone, and a `back hair` recipe on +hair that ends above the shoulders is refused (`PROPOSE_CLOTH_DRAFTED`), so the bun keeps its +idle key. + +**What the recipe drafted** (parts 1.48.0, measured): the keyed chains `lock`, `lock1_` and +`skirt_r`, `skirt_c`, `skirt_l` and their idle tracks are replaced; the fringe's chains keep +their names and places. The rig goes from 70 bones to 46 (the keyed chains' `_ctl` bones go). + +| piece | chains | constraints | +| --- | --- | --- | +| side locks | `lockt0_` and `lock1t0_`, 3 links each, from `head` | `_physics` on every link (rotate), `_translate` on the last: 8 | +| fringe | `bang_r`, `bang_c`, `bang_l`, 2 links each, as drafted in step 7 | the same: 9 | +| skirt | `skirt0_`, `skirt1_`, `skirt2_`, 3 links each, and `skirt_swing`, one link under `hip` | `skirt_swing_physics`; `skirt_swing_0`..`_2` (transform: each tree's first link turns with the swing, times 1.2, 1, 1.2); `_physics` on links 1 and 2 and `_translate` on link 2 of each tree: 13 | + +27 physics constraints and 3 transform constraints, 30 in all; `wind` 1 to 6 and `gravity` +2 to 12 from root to tip, multipliers of the world the player sets. The step-10 page sets +none, so the runtime's default world applies: wind (1, 0), gravity (0, 1) (stated by +`rigc render --help`, *--environment*). + +**Two corrections to the draft, both measured on this painting:** + +- **The side locks' `strength`.** At the recipe's default (`edge` 50; one tree is its own + edge) the locks trail the hello's head turn: `face|hair_front` reads a ghost of 122 px at + hello frame 8 (t 0.333 s), past the bar of 50. With `edge` 100 it reads 47. +- **The bun's idle amplitude.** With the locks', fringe's and skirt's tracks gone, `bun` is + the head's only keyed child, and `CHAIN_LAG` reads `neck > head_ctl > bun_ctl` as one + unbranched chain: `CHECK_CHAIN_LAG: bone "bun_ctl" in the chain from "neck" — amplitude + 0.800 below the link above's 0.900`, `check: FAIL`. The draft's own 0.8 under 0.9 was not + read before, because `head` had other keyed children. Raised to 1: `CHAIN_LAG: PASS`. + +**The hello's skirt keys.** `hello.ts` keys `skirt_r0`, `skirt_c0` and `skirt_l0` to follow +the settle's hip shift; under the recipe those bones do not exist and the swing carries the +skirt, so step 9c writes the hello with `--no-skirt-keys` (13 tracks; the other 13 are the +same keys, byte for byte). Without physics the keys are needed: step 9b's build with them left +out reads `thigh_l|bottomwear` gap 48 px on the hello, past the bar of 25. + +**The build's lines** (run on 2026-10-21): `CLOTH_SETTLES: WARN — 9 physics chain(s) …: 4 of +them settle before the loop's length`, the skirt's three trees at 1.917, 2 and 1.833 s and +`skirt_swing` at 1.25 s of the 3.333 s idle; the hair does not settle, because the idle keys +the head it hangs from. `loop: … with the rig's 27 physics constraint(s) left out … max |d| 0`, +and the `loop physics:` line naming all 27. Without `--lead-in`, `idle.gif` jumps at the wrap +(`f0040.png differs from f0000.png by max |d| 209, 1244 px over 40`); with `--lead-in 1`, +`max |d| 0, 0 px over 40`. `ANIMATIONS_DO_NOT_FOLD` reads both animations with the physics on +and off: 0 folding meshes. `TEXTURE_STRETCH` severity 1.117 (ceiling 1.927), worst in +`bottomwear`. + +**Bars, written down before measuring:** on each seam pair, gap ≤ 25 px and ghost ≤ 50 px on +the worst frame, physics on; 0 frames of pass-through; `ANIMATIONS_DO_NOT_FOLD` PASS; +`check: PASS` and `build: PASS`; `sleeve_r0` still within 4 degrees of rest. + +**Readings**, on the step's build, `rigc render --candidate out/check/build --animation + --fps 24 --geometry --seam … --pass-through bottomwear:thigh_r,bottomwear:thigh_l +--angle-range …`. `rigc render` starts the physics at rest at frame 0 and steps it, as the +player does when an animation starts, so the frames include the skirt hanging into place in +the first 2 s (gap / ghost, px, worst frame): + +| pair | `idle` | `hello` | +| --- | --- | --- | +| upperarm_r \| topwear | 0 / 2 | 8 / 8 | +| forearm_r \| upperarm_r | 0 / 2 | 1 / 25 | +| upperarm_l \| topwear | 0 / 0 | 0 / 1 | +| forearm_l \| upperarm_l | 0 / 1 | 0 / 0 | +| neck \| topwear | 0 / 0 | 0 / 5 | +| face \| hair_front | 0 / 0 | 7 / 47 | +| face \| neck | 0 / 0 | 0 / 1 | +| hair_back \| face | 0 / 1 | 0 / 0 | +| topwear \| bottomwear | 1 / 7 | 3 / 18 | +| thigh_r \| bottomwear | 6 / 1 | 6 / 5 | +| thigh_l \| bottomwear | 1 / 2 | 3 / 8 | +| shin_r \| thigh_r | 0 / 0 | 0 / 9 | +| shin_l \| thigh_l | 0 / 0 | 0 / 5 | +| shoes \| shin_r | 0 / 0 | 0 / 0 | +| shoes \| shin_l | SKIP | SKIP | + +Pass-through: 0 of 81 idle frames and 0 of 97 hello frames, both thighs. `sleeve_r0` against +`chest`: 4.00 degrees out at most under the hello. Every reading is inside its bar. + +**How far physics moves them.** Each link against the one above (`--angle-range`), the span +over the frames read, degrees, the settle from rest included: + +| link | `idle` | `hello` | +| --- | --- | --- | +| `lockt0_0` against `head` | 0.74 | 2.79 | +| `lockt0_1`, `lockt0_2` | 1.23, 1.87 | 3.46, 6.73 | +| `lock1t0_0` against `head` | 0.45 | 2.14 | +| `lock1t0_1`, `lock1t0_2` | 1.29, 2.22 | 3.12, 5.69 | +| `bang_c0` against `head`, `bang_c1` | 0.95, 2.83 | 11.26, 8.63 | +| `skirt_swing` against `hip` | 0.46 | 2.90 | +| `skirt0_0`, `skirt1_0`, `skirt2_0` against `hip` | 0.56, 0.46, 0.56 | 3.48, 2.90, 3.48 | +| `skirt0_2`, `skirt1_2`, `skirt2_2` | 2.61, 2.60, 7.41 | 4.74, 4.61, 8.84 | + +The tips, in the frame of the bone the piece hangs from (`--geometry`): under the hello the +side locks' ends move up to 5.17 and 5.67 rig px from their median, at t 3.667 s (the +settle), the fringe's up to 9.82 at t 0.333 s (the head turn), and the skirt's middle tree +6.78 at the end of the settle. Under the idle, once the skirt has hung into place (from +2 s on), the side locks' ends move 0.23 and 0.40 rig px and the skirt's less than 0.01: a +breath on the chest barely reaches hair that hangs from the head, and nothing in the idle +moves the hip. Step 8's keyed idle swung the locks' last links at an amplitude of 3.6 degrees. + +**Wind.** `rigc render --environment` with calm (wind (0, 0)) from 0 s and a gust (2, 0) +from 1.5 s, against calm throughout, on the idle: the side locks' ends 9.25 and 10.07 rig px +apart at about 2 s, the fringe 3.89 and 3.57, the skirt's hems 10.90, 8.37 and 12.83. The +step-10 page sets no world of its own; the player's `environment()` is where a page would. + +**Not measured:** the hello over the idle on two tracks with physics, as step 10's +`&under=idle` plays it (`rigc render` reads one animation at a time); the player's physics +against rigc's render frame by frame. + ### The joint script, `scripts/keypoints.py` - **Downloads, first run**: two model zips, 94,223,081 and 212,739,469 bytes (sizes from the @@ -567,3 +698,4 @@ changes is the packages' decision. | The editor's import through its window (`Import Data`), the `MISSING` images step | a licensed editor opened by a person; only the command line was run | | `out/check/build/` or `wave-build/` loading in a game engine's Spine 4.3 runtime | one engine, one load, one frame compared against `rigc render` | | The seams of step 10's hello over the idle (`&under=idle`, two tracks) | a reading of the two tracks mixed, frame by frame, as the player poses them | +| The player's physics against `rigc render`'s, and the hello over the idle with physics on | frames read from the player in a browser and compared with the render's, both tracks on | diff --git a/scripts/check-adapters.ts b/scripts/check-adapters.ts index 058fec4..7749187 100644 --- a/scripts/check-adapters.ts +++ b/scripts/check-adapters.ts @@ -59,7 +59,7 @@ function anchorExists(source: string, anchor: string) { return (n ? `${base}-${n}` : base) === decodeURIComponent(anchor); }); } -// Step 10's two addresses: the hello from step 9b over the idle, and step 9's wave. +// Step 10's two addresses: the hello from step 9b (on step 9c's physics build) over the idle, and step 9's wave. const tutorialModels: Record = { 'out/check/build/skeleton.model.json': { anim: 'hello', under: 'idle' }, 'wave-build/skeleton.model.json': { anim: 'wave', under: null }, @@ -196,6 +196,17 @@ try { const built = JSON.parse(readFileSync(join(consumer, 'out/check/build/skeleton.model.json'), 'utf8')) as { animations: { name: string }[] }; assert.deepEqual(built.animations.map(a => a.name), ['idle', 'hello'], 'GUIDE_HELLO: the build must hold idle and hello'); console.log('[bar:guide-hello] step 9b build exit 0; out/check/build holds idle, hello PASS'); + // Step 9c: the cloth recipe puts the side locks, the fringe and the skirt on physics. + run(['bash', '-eu', '-o', 'pipefail', '-c', blocks(step('9c'), 'sh')[0]], consumer); + const physical = JSON.parse(readFileSync(join(consumer, 'out/check/build/skeleton.model.json'), 'utf8')) as { animations: { name: string }[]; constraints: { kind: string; name: string }[] }; + assert.deepEqual(physical.animations.map(a => a.name), ['idle', 'hello'], 'GUIDE_CLOTH: the build must still hold idle and hello'); + const ofKind = (kind: string) => physical.constraints.filter(c => c.kind === kind).map(c => c.name); + assert.equal(ofKind('physics').length, 27, `GUIDE_CLOTH: expected 27 physics constraints, found ${ofKind('physics').length}`); + assert.equal(ofKind('transform').length, 3, `GUIDE_CLOTH: expected 3 transform constraints, found ${ofKind('transform').length}`); + for (const name of ['lockt0_0_physics', 'lock1t0_0_physics', 'bang_c1_physics', 'skirt_swing_physics', 'skirt1_2_physics']) + assert(ofKind('physics').includes(name), `GUIDE_CLOTH: missing physics constraint ${name}`); + reports.cloth = { physics: ofKind('physics').length, transform: ofKind('transform').length }; + console.log('[bar:guide-cloth] step 9c build exit 0; 27 physics and 3 transform constraints, idle and hello PASS'); // Verification tools are separate from each empty consumer installation. const tools = join(temp, 'tools'); mkdirSync(tools); writeFileSync(join(tools, 'package.json'), '{"private":true,"type":"module"}'); run(['npm', 'install', '--no-audit', '--no-fund', 'playwright@1.58.2'], tools); diff --git a/scripts/hello.ts b/scripts/hello.ts index a368f77..84d3329 100644 --- a/scripts/hello.ts +++ b/scripts/hello.ts @@ -1,7 +1,7 @@ // Make a four-second "hello" as one rig-c motion spec, for the config's // motion.animations_from. // -// bun hello.ts [--seconds 4] [--fps 24] [--out hello.anim.json] +// bun hello.ts [--seconds 4] [--fps 24] [--out hello.anim.json] [--no-skirt-keys] // // is the rig a build wrote, out/rig/rig.json: the bones the hello // keys are read from it, and the legs' lengths and rest angles for the weight @@ -26,11 +26,17 @@ // skirt's hem, so a larger hip move opens a gap there; the skirt's first // links follow the hip, and both ankles stay where they were painted (the // shoes are one image that no leg bone moves). +// --no-skirt-keys leaves the skirt out: once step 9c's cloth recipe hangs it on +// physics, the hip shift moves it and no key does. Without physics the keys +// are needed: with them left out, the hem opens 48 px over the left thigh. import { readFileSync, writeFileSync } from 'node:fs'; type Bone = { name: string; parent?: string; x?: number; y?: number; rotation?: number; length?: number }; const args = process.argv.slice(2); +const ni = args.indexOf('--no-skirt-keys'); +const skirtKeys = ni < 0; +if (!skirtKeys) args.splice(ni, 1); const opt = (name: string, dflt: number): number => { const i = args.indexOf(name); if (i < 0) return dflt; @@ -48,15 +54,15 @@ const oi = args.indexOf('--out'); const outPath = oi >= 0 ? args.splice(oi, 2)[1] : 'hello.anim.json'; const [rigPath] = args; if (!rigPath || !outPath) { - console.error('usage: bun hello.ts [--seconds 4] [--fps 24] [--out hello.anim.json]'); + console.error('usage: bun hello.ts [--seconds 4] [--fps 24] [--out hello.anim.json] [--no-skirt-keys]'); process.exit(2); } const rig = JSON.parse(readFileSync(rigPath, 'utf8')); const bones: Record = {}; for (const b of rig.bones ?? []) bones[b.name] = b; -const need = ['hip', 'chest', 'head', 'mouth', 'sleeve_r0', 'sleeve_r1', 'leg_r0', 'leg_r1', 'leg_l0', 'leg_l1', - 'skirt_r0', 'skirt_c0', 'skirt_l0']; +const skirt = skirtKeys ? ['skirt_r0', 'skirt_c0', 'skirt_l0'] : []; +const need = ['hip', 'chest', 'head', 'mouth', 'sleeve_r0', 'sleeve_r1', 'leg_r0', 'leg_r1', 'leg_l0', 'leg_l1', ...skirt]; const lacking = need.filter((n) => !bones[n]); if (lacking.length > 0) { console.error(`hello.ts: ${rigPath} has no bone ${lacking.join(', ')}; the hello keys all of ${need.join(', ')}`); @@ -149,9 +155,7 @@ const curves: [string, string, Curve, boolean?][] = [ ['leg_r1', 'rotate', (t) => legIK('leg_r', SHIFT * settle(t))[1]], ['leg_l0', 'rotate', (t) => legIK('leg_l', SHIFT * settle(t))[0]], ['leg_l1', 'rotate', (t) => legIK('leg_l', SHIFT * settle(t))[1]], - ['skirt_r0', 'rotate', (t) => SKIRT * settle(t)], - ['skirt_c0', 'rotate', (t) => SKIRT * settle(t)], - ['skirt_l0', 'rotate', (t) => SKIRT * settle(t)], + ...skirt.map((b): [string, string, Curve] => [b, 'rotate', (t) => SKIRT * settle(t)]), ]; const n = Math.round(seconds * fps); @@ -188,6 +192,6 @@ const [l0, l1] = legIK('leg_l', SHIFT); const f2 = (x: number) => (x >= 0 ? '+' : '') + x.toFixed(2); console.log( `hello.ts: 4 beats (hm?, heh, wave, settle), ${seconds} s at ${fps} fps, ${tracks.length} tracks of ${n + 1} keys; ` + - `sleeve_r0 at most ${maxUpper.toFixed(1)} deg; hip shift ${maxShift.toFixed(1)} px ` + + `sleeve_r0 at most ${maxUpper.toFixed(1)} deg; hip shift ${maxShift.toFixed(1)} px${skirtKeys ? '' : ', skirt left to physics'} ` + `(legs r ${f2(r0)}/${f2(r1)}, l ${f2(l0)}/${f2(l1)} deg, ankles held) -> ${outPath}`, );