diff --git a/README.md b/README.md index 9747bbf..d88eee9 100644 --- a/README.md +++ b/README.md @@ -131,26 +131,34 @@ The first run downloads two pose models (about 300 MB) and some Python packages. should see `18 observed, 0 occluded, 0 missing -> keypoints.json`. No Python? [docs/KEYPOINTS.md](docs/KEYPOINTS.md) has a hand-placed set for this very -painting; save it as `keypoints.json`. You get the same 42 bones without the `LINT` line -step 7 prints for the script's left ankle, and it builds green all the same. +painting; save it as `keypoints.json`. Its left ankle is already where step 7's first line +puts it, and you get the same 44 bones. It was tuned to the script's joints, so the steps +print what they quote, except that step 9b's hello reads `33.6 px in` and `16.05 -> 12.92 deg`. ### 7. Draft the bones ```sh +bun -e 'const f = "keypoints.json", k = await Bun.file(f).json(); k.people[0].joints.l_ankle.at = [848, 2140]; await Bun.write(f, JSON.stringify(k, null, 1) + "\n")' bunx rig-parts propose --parts work/rig --source inputs/painting.png --out work --keypoints keypoints.json bun ../getting-started/scripts/take.ts work/proposal.json config.json bones meshes regions motion limbs ``` `propose` drafts the bones, the meshes and an idle motion; `take.ts` copies them into the -config. This is where a person corrects the draft. The example uses it as is. - -You should see a note per joint, then a coverage line ending `42 bone(s) — 29 checked and -clean, 1 checked with a LINT line, 12 not checked`. The one `LINT` line begins -`LINT foot_l at [428, 1077], the l_ankle joint and leg_l1's tip, lies on none of leg_l's art -within 1 px along its joint line`: the script put the left ankle just off the leg, so the -left leg is drafted without its ankle cut and `shoes` stays one part. A `LINT` is something -to look at, not a refusal. Open `work/render/landmarks.png` to see the bones over the -painting. +config. This is where a person corrects the draft, and the first line is the example's one +correction. The script puts the left ankle at `[855.7, 2154.1]`, 12 px off the leg's art, +and without the line `propose` says so: `LINT foot_l at [428, 1077], the l_ankle joint and +leg_l1's tip, lies on none of leg_l's art within 1 px along its joint line`, and the left +leg is drafted without its ankle cut, so the shoes stay one part that no leg bone moves. +Moved onto the shin at `[848, 2140]` (painting px), both ankles lie on the leg art and on +the shoes, and `propose` cuts the shoes between the feet, one part per foot under each +shin. A `LINT` is something to look at, not a refusal; this one is worth fixing because +step 9b's hello moves a foot. + +You should see a note per joint, among them `and foot_r, shoes's share below the ankle, on +bone foot_r at the r_ankle joint under leg_r1` and the same for `foot_l`, then +`0 LINT line(s) over 6 mesh(es) and the hip` and a coverage line ending `44 bone(s) — 29 +checked and clean, 0 checked with a LINT line, 15 not checked`. Open +`work/render/landmarks.png` to see the bones over the painting. ### 8. Build the rig @@ -158,8 +166,10 @@ painting. bunx rig-parts build --config config.json --source inputs/painting.png --full inputs/layers/full --head inputs/layers/head --out out --loop ``` -About a minute. You should see `[check] check: PASS; 11 of 12 bar(s) measured, 1 skipped -(STRESS_OFF_CHAIN)`, then `build: PASS`. Open `out/idle.gif`: the character breathes and blinks in a 3.3-second loop. +About a minute. You should see `limb cut: "shoes" opaque 13735 = "foot_r" 8071 + "foot_l" +5664`, the shoes cut in two, then `[check] check: PASS; 11 of 13 bar(s) measured, 2 skipped +(FEET_STAY_LEVEL, STRESS_OFF_CHAIN)`, then `build: PASS`. `FEET_STAY_LEVEL` is skipped +because the idle turns no leg; step 9b's hello does. Open `out/idle.gif`: the character breathes and blinks in a 3.3-second loop. If any line says `FAIL`, nothing was written, and that line says what to change. ### 9. Give it a wave @@ -196,7 +206,7 @@ failure: where a mesh does fold, it names the part and the frames to look at. Step 9 turned joint positions into a motion. A motion can also be written as keys on the bones and handed to the build, which checks it with the rig. -[`scripts/hello.ts`](scripts/hello.ts) writes a four-second hello that way, reading the bones +[`scripts/hello.ts`](scripts/hello.ts) writes a hello of under five seconds that way, reading the bones from the rig step 8 built; the config names the file in `motion.animations_from`, and the build runs again. @@ -207,19 +217,24 @@ bun ../getting-started/scripts/take.ts work/hello-from.json config.json motion.a bunx rig-parts build --config config.json --source inputs/painting.png --full inputs/layers/full --head inputs/layers/head --out out --loop ``` -About a minute and a half. You should see `hello.ts: 4 beats (hm?, heh, wave, settle), 4 s at -24 fps, 16 tracks of 97 keys; sleeve_r0 at most 4.0 deg; hip shift 6.0 px`, then from the -build `animations beside the idle, from …/hello.anim.json as written: hello`, +About a minute and a half. You should see `hello.ts: 4 beats (hm?, heh, wave, settle), 4.75 s +at 24 fps, 16 tracks of 115 keys; sleeve_r0 at most 4.0 deg; settle: right ankle 33.2 px in +(40 % of the way to parallel, 15.96 -> 12.87 deg), hip 6.0 px`, then from the build +`animations beside the idle, from …/hello.anim.json as written: hello`, `ANIMATIONS_DO_NOT_FOLD: PASS — 2 animation(s) read by rigc render --folds at 12 fps (idle, -hello)`, `check: PASS; 11 of 12 bar(s) measured, 1 skipped (STRESS_OFF_CHAIN); 1 warning(s)`, -and `build: PASS`. The warning is `SEAM_OVER_MOTION`, never a bar: where the forearm turns +hello)`, `FEET_STAY_LEVEL: PASS`, `check: PASS; 12 of 13 bar(s) measured, 1 skipped +(STRESS_OFF_CHAIN); 1 warning(s)`, and `build: PASS`. The warning is `SEAM_OVER_MOTION`, never a bar: where the forearm turns at the elbow, a sliver of it shows past the sleeve's cuff. `out/check/build/` now holds `idle` and `hello`. The wave here is the forearm's, with the upper arm kept within 4 degrees of rest. On this painting the upper arm and its puff sleeve are one image under a sleeveless top, so -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. +swinging it out, as step 9's wave does, opens a gap at the armhole. Then she re-plants her +weight as the arm comes down: her splayed right leg (the picture's left) slides its foot +40 % of the way in toward parallel with the other, the knee bending the way it is painted, her hip +follows 6 px, and both go back by the end of the loop. The feet are the two parts step 7's +correction made; each keeps its shoe level while the leg turns, and the hello never keys +them. The comment at the top of `hello.ts` says why each limit is where it is. ### 9c. Let the hair and the skirt answer @@ -227,7 +242,7 @@ So far the hair and the skirt move only where a key moves them. The parts packag 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. +hello's hip, moves them, and no key does. ```sh bun ../getting-started/scripts/hello.ts out/rig/rig.json --no-skirt-keys @@ -249,9 +264,11 @@ bunx rig-parts build --config config.json --source inputs/painting.png --full in ``` 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 +keys (`hello.ts` then prints `14 tracks of 115 keys` and `skirt0_0 follows the thigh, the rest +left to physics`): step 9b's settle turns the skirt's top links by hand to follow her hip and +her right thigh, and from here physics does the hip's part. The thigh's stays a key, on +`skirt0_0`, the recipe's first link over the right thigh, which carries no physics of its +own. `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 @@ -274,12 +291,12 @@ swing at least as far as the one above. The draft gives the bun 0.8 degrees unde 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) +`check: PASS; 12 of 13 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 +settle 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 @@ -291,9 +308,9 @@ bun ../getting-started/scripts/serve.ts Open . You should see her notice you — "hm?", her head straightening — then smile with narrowed -eyes and two little shrugs, wave with her forearm beside her shoulder, and settle her weight -before it starts again. The locks beside her face trail her head, and the skirt swings after -her hip when she settles. `&under=idle` keeps the idle playing beneath, so she breathes +eyes and two little shrugs, wave with her forearm beside her shoulder, and re-plant her +weight, one foot sliding in toward the other and back, before it starts again. The locks beside her +face trail her head, and the skirt swings after her hip when she settles. `&under=idle` keeps the idle playing beneath, so she breathes through it. Step 9's wave is at ; the menu switches animations. Ctrl+C stops the server. That's the whole chain. The page is @@ -309,7 +326,7 @@ the original three are measured in [docs/REFERENCE.md](docs/REFERENCE.md#use-wha For your own site, with no Spine runtime. The page from step 10 is already this: it loads `skeleton.model.json`, the document a build writes next to the Spine files, and the part -images beside it. Copy `wave-build/` (that document plus 22 part PNGs, 1.6 MB; it holds +images beside it. Copy `wave-build/` (that document plus 28 part PNGs, 1.3 MB; it holds both `idle` and `wave`) next to a page, and start from the play README's [minimal page](https://github.com/rig-c/play#a-minimal-page): about twenty lines that load the player from a CDN, fit the rig to a box and play it. Point its `modelUrl` at @@ -575,7 +592,7 @@ guesses the torso from the clothes and draws no legs. **On the CPU**: [`scripts/keypoints.py`](scripts/keypoints.py) finds the 18 joints with a pose model (RTMPose), as in [step 6](#6-find-the-joints): about 300 MB of models the first time, then a few seconds per painting. On the example painting its joints were within 4–52 -pixels of a hand-placed set, and the rig built green on them unchanged. +pixels of a first hand-placed set, and the rig built green on them unchanged. **By hand**: read the 18 positions off the painting in an image editor and write them into a small JSON file. The format, a diagram of where each joint goes, and a complete example diff --git a/docs/KEYPOINTS.md b/docs/KEYPOINTS.md index c875409..10fae4c 100644 --- a/docs/KEYPOINTS.md +++ b/docs/KEYPOINTS.md @@ -42,32 +42,42 @@ This is a hand-placed set for the example painting (1664 x 2432). Save it as "source": "placed by hand", "people": [ { "id": "a", "joints": { - "nose": { "state": "observed", "at": [894, 344] }, - "neck": { "state": "observed", "at": [880, 490] }, - "r_shoulder": { "state": "occluded", "at": [690, 560] }, - "r_elbow": { "state": "observed", "at": [664, 847] }, - "r_wrist": { "state": "observed", "at": [573, 1120] }, - "l_shoulder": { "state": "occluded", "at": [1060, 550] }, - "l_elbow": { "state": "observed", "at": [1100, 831] }, - "l_wrist": { "state": "observed", "at": [1223, 1097] }, - "r_hip": { "state": "occluded", "at": [780, 1030] }, - "r_knee": { "state": "observed", "at": [650, 1650] }, - "r_ankle": { "state": "observed", "at": [455, 2110] }, - "l_hip": { "state": "occluded", "at": [970, 1030] }, - "l_knee": { "state": "observed", "at": [940, 1655] }, - "l_ankle": { "state": "observed", "at": [850, 2110] }, - "r_eye": { "state": "observed", "at": [825, 275] }, - "l_eye": { "state": "observed", "at": [944, 294] }, + "nose": { "state": "observed", "at": [891, 332] }, + "neck": { "state": "observed", "at": [877, 541] }, + "r_shoulder": { "state": "occluded", "at": [720, 532] }, + "r_elbow": { "state": "observed", "at": [672, 835] }, + "r_wrist": { "state": "observed", "at": [560, 1136] }, + "l_shoulder": { "state": "occluded", "at": [1034, 550] }, + "l_elbow": { "state": "observed", "at": [1098, 835] }, + "l_wrist": { "state": "observed", "at": [1228, 1107] }, + "r_hip": { "state": "occluded", "at": [779, 994] }, + "r_knee": { "state": "observed", "at": [637, 1657] }, + "r_ankle": { "state": "observed", "at": [447, 2148] }, + "l_hip": { "state": "occluded", "at": [1010, 1000] }, + "l_knee": { "state": "observed", "at": [944, 1669] }, + "l_ankle": { "state": "observed", "at": [848, 2140] }, + "r_eye": { "state": "observed", "at": [832, 272] }, + "l_eye": { "state": "observed", "at": [950, 284] }, "r_ear": { "state": "missing" }, "l_ear": { "state": "missing" } } } ] } ``` -With this set, the example rig comes out with 42 bones — a chain per leg — where the -script's joints give 40 and one chain down the middle of both legs. Both build green, and -the README's wave runs on both. [REFERENCE.md](REFERENCE.md#the-leg-chain-moves-with-the-joints) -says why. +With this set, the example rig comes out with 44 bones: a chain per leg, and the shoes cut +into one part per foot under each shin, the same as the script's joints give once the +README's step 7 has moved their left ankle. The feet are cut only when each ankle lies on +its leg's art and on the shoes; `l_ankle` sits at `[848, 2140]` for that, and the README's +step 7 line puts it there whichever set you use. Earlier versions of the parts package gave +other counts; [REFERENCE.md](REFERENCE.md#the-leg-chain-moves-with-the-joints) keeps them. + +The set was tuned to land where the script's joints do: each joint here is within a pixel +of what `scripts/keypoints.py` writes for this painting, and the ears, which it places, +are left `missing`. The hips, the knees and the right ankle are the ones that matter. An +earlier set had them 14–50 px off, and the right leg's stance came out 0.7 degrees wider: +the README's step 9b hello then slid the right foot 41.1 px instead of 33.2 and opened a +seam at the right knee. With this set step 9b prints `right ankle 33.6 px in (40 % of the +way to parallel, 16.05 -> 12.92 deg)`, and every other line the README quotes as written. ## What the script writes diff --git a/docs/REFERENCE.md b/docs/REFERENCE.md index 1625a12..780b021 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, and once more on 2026-10-21 with step 9c. 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 twice on 2026-10-21, with step 9c and then with the stance settle. Everything marked *measured* on this page was run on this machine with these versions. | | | @@ -64,6 +64,19 @@ readings under *Step 9c* were taken on. One line's wording differs between the t `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). +Later on 2026-10-21, with step 7's ankle correction and the stance settle, every `sh` block +was run verbatim in order from an empty folder once more (run 7), `git clone` again replaced +by a copy of the checkout; `bun add` installed parts 1.49.1, rig-c 2.64.1, pose 0.37.0 and +play 1.12.0. Every expected line on the README held, the ones changed by this step included +(`44 bone(s)`, `limb cut: "shoes" …`, step 8's `11 of 13`, the two `hello.ts` lines, steps +9b's and 9c's `12 of 13`). Its outputs were byte-identical to the scratch run the readings +under [*The settle: a stance change*](#the-settle-a-stance-change) were taken on +(`keypoints.json`, `config.json`, `wave.motion.json`, `wave-build/`'s `skeleton.json` and +`skeleton.model.json`, step 9b's and step 9c's `skeleton.model.json`, step 9c's +`skeleton.json`, `skeleton.atlas`, `skeleton.png` and `out/idle.gif`), except +`hello.anim.json`'s `note`, whose wording was changed between the two runs; its tracks are +the same. 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 @@ -113,6 +126,10 @@ Run 6 (clean, 2026-10-21, caches warm, the machine's load average 13–16 from o 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. +Run 7 (clean, 2026-10-21, caches warm, other jobs running, so an upper bound): **416.6 s** +in all — steps 1 to 10 took 1.0, 3.8, 3.7, 6.8, 42.1, 2.1, 5.5, 75.5, 6.4, 119.5 (9b), 146.1 +(9c) and 2.4 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`. @@ -157,6 +174,11 @@ published package checks every build with rigc's own validator, and the ### Step 9b, the hello +The settle this section measures (hip 6 px, 0.7 s, the ankles held) was replaced on +2026-10-21 by a stance change, after step 7 began cutting the shoes into one part per foot; +its readings are under [*The settle: a stance change*](#the-settle-a-stance-change). The rest +of this section is as measured then. + `scripts/hello.ts` writes keys, not joint positions: the face is outside the 18 body joints rig-pose reads, and the arm's limit is an angle, which a key states exactly. It reads the bones from `out/rig/rig.json` — which bones exist, and the legs' lengths and rest angles, @@ -227,7 +249,8 @@ 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). +their names and places. The rig goes from 70 bones to 46 (the keyed chains' `_ctl` bones go); +with step 7's ankle correction, from 72 to 48 (the two feet are bones of their own). | piece | chains | constraints | | --- | --- | --- | @@ -254,7 +277,8 @@ none, so the runtime's default world applies: wind (1, 0), gravity (0, 1) (state **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 +same keys, byte for byte; since the stance change, 14 tracks, the 14th keying `skirt0_0`, see +below). 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 @@ -329,6 +353,136 @@ step-10 page sets no world of its own; the player's `environment()` is where a p `&under=idle` plays it (`rigc render` reads one animation at a time); the player's physics against rigc's render frame by frame. +### The settle: a stance change + +Added 2026-10-21. The owner asked for the settle to bring the feet toward a parallel stance +and back, which would also drive the skirt. A read-only probe on this build (parts 1.48.0) +designed it; the readings below are the guide's own, on parts 1.49.1 and rig-c 2.64.1. + +**The feet, through the producer.** The parts package's limbs rule drafts `foot_r` and +`foot_l` under the shins "when every leg's ankle lies on one footwear part — `foot_` on a +bone of its own at the ankle under the shin, `inherit` `"noRotationOrReflection"`, so the +shoe keeps its setup angle while the leg turns" (stated by the parts [AUTHORING][authoring], +*Limbs: cut at their joints*). The script's `l_ankle`, at painting px `[855.7, 2154.1]`, lies +11.761 px from the leg's art along its joint line, so `propose` prints `LINT foot_l … +(ASSEMBLE_LIMB_JOINT_ON_ART)` and leaves `shoes` whole. The hand-placed set's `[850, 2110]` +lies on the leg but 22 px from the shoes, so it leaves them whole too, with a note and no +`LINT`. At `[848, 2140]` both sets give `propose` 44 bones and 0 `LINT` lines, and `build` +prints `limb cut: "shoes" opaque 13735 = "foot_r" 8071 + "foot_l" 5664` (measured). Step 7's +first line moves the ankle there in whichever `keypoints.json` the reader has, and +[KEYPOINTS.md](KEYPOINTS.md)'s set now holds it too. The build's rig has 72 bones (70 +before) and 28 parts; step 8's `check` adds `FEET_STAY_LEVEL`, skipped on the idle, which +turns no leg. The ankle caps are reported missing (`limb cap: … "ankle" … 593 px missing`, +`… 620 px missing`), and `RECOMPOSITE_HOLES` reads the same 402 px hole at the left ankle as +before the split (`error_px 6275`): neither shows while the foot stays level and the shin +draws over it. + +**The painted stance** (rig px, from the rig's leg bones): the right leg (the picture's left) +runs 15.96 degrees from straight down, hip to ankle; the left 8.09 degrees, the same way. The +right shoe is painted turned out 44.4 degrees more than the left (the probe's reading of the +shoes' long axes). + +**The keys** (`scripts/hello.ts`, from the rig's leg lengths and rest angles): + +| | value | +| --- | --- | +| right ankle | 33.2 px in along the floor: 40 % of the 83.0 px to where hip-to-ankle is parallel to the left leg (15.96 → 12.87 degrees) | +| right leg | two-bone IK, the knee bending the way it is painted: `leg_r0` +9.61, `leg_r1` −13.63 degrees at the hold | +| hip | x −6 px, y 0 | +| left leg | ankle held, IK under the hip's move: `leg_l0` +1.70, `leg_l1` −2.65 degrees | +| feet | not keyed: each keeps its shoe level by its inherit and goes with its ankle | +| skirt, step 9b | `skirt_r0` equal to `leg_r0`; `skirt_c0`, `skirt_l0` 1.66 degrees after the hip, as before | +| skirt, step 9c | `skirt0_0` equal to `leg_r0`: the first link of the tree over the right thigh, which takes the swing's turn through a transform constraint and has no physics of its own, so a key adds to it; the rest of the skirt answers the hip by physics | +| timing | out over 3.15–3.75 s as the arm comes down, held to 4.15 s, back over 4.15–4.75 s, smoothstep; the hello is 4.75 s (115 keys at 24 fps), every track at rest at both ends | + +**Bars, written down before measuring** (from rig-c/getting-started#13): hem +`thigh_?|bottomwear` gap ≤ 25 px; knee `shin_?|thigh_?` ghost ≤ 50 px, and `shin_?|foot_?` +within the same; 0 frames of pass-through; the ankles (`leg_?1`'s tips) and the soles (each +foot's lowest vertex) within 2 px of rest (`--geometry`); no folds; `sleeve_r0` within 4 +degrees; every seam pair within gap ≤ 25 and ghost ≤ 50 on `idle` and `hello`, physics on; +`FEET_STAY_LEVEL` PASS; `check: PASS`; `build: PASS`. + +**50 % failed one, and the beat came down to 40 %.** The probe proposed 50 % (41.5 px, +`leg_r0` +11.11, `leg_r1` −15.28 degrees). On step 9c's build, physics on, it read +`topwear|bottomwear` ghost **54** px (frames 86–101, the hold), over the bar of 50: the +skirt's top, turned with `skirt0_0`, pulls off the shirt. Every other pair passed (hem +`thigh_r` 20 / 3, knee 1 / 40), and step 9b's plain build passed everything (`topwear` 1 / +45). At 40 % every bar holds on both builds. The bars were not changed. + +**Readings at 40 %**, `rigc render --candidate out/check/build --animation --fps 24 +--geometry --folds --seam … --pass-through bottomwear:thigh_r,bottomwear:thigh_l +--angle-range …`, physics on where the rig has it (gap / ghost, px, worst frame; `rigc +render` starts physics at rest, so the frames include the skirt hanging into place): + +| pair | 9b `idle` | 9b `hello` | 9c `idle` | 9c `hello` | +| --- | --- | --- | --- | --- | +| upperarm_r \| topwear | 0 / 2 | 8 / 8 | 0 / 2 | 8 / 8 | +| forearm_r \| upperarm_r | 0 / 2 | 1 / 26 | 0 / 2 | 1 / 26 | +| upperarm_l \| topwear | 0 / 0 | 0 / 1 | 0 / 0 | 0 / 1 | +| forearm_l \| upperarm_l | 0 / 1 | 0 / 0 | 0 / 1 | 0 / 0 | +| neck \| topwear | 0 / 0 | 0 / 5 | 0 / 0 | 0 / 5 | +| face \| hair_front | 2 / 0 | 0 / 0 | 0 / 0 | 7 / 47 | +| face \| neck | 0 / 0 | 0 / 1 | 0 / 0 | 0 / 1 | +| hair_back \| face | 0 / 1 | 0 / 0 | 0 / 1 | 0 / 0 | +| topwear \| bottomwear | 0 / 3 | 1 / 38 | 1 / 7 | 3 / 44 | +| thigh_r \| bottomwear | 1 / 0 | 11 / 0 | 6 / 1 | 10 / 2 | +| thigh_l \| bottomwear | 0 / 1 | 11 / 3 | 1 / 2 | 7 / 6 | +| shin_r \| thigh_r | 0 / 0 | 1 / 38 | 0 / 0 | 1 / 38 | +| shin_l \| thigh_l | 0 / 0 | 0 / 5 | 0 / 0 | 0 / 5 | +| shin_r \| foot_r | 0 / 0 | 0 / 0 | 0 / 0 | 0 / 0 | +| shin_l \| foot_l | SKIP | SKIP | SKIP | SKIP | + +The two `shoes` pairs of the earlier tables are gone with the part; the two foot pairs take +their place, and `shin_l|foot_l` is rigc's `SKIP` as `shoes|shin_l` was. Pass-through: 0 of +81 idle and 0 of 115 hello frames, both thighs, both builds. Folds: none. Feet: the ankles' +y and the soles' lowest y move 0.001 px at most on every frame (the right ankle 142, the +left 146, the soles 26 and 22, world y up); the right ankle's x runs −192.00 to −158.80, +33.2 px. `sleeve_r0` against `chest`: 4.00 degrees out at most. The build: `FEET_STAY_LEVEL: +PASS — 2 animation(s) …: each foot's drawing within ±10 degrees of its setup pose while a bone +above it is turned`, `ANIMATIONS_DO_NOT_FOLD: PASS`, `check: PASS; 12 of 13 bar(s) measured, +1 skipped (STRESS_OFF_CHAIN)`, `build: PASS`, on both 9b and 9c. Every reading is inside its +bar; the worst is `face|hair_front` ghost 47 (the head turn at 0.333 s, as before) and +`topwear|bottomwear` 44. + +**Angles under the hello** (`--angle-range`, span over the frames read, degrees): + +| bone against | 9b | 9c | before, the hip-only settle (9c) | +| --- | --- | --- | --- | +| `leg_r0` against `hip` | 9.61 | 9.61 | — | +| `leg_r1` against `leg_r0` | 13.63 | 13.63 | — | +| `leg_l0`, `leg_l1` | 1.70, 2.65 | 1.70, 2.65 | — | +| `foot_r`, `foot_l` against `hip` | 0, 0 | 0, 0 | — | +| `skirt_r0` / `skirt0_0` against `hip` (keyed) | 9.61 | 10.28 | 3.48 | +| `skirt_swing` against `hip` | — | 1.60 | 2.90 | +| `skirt1_0`, `skirt2_0` against `hip` | 1.66, 1.66 (keyed) | 1.60, 1.92 | 2.90, 3.48 | +| `skirt0_2` against `skirt0_1` | — | 10.44 | 4.74 | +| `skirt2_2` against `skirt2_1` | — | 8.33 | 8.84 | + +The feet's bones do not turn, so the right shoe stays drawn at its painted angle, turned out: +the legs come together, the feet do not become parallel. The skirt answers in two ways: the +tree over the right thigh follows the thigh by its key, and its tip link swings 10.44 degrees +against the link above (4.74 under the hip-only settle); the shared swing and the other +trees answer the hip, and less than before (1.60 against 2.90), because the hip now takes +0.6 s to move where it took 0.35 s. + +**Needs a part** (from the probe, not changed by this step): + +- **A turned-in right shoe.** The toe-out is painted. Turning the foot's image in the + picture's plane reads as a shoe tipped on its side, pushes the sole 13–22 px through the + floor, and past 10 degrees fails `FEET_STAY_LEVEL`. A true parallel stance needs a second + painting of the right shoe, switched in by an attachment key. +- **Thigh art under the hem**, to go further than about 50–75 % of the way: the thigh turns + about the hip 180 px above the hem and its art starts at the hem, so a turn opens a sliver + that only the skirt's link covers, and past about 75 % the skirt mesh itself splits. +- **A lower hip or a painting with the legs closer**, for straight parallel legs: with the + hip where it is the right leg must shorten 18 px, which only the knee gives, and from 75 % + the knees nearly touch. +- **Ankle caps**, the 593 and 620 px the `limb cap:` lines size, and the 402 px hole at the + left ankle: unseen while the feet stay level, seen as soon as a foot turns against its + shin. + +**Not measured:** the hello over the idle on two tracks, as step 10 plays it. + ### The joint script, `scripts/keypoints.py` - **Downloads, first run**: two model zips, 94,223,081 and 212,739,469 bytes (sizes from the @@ -342,8 +496,10 @@ against rigc's render frame by frame. both times with every package and model already cached — so that 40 s is environment set-up and first imports, not downloads. - **Agreement with hand-placed joints**: on the example painting, within 4–52 painting - pixels of the set in [KEYPOINTS.md](KEYPOINTS.md) (neck 52, l_hip 49, l_ankle 44, - r_shoulder 40; elbows, wrists, knees 4–21). + pixels of the set first placed in [KEYPOINTS.md](KEYPOINTS.md) (neck 52, l_hip 49, l_ankle 44, + r_shoulder 40; elbows, wrists, knees 4–21). The set there now was moved to within a pixel + of the script's joints, so that the no-Python route gives the same hello + ([KEYPOINTS.md](KEYPOINTS.md#the-file)). - **The estimator's joints built green unedited**: `propose --keypoints` (2 LINT lines, both the leg chain's top under the skirt) → `take.ts` → `build` PASS 10/10. - **The hand-placed set** through the same steps: 42 bones, a chain per leg on the one @@ -358,7 +514,9 @@ a different, smaller pose model; it was not tried and would need its own downloa On the example painting both legs are one See-through layer. From the script's joints the proposer gave one leg chain down the middle of both legs (40 bones); from the hand-placed -set it gave a chain per leg on that same layer (42 bones). Which one you get depends on +set it gave a chain per leg on that same layer (42 bones). On parts 1.49.1 both sets give a +chain per leg, and with step 7's ankle correction both give 44 bones, the feet cut from the +shoes. Which one you get depends on where the knees and ankles fall on the leg art, and a note on the leg part says which and why. Both build green. The README's map leaves the legs out for this reason — the wave does not move them, and a map written for one bone list misses on the other. To give each leg its diff --git a/scripts/check-adapters.ts b/scripts/check-adapters.ts index 7749187..78dbb91 100644 --- a/scripts/check-adapters.ts +++ b/scripts/check-adapters.ts @@ -156,6 +156,15 @@ try { writeFileSync(join(consumer, 'keypoints.json'), keypoints); const beforeDraft = readFileSync(join(consumer, 'config.json'), 'utf8'); for (const n of [7, 8]) run(['bash', '-eu', '-o', 'pipefail', '-c', blocks(step(n), 'sh')[0]], consumer); + // Step 7's ankle correction lets the producer cut the shoes into one part per foot. + type Built = { animations: { name: string; duration?: number }[]; bones: { name: string }[]; slots: { name: string }[]; constraints: { kind: string; name: string }[] }; + const feetBuild = JSON.parse(readFileSync(join(consumer, 'out/check/build/skeleton.model.json'), 'utf8')) as Built; + const slotNames = feetBuild.slots.map(slot => slot.name); + for (const name of ['foot_r', 'foot_l']) assert(slotNames.includes(name), `GUIDE_FEET: step 8's build has no ${name} slot (step 7's l_ankle correction)`); + assert(!slotNames.includes('shoes'), 'GUIDE_FEET: step 8\'s build still draws the shoes as one part'); + assert.equal(feetBuild.bones.length, 72, `GUIDE_FEET: expected 72 bones in step 8's build, found ${feetBuild.bones.length}`); + reports.feet = { slots: ['foot_r', 'foot_l'], bones: feetBuild.bones.length }; + console.log('[bar:guide-feet] step 8 build: foot_r and foot_l slots, no shoes slot, 72 bones PASS'); // Revert the real field-copy repair in a private README copy. Its command, // over the same proposal and installed packages, must reproduce the refusal. const take = blocks(step(7), 'sh')[0].split('\n').find(line => line.includes('/scripts/take.ts ')); @@ -193,13 +202,17 @@ try { reports.modelSha256 = createHash('sha256').update(readFileSync(join(consumer, 'wave-build/skeleton.model.json'))).digest('hex'); // Step 9b: the hello, named in the config and gated by the build. run(['bash', '-eu', '-o', 'pipefail', '-c', blocks(step('9b'), 'sh')[0]], consumer); - const built = JSON.parse(readFileSync(join(consumer, 'out/check/build/skeleton.model.json'), 'utf8')) as { animations: { name: string }[] }; + const built = JSON.parse(readFileSync(join(consumer, 'out/check/build/skeleton.model.json'), 'utf8')) as Built; 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'); + const helloLength = (b: Built) => b.animations.find(a => a.name === 'hello')?.duration; + assert.equal(helloLength(built), 4.75, `GUIDE_HELLO: expected a 4.75 s hello, found ${helloLength(built)}`); + console.log('[bar:guide-hello] step 9b build exit 0; out/check/build holds idle, hello (4.75 s) 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 }[] }; + const physical = JSON.parse(readFileSync(join(consumer, 'out/check/build/skeleton.model.json'), 'utf8')) as Built; assert.deepEqual(physical.animations.map(a => a.name), ['idle', 'hello'], 'GUIDE_CLOTH: the build must still hold idle and hello'); + assert.equal(helloLength(physical), 4.75, `GUIDE_CLOTH: expected a 4.75 s hello, found ${helloLength(physical)}`); + for (const name of ['foot_r', 'foot_l']) assert(physical.slots.some(slot => slot.name === name), `GUIDE_CLOTH: the cloth build lost the ${name} slot`); 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}`); diff --git a/scripts/hello.ts b/scripts/hello.ts index 84d3329..1dc9fb7 100644 --- a/scripts/hello.ts +++ b/scripts/hello.ts @@ -1,34 +1,48 @@ -// Make a four-second "hello" as one rig-c motion spec, for the config's +// Make a hello of under five seconds as one rig-c motion spec, for the config's // motion.animations_from. // -// bun hello.ts [--seconds 4] [--fps 24] [--out hello.anim.json] [--no-skirt-keys] +// bun hello.ts [--seconds 4.75] [--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 -// shift. The output is {"spec": "rigc-motion/1", "animations": {"hello": …}}, -// the shape rig-parts reads beside the idle; `rig-parts build` then gates it -// with the rig and reads it for folds. +// keys are read from it, and the legs' lengths and rest angles for the settle. +// The output is {"spec": "rigc-motion/1", "animations": {"hello": …}}, the +// shape rig-parts reads beside the idle; `rig-parts build` then gates it with +// the rig and reads it for folds. // // Four beats, every track back at rest by the end, so it loops: // hm? the head straightens toward the viewer, a small lean, brows up; // heh the eyes narrow and the mouth widens, two shoulder bobs; // wave the forearm comes up beside the shoulder and waves twice; -// settle she shifts her weight onto her right foot and back, to rest. +// settle she re-plants her weight: as the arm comes down, her splayed right +// leg (the picture's left) slides its foot 40 % of the way in toward +// parallel with the other, her hip follows it, and both go back. // Nothing here comes from a video or a dataset: it is an authored test motion, // keyed on the bones, sampled once per frame. // -// Two limits come from the example painting, measured with `rigc render --seam`: +// The limits come from the example painting, measured with `rigc render --seam`: // - The upper arm stays within 4 degrees of rest. Its image is the puff sleeve // and the arm in one piece, under a sleeveless top, so swinging it out shows // background at the armhole (50 px at 6 degrees). The forearm tucks under // the cuff and is free at the elbow, so the wave is the forearm's. -// - The weight shift is 6 px (0.5 % of the figure). The thigh art starts at the -// 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. +// - The right foot comes 40 % of the way to parallel, 33.2 px. Each foot is a +// part of its own on a bone that keeps the shoe level (step 7's keypoint +// correction gives the rig foot_r and foot_l), so the ankle slides along the +// floor and a two-bone solve bends the knee the way it is painted. The thigh +// art starts at the skirt's hem, so the skirt's link over the right thigh +// turns with it. At 50 % the skirt's top pulls off the shirt under physics +// (topwear|bottomwear ghost 54 px, over the guide's 50); past about 75 % +// the hem opens and the knees meet. +// - The hip moves 6 px (0.5 % of the figure); a larger move opens the hem +// over the left thigh. The left foot stays where it was painted. +// - The feet are never keyed: the right shoe is painted turned out, and +// turning its image reads as a shoe tipped on its side. +// --no-skirt-keys is for step 9c's cloth recipe, which hangs the skirt on +// physics: the hip's move then swings it and no key does, except on skirt0_0, +// the first link of the tree over the right thigh, which carries no physics of +// its own and takes the thigh's turn. That bone is the recipe's, so it is not +// in the rig this reads; the build that follows checks it. Without physics the +// keys are needed: skirt_r0 takes the thigh's turn and skirt_c0, skirt_l0 +// follow the hip, or the hem opens over the thighs. import { readFileSync, writeFileSync } from 'node:fs'; type Bone = { name: string; parent?: string; x?: number; y?: number; rotation?: number; length?: number }; @@ -48,35 +62,42 @@ const opt = (name: string, dflt: number): number => { args.splice(i, 2); return v; }; -const seconds = opt('--seconds', 4); +const SHEET = 4.75; // seconds: the beat sheet's own length +const seconds = opt('--seconds', SHEET); const fps = opt('--fps', 24); 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] [--no-skirt-keys]'); + console.error('usage: bun hello.ts [--seconds 4.75] [--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 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 need = ['hip', 'chest', 'head', 'mouth', 'sleeve_r0', 'sleeve_r1', 'leg_r0', 'leg_r1', 'leg_l0', 'leg_l1', 'foot_r', 'foot_l', + ...(skirtKeys ? ['skirt_r0', 'skirt_c0', 'skirt_l0'] : [])]; 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(', ')}`); + const feet = lacking.includes('foot_r') || lacking.includes('foot_l') + ? ' (no foot_r/foot_l: the shoes are one part, which no leg bone moves; see step 7)' : ''; + console.error(`hello.ts: ${rigPath} has no bone ${lacking.join(', ')}; the hello keys all of ${need.join(', ')}${feet}`); process.exit(1); } for (const leg of ['leg_r', 'leg_l']) { if (bones[`${leg}0`].parent !== 'hip' || bones[`${leg}1`].parent !== `${leg}0`) { - console.error(`hello.ts: ${leg}0 must hang from hip and ${leg}1 from ${leg}0 for the weight shift`); + console.error(`hello.ts: ${leg}0 must hang from hip and ${leg}1 from ${leg}0 for the settle`); process.exit(1); } } +if (bones.hip.rotation) { + console.error('hello.ts: the hip is turned at rest; the settle slides the foot along the hip\'s x axis, which is then not the floor'); + process.exit(1); +} -// The beat sheet is written for 4 s; another --seconds stretches it evenly. -const k = seconds / 4; +// The beat sheet is written for 4.75 s; another --seconds stretches it evenly. +const k = seconds / SHEET; const smooth = (a: number, b: number, t: number) => { const x = Math.min(1, Math.max(0, (t - a) / (b - a))); return x * x * (3 - 2 * x); @@ -98,16 +119,17 @@ const twoLifts = (t: number, a: number, b: number) => const UPPER_ARM = -4; // degrees: sleeve_r0, out from her side; the limit above const FOREARM = -125; // degrees: sleeve_r1, the forearm up beside the shoulder const WAVE = 15; // degrees each way, twice a second -const SHIFT = -6; // px: hip x, toward the image's left, onto her right foot; the limit above -const SKIRT = 1.66; // degrees: skirt_*0 following the hip +const STANCE = 0.4; // of the way from the painted stance to parallel: the right ankle's slide; the limit above +const SHIFT = -6; // px: hip x, toward the picture's left, over the feet's new middle; the limit above +const SKIRT = 1.66; // degrees: skirt_c0, skirt_l0 following the hip (no physics) -// Settle: out over 3.30–3.65 s, back over 3.65–4.00 s. -const settle = (t: number) => smooth(3.3, 3.65, t) - smooth(3.65, 4.0, t); +// Settle: out over 3.15–3.75 s, held to 4.15 s, back over 4.15–4.75 s. +const settle = (t: number) => smooth(3.15, 3.75, t) - smooth(4.15, 4.75, t); -// Two-bone leg IK in the hip's frame (the hip has no rotation): the hip moves -// by dx, the ankle stays put, the knee keeps the side it was painted on. +// Legs, in the hip's frame (the hip has no rotation, so its x axis is the floor). const rad = Math.PI / 180; -const legIK = (leg: string, dx: number): [number, number] => { +type Leg = { R: [number, number]; A: [number, number]; L0: number; L1: number; r0: number; r1: number }; +const legOf = (leg: string): Leg => { const b0 = bones[`${leg}0`]; const b1 = bones[`${leg}1`]; const L0 = b0.length ?? 0; @@ -117,8 +139,21 @@ const legIK = (leg: string, dx: number): [number, number] => { const R: [number, number] = [b0.x ?? 0, b0.y ?? 0]; const K: [number, number] = [R[0] + L0 * Math.cos(r0), R[1] + L0 * Math.sin(r0)]; const A: [number, number] = [K[0] + L1 * Math.cos(r0 + r1), K[1] + L1 * Math.sin(r0 + r1)]; + return { R, A, L0, L1, r0, r1 }; +}; +const legs = { leg_r: legOf('leg_r'), leg_l: legOf('leg_l') }; +// Hip-to-ankle angle from straight down, degrees; + leans the ankle toward the picture's left. +const tilt = (l: Leg, ax = l.A[0]) => Math.atan2(l.R[0] - ax, l.R[1] - l.A[1]) / rad; +// The right ankle's x where its hip-to-ankle line is parallel to the left leg's, on the same floor. +const parallelX = legs.leg_r.R[0] - (legs.leg_r.R[1] - legs.leg_r.A[1]) * Math.tan(tilt(legs.leg_l) * rad); +const inward = STANCE * (parallelX - legs.leg_r.A[0]); + +// Two-bone IK: the hip moves by dx, the ankle by ax along the floor; the knee +// keeps the side it was painted on. Returns the keys for 0 and 1. +const legIK = (leg: 'leg_r' | 'leg_l', dx: number, ax: number): [number, number] => { + const { R, A, L0, L1, r0, r1 } = legs[leg]; // The ankle, seen from the moved hip. - const Ax = A[0] - dx; + const Ax = A[0] + ax - dx; const Ay = A[1]; const d = Math.hypot(Ax - R[0], Ay - R[1]); if (d > L0 + L1) { @@ -134,6 +169,8 @@ const legIK = (leg: string, dx: number): [number, number] => { const wrap = (a: number) => Math.atan2(Math.sin(a), Math.cos(a)) / rad; return [wrap(n0 - r0), wrap(n1 - r1)]; }; +const right = (t: number) => legIK('leg_r', SHIFT * settle(t), inward * settle(t)); +const left = (t: number) => legIK('leg_l', SHIFT * settle(t), 0); type Curve = (t: number) => number | number[]; const curves: [string, string, Curve, boolean?][] = [ @@ -151,11 +188,17 @@ const curves: [string, string, Curve, boolean?][] = [ ramp(t, [[0, 0], [1.9, 0], [2.2, FOREARM], [3.1, FOREARM], [3.4, 0]]) + (t >= 2.2 && t <= 3.1 ? WAVE * Math.sin(2 * Math.PI * 2 * (t - 2.2)) * smooth(2.2, 2.3, t) * (1 - smooth(3.0, 3.1, t)) : 0)], ['hip', 'translate', (t) => [SHIFT * settle(t), 0]], - ['leg_r0', 'rotate', (t) => legIK('leg_r', SHIFT * settle(t))[0]], - ['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.map((b): [string, string, Curve] => [b, 'rotate', (t) => SKIRT * settle(t)]), + ['leg_r0', 'rotate', (t) => right(t)[0]], + ['leg_r1', 'rotate', (t) => right(t)[1]], + ['leg_l0', 'rotate', (t) => left(t)[0]], + ['leg_l1', 'rotate', (t) => left(t)[1]], + ...(skirtKeys + ? [ + ['skirt_r0', 'rotate', (t: number) => right(t)[0]], + ['skirt_c0', 'rotate', (t: number) => SKIRT * settle(t)], + ['skirt_l0', 'rotate', (t: number) => SKIRT * settle(t)], + ] as [string, string, Curve][] + : [['skirt0_0', 'rotate', (t: number) => right(t)[0]]] as [string, string, Curve][]), ]; const n = Math.round(seconds * fps); @@ -181,17 +224,19 @@ const doc = { hello: { duration: seconds, loop: true, - note: `scripts/hello.ts over ${rigPath}: hm? (head straightens, brows up), heh (eyes narrowed, mouth wide, two shoulder bobs), an elbow-down wave, a weight-shift settle; ${seconds} s at ${fps} fps`, + note: `scripts/hello.ts over ${rigPath}: hm? (head straightens, brows up), heh (eyes narrowed, mouth wide, two shoulder bobs), an elbow-down wave, a settle (the right foot 40 % of the way to parallel, the hip after it); ${seconds} s at ${fps} fps`, tracks, }, }, }; writeFileSync(outPath, JSON.stringify(doc) + '\n'); -const [r0, r1] = legIK('leg_r', SHIFT); -const [l0, l1] = legIK('leg_l', SHIFT); +const [r0, r1] = legIK('leg_r', SHIFT, inward); +const [l0, l1] = legIK('leg_l', SHIFT, 0); 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${skirtKeys ? '' : ', skirt left to physics'} ` + - `(legs r ${f2(r0)}/${f2(r1)}, l ${f2(l0)}/${f2(l1)} deg, ankles held) -> ${outPath}`, + `sleeve_r0 at most ${maxUpper.toFixed(1)} deg; settle: right ankle ${inward.toFixed(1)} px in ` + + `(${Math.round(STANCE * 100)} % of the way to parallel, ${tilt(legs.leg_r).toFixed(2)} -> ${tilt(legs.leg_r, legs.leg_r.A[0] + inward).toFixed(2)} deg), ` + + `hip ${maxShift.toFixed(1)} px (legs r ${f2(r0)}/${f2(r1)}, l ${f2(l0)}/${f2(l1)} deg, feet on the floor); ` + + `${skirtKeys ? 'skirt_r0 follows the thigh' : 'skirt0_0 follows the thigh, the rest left to physics'} -> ${outPath}`, );