spine-rigc 0.16.0 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cli.ts +71 -2
- package/docs/AUTHORING.md +204 -87
- package/docs/FACE.md +133 -0
- package/package.json +1 -1
- package/src/compile.ts +512 -3
- package/src/deformgen.ts +107 -12
- package/src/depth.ts +493 -0
- package/src/mesh.ts +104 -1
- package/src/rig.ts +124 -0
- package/src/types.ts +51 -0
- package/src/validate.ts +47 -0
package/cli.ts
CHANGED
|
@@ -114,6 +114,7 @@ import {
|
|
|
114
114
|
} from './src/render.ts';
|
|
115
115
|
import { CLI_DEFAULT_PROFILE, reportLines, validate, VALIDATE_PROFILES, type ValidateProfile } from './src/validate.ts';
|
|
116
116
|
import { parseMotionSpec } from './src/motion.ts';
|
|
117
|
+
import type { FoldLimit, TurnCeiling } from './src/depth.ts';
|
|
117
118
|
import type { CompileResult } from './src/types.ts';
|
|
118
119
|
|
|
119
120
|
/**
|
|
@@ -407,10 +408,72 @@ function runGate(
|
|
|
407
408
|
* unconditionally, so a build whose only mesh was a ribbon or a contour got a
|
|
408
409
|
* sentence about a rim ring and a seam it does not have.
|
|
409
410
|
*/
|
|
411
|
+
/**
|
|
412
|
+
* What a depth map and a soft region put on a mesh, when it named either.
|
|
413
|
+
*
|
|
414
|
+
* The digests are the reason this prints at all: a claim about a rig can name
|
|
415
|
+
* WHICH sheet produced it, and two runs a reader believes differ can be shown to
|
|
416
|
+
* have read the same pixels. The ranges and counts are what say the input
|
|
417
|
+
* reached the geometry rather than merely being resolved — a `carried 0` never
|
|
418
|
+
* gets here (it is refused) and a `ramped 0` is a hard-edged mask, which is
|
|
419
|
+
* legal and usually not what somebody meant.
|
|
420
|
+
*/
|
|
421
|
+
/**
|
|
422
|
+
* One axis's two ceilings, as `+31.41 / -18.03`, or what is unbounded on it.
|
|
423
|
+
*
|
|
424
|
+
* ⚠️ `none` and a number are different claims and are printed differently. A
|
|
425
|
+
* sheet with no gradient along an axis cannot fold anything on it AT ANY ANGLE,
|
|
426
|
+
* which is a fact about the sheet worth reading; printing `90` for it would be
|
|
427
|
+
* a limit nothing measured.
|
|
428
|
+
*/
|
|
429
|
+
function ceilingPair(axis: { positive: FoldLimit | null; negative: FoldLimit | null }): string {
|
|
430
|
+
const one = (l: FoldLimit | null, sign: string) => (l === null ? `${sign}none` : `${sign}${l.degrees.toFixed(2)}°`);
|
|
431
|
+
return `${one(axis.positive, '+')} / ${one(axis.negative, '-')}`;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/** The tightest of the four, so the line that names a triangle names the right one. */
|
|
435
|
+
function tightestFold(c: TurnCeiling): { kind: string; sign: string; limit: FoldLimit } | null {
|
|
436
|
+
const all = [
|
|
437
|
+
{ kind: 'yaw', sign: '+', limit: c.yaw.positive },
|
|
438
|
+
{ kind: 'yaw', sign: '-', limit: c.yaw.negative },
|
|
439
|
+
{ kind: 'pitch', sign: '+', limit: c.pitch.positive },
|
|
440
|
+
{ kind: 'pitch', sign: '-', limit: c.pitch.negative },
|
|
441
|
+
].filter((e): e is { kind: string; sign: string; limit: FoldLimit } => e.limit !== null);
|
|
442
|
+
if (all.length === 0) return null;
|
|
443
|
+
return all.reduce((best, e) => (e.limit.degrees < best.limit.degrees ? e : best));
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function meshDepthNote(m: CompileResult['meshes'][number]): string {
|
|
447
|
+
const parts: string[] = [];
|
|
448
|
+
if (m.depth) {
|
|
449
|
+
parts.push(
|
|
450
|
+
`depth "${m.depth.image}" ${m.depth.digest} near=${m.depth.near} zScale=${m.depth.zScale} ` +
|
|
451
|
+
`z=[${m.depth.range[0]}, ${m.depth.range[1]}]`,
|
|
452
|
+
);
|
|
453
|
+
const c = m.depth.ceiling;
|
|
454
|
+
parts.push(`turn ceiling yaw ${ceilingPair(c.yaw)} pitch ${ceilingPair(c.pitch)}`);
|
|
455
|
+
const worst = tightestFold(c);
|
|
456
|
+
parts.push(
|
|
457
|
+
worst === null
|
|
458
|
+
? ` nothing in this sheet folds: ${c.measured} triangle(s) measured, none with a depth gradient across it`
|
|
459
|
+
: ` first to fold: ${worst.kind} ${worst.sign} at ${worst.limit.degrees.toFixed(2)}°, ` +
|
|
460
|
+
`triangle ${worst.limit.triangle} [${worst.limit.ids.join(',')}]` +
|
|
461
|
+
`${c.degenerate ? `; ${c.degenerate} triangle(s) too flat in setup to measure` : ''}`,
|
|
462
|
+
);
|
|
463
|
+
}
|
|
464
|
+
if (m.soft) {
|
|
465
|
+
parts.push(
|
|
466
|
+
`soft "${m.soft.mask}" ${m.soft.digest} -> ${m.soft.bone}, ${m.soft.carried} carried / ${m.soft.ramped} in the falloff`,
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
return parts.length === 0 ? '' : `\n ${parts.join('\n ')}`;
|
|
470
|
+
}
|
|
471
|
+
|
|
410
472
|
const MESH_KIND_NOTES: Record<CompileResult['meshes'][number]['kind'], string> = {
|
|
411
473
|
ring: 'ring rim ring pinned on the window edge, seam ring pinned on the mask contour, aperture moves',
|
|
412
474
|
ribbon: 'ribbon entry row pinned, rows share their weights so the strip lengthens without widening',
|
|
413
475
|
contour: 'contour the art\'s own silhouette, every vertex pinned to the slot bone (geometry, not a deformation)',
|
|
476
|
+
grid: 'grid a lattice over the part window at stated column and row positions, every vertex pinned to the slot bone',
|
|
414
477
|
authored: 'authored geometry rigc did not build; it assumes nothing about the topology',
|
|
415
478
|
};
|
|
416
479
|
|
|
@@ -796,7 +859,8 @@ function cmdBuild(flags: Record<string, string>): void {
|
|
|
796
859
|
for (const m of result.meshes) {
|
|
797
860
|
console.log(
|
|
798
861
|
` MESH ${m.slot.padEnd(12)} ${m.kind.padEnd(8)} ${m.vertices} vertices / ${m.triangles} triangles ` +
|
|
799
|
-
`${meshBudget(result.rig)} bones=[${m.bones.join(', ')}] attachments=[${m.attachments.join(', ')}]${meshFit(m)}
|
|
862
|
+
`${meshBudget(result.rig)} bones=[${m.bones.join(', ')}] attachments=[${m.attachments.join(', ')}]${meshFit(m)}` +
|
|
863
|
+
meshDepthNote(m),
|
|
800
864
|
);
|
|
801
865
|
}
|
|
802
866
|
for (const ph of result.physics) {
|
|
@@ -2223,9 +2287,14 @@ function cmdExplain(flags: Record<string, string>): void {
|
|
|
2223
2287
|
console.log('\nmeshes');
|
|
2224
2288
|
for (const kind of new Set(result.meshes.map((m) => m.kind))) console.log(` ${MESH_KIND_NOTES[kind]}`);
|
|
2225
2289
|
for (const m of result.meshes) {
|
|
2290
|
+
// The depth block belongs here more than it belongs in `build`: `explain`
|
|
2291
|
+
// is the command that says what a spec MEANS, and the turn ceiling is the
|
|
2292
|
+
// number an author needs before writing a key rather than after a refusal.
|
|
2293
|
+
// It was absent, while `docs/AUTHORING.md` said both commands printed it.
|
|
2226
2294
|
console.log(
|
|
2227
2295
|
` ${m.slot.padEnd(12)} ${m.kind.padEnd(8)} ${m.vertices} vertices / ${m.triangles} triangles ` +
|
|
2228
|
-
`${meshBudget(result.rig)} bones=[${m.bones.join(', ')}]${meshFit(m)}
|
|
2296
|
+
`${meshBudget(result.rig)} bones=[${m.bones.join(', ')}]${meshFit(m)}` +
|
|
2297
|
+
meshDepthNote(m),
|
|
2229
2298
|
);
|
|
2230
2299
|
}
|
|
2231
2300
|
}
|
package/docs/AUTHORING.md
CHANGED
|
@@ -788,9 +788,11 @@ silhouette, and an octagon's sides pass `R · cos(π/8)` from its centre, so 5.7
|
|
|
788
788
|
the drawing — its whole ink outline, between the spokes — was not going to be
|
|
789
789
|
drawn, and every assertion passed (issue #277).
|
|
790
790
|
|
|
791
|
-
The generators are `ring`, `ribbon` and `
|
|
791
|
+
The generators are `ring`, `ribbon`, `contour` and `grid` (see
|
|
792
792
|
[`src/mesh.ts`](../src/mesh.ts)); the first two encode a deformation model rather
|
|
793
|
-
than a table of numbers, which is why they are code invoked by data.
|
|
793
|
+
than a table of numbers, which is why they are code invoked by data. The last two
|
|
794
|
+
are geometry: they pin every vertex to the slot bone and exist to give a
|
|
795
|
+
`deform` timeline (§4.12) somewhere to push. A generator
|
|
794
796
|
is for a skeleton with **no** manifest; a cut that has one invokes the same
|
|
795
797
|
builders through the manifest's `mesh` block.
|
|
796
798
|
|
|
@@ -910,109 +912,206 @@ Self-intersection is refused; **holes are not cut out**; and nothing here does
|
|
|
910
912
|
interior/Steiner points, so a contour mesh bends only where its outline has
|
|
911
913
|
vertices.
|
|
912
914
|
|
|
913
|
-
|
|
914
|
-
the `tolerance`/`margin` sweep that picked their settings and what each one measured.
|
|
915
|
+
#### `grid` — a lattice over the part window
|
|
915
916
|
|
|
916
|
-
**
|
|
917
|
-
and
|
|
918
|
-
|
|
917
|
+
**When you need one:** the part is a plate whose *surface* moves — a face that
|
|
918
|
+
turns, a cloth that ripples — and a `deform` model (§4.12) needs interior
|
|
919
|
+
vertices to push. A `contour` gives you the silhouette and nothing inside it;
|
|
920
|
+
this gives you columns and rows.
|
|
919
921
|
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
carrying it up to and including `end` is clipped to the polygon, so a window, a
|
|
924
|
-
portal or a wipe is one attachment rather than a second set of art.
|
|
922
|
+
It takes **no geometry and no size**: like a `contour`, the window is the
|
|
923
|
+
attachment's own `image`, and every vertex is pinned to the slot bone at weight
|
|
924
|
+
1, so an undeformed grid draws exactly what the region drew.
|
|
925
925
|
|
|
926
926
|
```json
|
|
927
|
-
"
|
|
928
|
-
|
|
929
|
-
"mask_a": { "mask_a": { "type": "clipping", "end": "box", "vertexCount": 3,
|
|
930
|
-
"vertices": [0, 0, 200, 0, 0, 160],
|
|
931
|
-
"color": "ff00ffff" } }
|
|
927
|
+
"generator": { "kind": "grid", "us": [0.0235, 0.1471, 0.5, 0.8529, 0.9765],
|
|
928
|
+
"vs": [0.0263, 0.2632, 0.5, 0.7368, 0.9737] }
|
|
932
929
|
```
|
|
933
930
|
|
|
934
|
-
Both polygons above are invented — an axis-aligned rectangle and a right triangle,
|
|
935
|
-
in round numbers, so that nothing here can be mistaken for a shape measured off a
|
|
936
|
-
reference. A real one is measured off your own art (§8) or drawn to the volume the
|
|
937
|
-
game needs.
|
|
938
|
-
|
|
939
931
|
| Field | Meaning |
|
|
940
932
|
| --- | --- |
|
|
941
|
-
| `
|
|
942
|
-
| `
|
|
943
|
-
| `
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
933
|
+
| `us` / `vs` | column and row positions across the window, `0..1`, ascending, at least two each. **Positions, not a count** |
|
|
934
|
+
| `cols` / `rows` | an even division of the whole window instead. Refused beside `us`/`vs` |
|
|
935
|
+
| `depth` | a depth map, below — this is the generator it was built for |
|
|
936
|
+
|
|
937
|
+
⭐ **Positions, because [FACE §4.1](FACE.md) places columns where the drawing
|
|
938
|
+
needs them.** The five above are `gallery/portrait`'s own — dense at the
|
|
939
|
+
silhouette, sparse across the middle, and not reaching the window edge. A
|
|
940
|
+
generator that could only divide evenly would be a step backwards from the table
|
|
941
|
+
it replaces, and it does replace it: the selftest builds that exact mesh from
|
|
942
|
+
those five numbers and requires it to come out identical to the 25 vertex pairs
|
|
943
|
+
and 32 triangles the example shipped by hand.
|
|
944
|
+
|
|
945
|
+
🚨 **The reason to generate this at all is the hull.** Spine's `hull` is a
|
|
946
|
+
**count** — the first `hull` entries of the vertex list are the outline — so the
|
|
947
|
+
perimeter must be listed first and in walk order (top row, right column, bottom
|
|
948
|
+
row, left column), interior after. A hand-numbered grid written a row at a time
|
|
949
|
+
puts interior vertices in the hull, and the mesh loads, draws and deforms wrong
|
|
950
|
+
with nothing anywhere to say so. The generator satisfies that by construction
|
|
951
|
+
rather than being checked afterwards.
|
|
952
|
+
|
|
953
|
+
| The input | What you get |
|
|
954
|
+
| --- | --- |
|
|
955
|
+
| positions **and** a count | `states both positions ("us"/"vs") and a count ("cols"/"rows") … Drop one` |
|
|
956
|
+
| neither | `A lattice is not a default` |
|
|
957
|
+
| one axis only | `a lattice needs both axes` |
|
|
958
|
+
| positions that do not ascend | `"us" is not ascending: [1]=0.6 and [2]=0.6` — equal neighbours put two vertices in one place and collapse a row of triangles to zero area |
|
|
959
|
+
| a position outside `0..1` | `positions are fractions of the part window, 0..1` |
|
|
960
|
+
| `cols` or `rows` under 2 | `it is a whole number of at least 2` |
|
|
956
961
|
|
|
957
|
-
|
|
958
|
-
`skeletonData.findSlot` returns `null` on a miss and the parser assigns that null
|
|
959
|
-
without a word, so the clip never ends — it runs to the bottom of the draw order
|
|
960
|
-
and takes every slot below it out of the frame. rigc refuses a name the rig does
|
|
961
|
-
not declare. Omitting `end` entirely is the format's own way of saying "clip
|
|
962
|
-
everything after this one", and is left alone.
|
|
962
|
+
#### `depth` — give every vertex its own z, instead of one cylinder radius
|
|
963
963
|
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
was supposed to hide something would not. It is valid Spine and the default
|
|
967
|
-
`--profile spine` accepts it; the refusal is policy, not validity.
|
|
964
|
+
**On a `contour` or a `grid`.** A grid is the one it was built for: a turn
|
|
965
|
+
needs interior vertices to move, and a contour has none of its own.
|
|
968
966
|
|
|
969
|
-
|
|
970
|
-
|
|
967
|
+
A `yaw` or `pitch` key (§4.12) turns a part by treating it as painted on a
|
|
968
|
+
cylinder: a vertex `u` off the axis gets `z = √(radius² − u²)`. That is the right
|
|
969
|
+
model for a fringe or a plate that really does bend like a barrel, and the wrong
|
|
970
|
+
one for a face — a nose is not on the skull's cylinder and an ear is behind it,
|
|
971
|
+
and no single radius puts both where they are.
|
|
971
972
|
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
bone like any other vertex attachment, so an animated path is a moving track.
|
|
973
|
+
Name a **depth map** on the generator and every vertex gets its own `z`, sampled
|
|
974
|
+
off a greyscale sheet in the part's own pixel grid:
|
|
975
975
|
|
|
976
976
|
```json
|
|
977
|
-
"
|
|
978
|
-
|
|
979
|
-
|
|
977
|
+
"generator": {
|
|
978
|
+
"kind": "contour", "tolerance": 1.5, "margin": 2,
|
|
979
|
+
"depth": { "image": "face_depth.png", "near": "white", "zScale": 40 }
|
|
980
|
+
}
|
|
980
981
|
```
|
|
981
982
|
|
|
982
|
-
That is nine invented points forming a straight line: knots at x = 0, 90 and 180
|
|
983
|
-
with their handles evenly spaced between them, which is the simplest path there is
|
|
984
|
-
and the one worth reading twice.
|
|
985
|
-
|
|
986
983
|
| Field | Meaning |
|
|
987
984
|
| --- | --- |
|
|
988
|
-
| `
|
|
989
|
-
| `
|
|
990
|
-
| `
|
|
991
|
-
| `
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
985
|
+
| `image` | **required.** The sheet, relative to the rig's `images` directory, and the **same pixel size as this attachment's `image`**. It is not packed into the atlas — it is a measurement rigc reads at compile time, not art anything draws |
|
|
986
|
+
| `near` | **required.** `"white"` or `"black"` — which end of the range is closest to the viewer. Stated rather than defaulted: both conventions are in use, and a sheet read with the wrong one turns the part inside out with every gate still green |
|
|
987
|
+
| `zScale` | **required.** How many world units the map's full range spans, in the attachment's own units — the number `radius` used to carry. 8 bits of level say nothing about scale, so this is authored, never measured |
|
|
988
|
+
| `gamma`, `contrast`, `bias` | the tone curve applied to the nearness, defaults `1` / `1` / `0`. State them when a consumer's own renderer curves the same sheet, so the mesh and that renderer describe one surface |
|
|
989
|
+
|
|
990
|
+
The order is fixed and it matters, because every step is a place two
|
|
991
|
+
implementations can silently disagree: **bilinear sample of the raw level** (at
|
|
992
|
+
pixel centres, which is what a GPU's linear filter does), then `near`, then the
|
|
993
|
+
curve clamped back into 0..1, then `zScale`. Naming a map changes no emitted byte
|
|
994
|
+
on its own — a `yaw` or `pitch` key has to ask for it with `"depth": true`.
|
|
995
|
+
|
|
996
|
+
`build` and `explain` report the sheet's digest (over the levels, so a re-encode
|
|
997
|
+
of the same map reads the same) and the `z` range actually sampled, which is the
|
|
998
|
+
number that says whether the map covers the part or a corner of it.
|
|
999
|
+
|
|
1000
|
+
⭐ **And the TURN CEILING — the angle to write on the key, before you write it.**
|
|
1001
|
+
|
|
1002
|
+
```
|
|
1003
|
+
MESH face grid 1089 vertices / 2048 triangles (budget 3000) bones=[face]
|
|
1004
|
+
depth "face_depth.png" f552a2f50d21 near=white zScale=60 z=[0, 60]
|
|
1005
|
+
turn ceiling yaw +31.41° / -32.01° pitch +32.01° / -31.41°
|
|
1006
|
+
first to fold: yaw + at 31.41°, triangle 960 [113,112,593]
|
|
1007
|
+
```
|
|
1008
|
+
|
|
1009
|
+
Past that angle a triangle turns inside out and `A39` refuses the build by name.
|
|
1010
|
+
The loop this replaces is *pick an angle, build, read the refusal, guess again*.
|
|
1011
|
+
|
|
1012
|
+
The arithmetic is exact and worth knowing, because it tells you what to change.
|
|
1013
|
+
A `yaw` sends each vertex to `x' = u·cos t − z·sin t`, so a triangle's area is
|
|
1014
|
+
`A₀·cos t − A_yaw·sin t` where `A_yaw` is the same area with **z substituted for
|
|
1015
|
+
u** — and it reaches zero at `tan t = A₀/A_yaw`. Three consequences:
|
|
1016
|
+
|
|
1017
|
+
- ⚠️ **The ceiling is a property of the SHEET, not of the mesh.** Over a smooth
|
|
1018
|
+
map the limit approaches `1 / max|dz/du|`, the reciprocal of its steepest
|
|
1019
|
+
slope, with no mesh term in it. Refining the lattice does not lower the angle
|
|
1020
|
+
— it finds slopes that were always there. So a ceiling you cannot live with is
|
|
1021
|
+
fixed by editing the depth map, not by meshing differently:
|
|
1022
|
+
[`docs/FACE.md` §2.2](FACE.md) has the measured ladder and the rule.
|
|
1023
|
+
- **The four numbers are four different answers**, each from its own triangle.
|
|
1024
|
+
A part that turns 30° left and 18° right is ordinary, not an anomaly.
|
|
1025
|
+
- **`none` means no gradient on that axis**, and is a different statement from a
|
|
1026
|
+
large number. A sheet varying linearly along one axis cannot fold the other at
|
|
1027
|
+
*any* angle — `A_pitch` is then identically zero — so a linear ramp reports a
|
|
1028
|
+
yaw ceiling and `pitch +none / -none`.
|
|
1029
|
+
|
|
1030
|
+
⛔ It is a **report and never a refusal**. `A39` owns the refusal, from the
|
|
1031
|
+
artifact and through the runtime; a second wall here would be the compiler
|
|
1032
|
+
inventing a policy out of a measurement. What holds the two together is a
|
|
1033
|
+
control: `TC01` requires this number to be the angle `A39` actually fires at, on
|
|
1034
|
+
the triangle `A39` actually names, to 0.01°.
|
|
1035
|
+
|
|
1036
|
+
🚨 **The one that will catch you: the sheet has to cover the mesh, and usually
|
|
1037
|
+
it does not.** A contour mesh puts every vertex *on* the silhouette and pushes it
|
|
1038
|
+
`margin` pixels outside; a grid spans the whole window, corners included. A depth
|
|
1039
|
+
sheet is usually cut to the art's own alpha, so both reach past it — and a naive
|
|
1040
|
+
sample gives those vertices the background depth and folds them away from the
|
|
1041
|
+
turn, with correct arithmetic and plausible numbers all the way down. rigc
|
|
1042
|
+
refuses it instead:
|
|
1043
|
+
|
|
1044
|
+
| The input | What you get |
|
|
1045
|
+
| --- | --- |
|
|
1046
|
+
| a sheet cut to the art's alpha, on a **contour** | `does not cover 12 of the mesh's 12 vertices … A contour mesh puts every vertex ON the silhouette and pushes it out by the margin … Dilate the sheet past the mesh margin, or lower the margin.` |
|
|
1047
|
+
| a sheet cut to the art's alpha, on a **grid** | `does not cover 36 of the mesh's 81 vertices … A grid spans the whole part window, corners included … Dilate the sheet to the window, or state "us"/"vs" that keep the lattice inside the art.` — the two topologies run out of sheet for different reasons, and the message says which |
|
|
1048
|
+
| a sheet that is not the part's size | `the depth map … is 32x32 and the part is 64x64. A depth map is sampled in the part's own pixel grid` |
|
|
1049
|
+
| a colour sheet | `the depth map … is not greyscale — pixel (0, 0) is rgb(10, 200, 10)` |
|
|
1050
|
+
| `zScale` at or below 0 | `it is how many units the map's full range spans, so a positive number. To put the near end at the back, say "near": "black"` |
|
|
1051
|
+
| `gamma` or `contrast` at or below 0 | `collapses the range onto the midpoint … so the map would describe a flat part` |
|
|
1052
|
+
| a `near` that is neither | `it is "white" or "black"` |
|
|
1053
|
+
|
|
1054
|
+
A sheet with **no alpha channel** covers its whole grid by construction and the
|
|
1055
|
+
coverage check has nothing to test; what its background level means is then your
|
|
1056
|
+
statement, and the reported range is where it shows up.
|
|
1057
|
+
|
|
1058
|
+
##### `soft` — which part is soft, and which bone carries it
|
|
1059
|
+
|
|
1060
|
+
A mesh can say **which vertices are soft**, so a `physics` constraint (§3.5) on
|
|
1061
|
+
their bone answers an impact over exactly that region — a chest, a cheek, a
|
|
1062
|
+
hanging sleeve.
|
|
1063
|
+
|
|
1064
|
+
```json
|
|
1065
|
+
"generator": {
|
|
1066
|
+
"kind": "grid", "cols": 9, "rows": 9,
|
|
1067
|
+
"depth": { "image": "face_depth.png", "near": "white", "zScale": 40 },
|
|
1068
|
+
"soft": { "bone": "cheek_wobble", "mask": "cheek_soft.png" }
|
|
1069
|
+
}
|
|
1070
|
+
```
|
|
1015
1071
|
|
|
1072
|
+
| Field | Meaning |
|
|
1073
|
+
| --- | --- |
|
|
1074
|
+
| `bone` | **required.** The bone the region is carried by. It has to already exist — a bone a physics constraint targets is part of the skeleton, not a side effect of a mesh |
|
|
1075
|
+
| `mask` | **required.** A greyscale sheet in the part's own pixel grid: the level IS the weight, black still and white fully carried, sampled at each vertex. Alpha is not read — a transparent pixel is black |
|
|
1076
|
+
|
|
1077
|
+
The remainder always stays on the slot bone, so every vertex closes at 1 by
|
|
1078
|
+
construction rather than by `A20` catching it later. `build` and `explain` report
|
|
1079
|
+
the mask's digest, how many vertices were carried outright and how many landed in
|
|
1080
|
+
the painted falloff.
|
|
1081
|
+
|
|
1082
|
+
🚨 **Why a painted mask and not a depth threshold.** rigc tried the threshold —
|
|
1083
|
+
"the near part wobbles" — for exactly one day. It is wrong, and instructively so:
|
|
1084
|
+
**softness and prominence are different properties of a drawing.** The most
|
|
1085
|
+
prominent thing on a face is the nose, and a nose does not wobble. A threshold
|
|
1086
|
+
produced a region that was plausible, gated green and carried the wrong pixels.
|
|
1087
|
+
It also claimed something untrue — "no mask painted" — while the renderer this
|
|
1088
|
+
was modelled on had a hand-painted spring mask all along. rigc does not get to
|
|
1089
|
+
delete an input by guessing it.
|
|
1090
|
+
|
|
1091
|
+
⭐ The falloff is painted for the same reason. A `feather` parameter would be
|
|
1092
|
+
rigc guessing the shape of something you can simply draw.
|
|
1093
|
+
|
|
1094
|
+
**`A21_MESH_RIM_PINNED` splits on this rather than being relaxed.** A wobbling
|
|
1095
|
+
silhouette is *supposed* to move; the invariant is that nothing else does. Every
|
|
1096
|
+
vertex must be pinned to the slot bone or shared between it and the one declared
|
|
1097
|
+
bone, must close at 1, and at least one must actually be carried.
|
|
1098
|
+
|
|
1099
|
+
| The input | What you get |
|
|
1100
|
+
| --- | --- |
|
|
1101
|
+
| a bone the rig does not declare | `names bone "x", which this rig does not declare … a bone a physics constraint has to target is part of the skeleton` |
|
|
1102
|
+
| the slot's own bone | `moves nothing — a soft region needs a bone that can move independently` |
|
|
1103
|
+
| a mask that is black everywhere | `carries no vertex of this mesh — every one of its 49 vertices samples black` |
|
|
1104
|
+
| a colour mask | `is not greyscale — pixel (0, 0) is rgb(10, 200, 10)` |
|
|
1105
|
+
| a mask that is not the part's size | `is 48x32 and the part is 96x64` |
|
|
1106
|
+
| a mask that is not on disk | `the soft mask "x.png" is not at …` |
|
|
1107
|
+
|
|
1108
|
+
🚨 **What it cannot do yet.** A carried mesh has two bones on some vertices, and
|
|
1109
|
+
a `transform` key (§4.12) needs one coordinate space to be evaluated in — so the
|
|
1110
|
+
**angle** a raised surface turns through and the **impact** a soft one answers
|
|
1111
|
+
cannot ride the *same* attachment today. `SF03` holds that refusal, so the day it
|
|
1112
|
+
changes something fails and says so;
|
|
1113
|
+
[#389](https://github.com/firejune/rigc/issues/389) has the arithmetic showing it
|
|
1114
|
+
need not be true. Until then, put the two on separate slots.
|
|
1016
1115
|
#### 3.4.1 A skin that switches bones and constraints on
|
|
1017
1116
|
|
|
1018
1117
|
**When you need one:** a skin that is more than a change of art — a variant with an
|
|
@@ -2082,7 +2181,7 @@ example needed it:
|
|
|
2082
2181
|
|
|
2083
2182
|
| `kind` | Parameters | What it evaluates | Worked case |
|
|
2084
2183
|
| --- | --- | --- | --- |
|
|
2085
|
-
| `yaw` | `radius`, `degrees`, `about` | `dx = (x−about)·(cos t − 1) − z·sin t`, `z = √(radius² − (x−about)²)` — the 2.5D turn (FACE §1) | `gallery/portrait` |
|
|
2184
|
+
| `yaw` | `radius` **or** `depth`, `degrees`, `about` | `dx = (x−about)·(cos t − 1) − z·sin t`, with `z = √(radius² − (x−about)²)` from a cylinder or `z` read per vertex off a depth map — the 2.5D turn (FACE §1) | `gallery/portrait` |
|
|
2086
2185
|
| `pitch` | the same | the same expression with `y` for `x` — a nod rather than a turn | `gallery/nod` |
|
|
2087
2186
|
| `affine` | `scale`, `about` | `dx = (sx−1)·(x−ax)`, `dy = (sy−1)·(y−ay)` — a scale about a fixed point | `gallery/squash` |
|
|
2088
2187
|
| `wave` | `amplitude`, `wavelength`, `phase`, `along`, `axis` | `d = amplitude · sin(2π·along/wavelength + phase)` | `gallery/nod` |
|
|
@@ -2105,6 +2204,24 @@ of a run leaves a **step at the run's edge**, and that is one half of the defect
|
|
|
2105
2204
|
[#313](https://github.com/firejune/rigc/issues/313) records. If you want a
|
|
2106
2205
|
partial run, write it.
|
|
2107
2206
|
|
|
2207
|
+
**There is no `parallax` kind, and the reason is worth stating.** A pure depth
|
|
2208
|
+
slide — `d = z · offset`, no angle — is this form with a term dropped: subtract
|
|
2209
|
+
them and the whole remainder is `u·(cos t − 1)`, independent of the depth, so
|
|
2210
|
+
the slide *is* a turn at a small angle (at 16° the gap is 2.32px, at 1°
|
|
2211
|
+
0.0091px, quartering with each halving). 🚨 And what it is for is not rigc's to
|
|
2212
|
+
state: a depth slide is a **camera** move, driven by a pointer rather than by a
|
|
2213
|
+
clock, and this spec is a timeline. What rigc states about a raised surface is
|
|
2214
|
+
the **angle** it may turn through, and — for a soft one — how it answers an
|
|
2215
|
+
**impact**. The camera belongs to whatever draws the result.
|
|
2216
|
+
|
|
2217
|
+
**A turn projects off one surface, stated one way.** `"depth": true` reads each
|
|
2218
|
+
vertex's `z` off the map its attachment's generator names (§3.4) instead of
|
|
2219
|
+
deriving it from a `radius`; the closed form does not change, only where `z`
|
|
2220
|
+
comes from. Saying both is refused — they are two answers to how far forward a
|
|
2221
|
+
vertex sits, and a key carrying both leaves a reader unable to say which one the
|
|
2222
|
+
output came from. So is `"depth": true` on an attachment that named no map,
|
|
2223
|
+
rather than a silent fall back to a cylinder.
|
|
2224
|
+
|
|
2108
2225
|
**It is not a `rigc tween`.** MOTION §7 refuses a command that generates
|
|
2109
2226
|
in-betweens and this is not one: the transform is evaluated **at one key**, from
|
|
2110
2227
|
parameters that key states, and what happens between two keys is still the
|
package/docs/FACE.md
CHANGED
|
@@ -245,6 +245,139 @@ shape the drawing only implies. Two readings that help:
|
|
|
245
245
|
measured 5.406 on the artifact. If the fringe does not read as separate,
|
|
246
246
|
this is the one number to move, and moving it moves nothing else.
|
|
247
247
|
|
|
248
|
+
### 2.1 One depth per part, and the limit of that
|
|
249
|
+
|
|
250
|
+
Everything above states **one depth per part** — a stand-off the whole plate
|
|
251
|
+
shares. That is the right resolution for a part list: the fringe is 26 in front
|
|
252
|
+
of the skull, and arguing about which *pixel* of the fringe is 26 would be
|
|
253
|
+
arguing past what the drawing says.
|
|
254
|
+
|
|
255
|
+
It stops being the right resolution the moment a part's own surface is the
|
|
256
|
+
subject. §4 meshes the face plate into columns precisely because the plate is not
|
|
257
|
+
flat, and §4.2 then finds that refining those columns makes the fold **worse**,
|
|
258
|
+
not better — the columns are sampling a cylinder that was never the shape of a
|
|
259
|
+
face. A cylinder is one number pretending to be a surface, and past a certain
|
|
260
|
+
density the pretence is what fails.
|
|
261
|
+
|
|
262
|
+
⭐ **Two things had to arrive together, and both are now generated rather than
|
|
263
|
+
written.** The lattice itself was hand-numbered — this example's 25 vertex pairs,
|
|
264
|
+
32 triangles and hull walk were a person's arithmetic — and
|
|
265
|
+
[AUTHORING §3.4](AUTHORING.md#grid--a-lattice-over-the-part-window)'s `grid`
|
|
266
|
+
generator now builds it from the column positions alone, reproducing this one
|
|
267
|
+
exactly. That is the prerequisite: a turn needs interior vertices to move, and
|
|
268
|
+
§4.3's contour has none.
|
|
269
|
+
|
|
270
|
+
⭐ **And a depth map is the number above becoming a surface.** A greyscale sheet in the
|
|
271
|
+
part's own pixel grid gives every mesh vertex its own `z`, sampled where the
|
|
272
|
+
vertex actually is, and `yaw`/`pitch` project off it with the same closed form as
|
|
273
|
+
§1 — only the source of `z` changes. What it buys is stated where it is authored:
|
|
274
|
+
[AUTHORING §3.4](AUTHORING.md#depth--give-every-vertex-its-own-z-instead-of-one-cylinder-radius),
|
|
275
|
+
with the sampling order, the refusals and the one that matters most (a sheet cut
|
|
276
|
+
to the art covers **none** of a contour mesh's vertices, because they all sit on
|
|
277
|
+
the silhouette and outside it).
|
|
278
|
+
|
|
279
|
+
⭐ **And the mesh really is evaluating the pixels' model — measured, not
|
|
280
|
+
asserted.** A consumer that renders the same sheet in a shader displaces every
|
|
281
|
+
PIXEL by its own depth; a mesh displaces vertices and interpolates across
|
|
282
|
+
triangles. On a dome (a ramp would prove nothing — linear interpolation is exact
|
|
283
|
+
on a linear field) at 18°, the two evaluations converge as the lattice refines:
|
|
284
|
+
|
|
285
|
+
| lattice | 3×3 | 5×5 | 9×9 | 17×17 | 33×33 |
|
|
286
|
+
| --- | --- | --- | --- | --- | --- |
|
|
287
|
+
| mean disagreement, px | 6.4141 | 1.8452 | 0.5887 | 0.2276 | **0.0926** |
|
|
288
|
+
| the same lattice reading a **cylinder** instead | 3.8972 | 3.1628 | 3.2558 | 3.3429 | **3.3777** |
|
|
289
|
+
|
|
290
|
+
🚨 **Read the second row before the first.** A mesh evaluating the wrong surface
|
|
291
|
+
does not converge — it settles at ~3.4px however dense it gets — and at 3×3 it
|
|
292
|
+
reads *better* than the right one. So one measurement at one density cannot tell
|
|
293
|
+
the two models apart, and would have picked the wrong one. The claim is the
|
|
294
|
+
convergence, never a single number. (`CS01`–`CS03` in `selftest.ts`; the worst
|
|
295
|
+
case falls more slowly than the mean and is expected to, because the dome's rim
|
|
296
|
+
has an unbounded depth gradient.)
|
|
297
|
+
|
|
298
|
+
⚠️ **This is not the same quantity as §4.2's fold angle, which refining makes
|
|
299
|
+
WORSE.** Both are true: a finer lattice buys fidelity to the model and costs the
|
|
300
|
+
angle at which a column pair inverts. This measures the first; `A39` refuses the
|
|
301
|
+
second.
|
|
302
|
+
|
|
303
|
+
⚠️ **It does not make depth measurable.** The map is relative — 8 bits of level
|
|
304
|
+
say nothing about world units — so `zScale` is authored exactly as the fringe's
|
|
305
|
+
26 was, and the two warnings above survive intact: get it right and the parallax
|
|
306
|
+
is free, get it wrong and nothing complains. What the map removes is the
|
|
307
|
+
*resolution* limit, not the judgement.
|
|
308
|
+
|
|
309
|
+
### 2.2 The angle belongs to the map, not to the mesh
|
|
310
|
+
|
|
311
|
+
§2.1 leaves an open question and it has now been measured. §4.2 finds that
|
|
312
|
+
refining the lattice makes the fold **worse**, and read that as the cylinder's
|
|
313
|
+
error surfacing — so per-vertex depth ought to have bought the angle back. ⛔ **It
|
|
314
|
+
does not, and it never could have.**
|
|
315
|
+
|
|
316
|
+
Two neighbouring vertices swap places when the turn tips one past the other,
|
|
317
|
+
which is `tan t ≥ Δu/Δz` — so the largest turn a part supports is
|
|
318
|
+
|
|
319
|
+
tan t_max = 1 / max |dz/du|
|
|
320
|
+
|
|
321
|
+
**the reciprocal of the steepest slope anywhere in its depth map**, and there is
|
|
322
|
+
no mesh in that formula at all. Refining the lattice does not change the angle;
|
|
323
|
+
it changes which slopes the lattice is close enough to *find*. A map with a
|
|
324
|
+
vertical edge has an infinite slope there, so a fine enough mesh folds at any
|
|
325
|
+
angle you name.
|
|
326
|
+
|
|
327
|
+
⭐ **Which is exactly what a dome is.** `z = Z√(1 − r²)` is vertical at its rim,
|
|
328
|
+
and an even lattice over it folds at `tan t = √h·√(R/2)/Z` for spacing
|
|
329
|
+
`h = W/(side−1)` — a **√h** that goes to zero. Measured against that closed form
|
|
330
|
+
over a 1,300× range of vertex counts, agreeing to ≤ 1.2°:
|
|
331
|
+
|
|
332
|
+
| lattice | 5×5 | 17×17 | 33×33 | 65×65 | 129×129 | 181×181 |
|
|
333
|
+
| --- | --- | --- | --- | --- | --- | --- |
|
|
334
|
+
| **dome**, largest turn admitted | 62° | 41° | 31° | 23° | 17° | **14°** |
|
|
335
|
+
| the closed form above | 59.0° | 39.8° | 30.5° | 22.6° | 16.4° | **14.0°** |
|
|
336
|
+
| **raised cosine**, slope bounded | 73° | 65° | 64° | 64° | 64° | **63°** |
|
|
337
|
+
| its closed form, `atan(2R/Zπ)` | 64.8° | 64.8° | 64.8° | 64.8° | 64.8° | **64.8°** |
|
|
338
|
+
|
|
339
|
+
⇒ **Author the map so its slope is bounded, and the angle stops depending on the
|
|
340
|
+
mesh.** A raised cosine — flat at the centre, flat again at the rim — holds
|
|
341
|
+
63–64° from 289 vertices to 32,761. The dome loses three quarters of its angle
|
|
342
|
+
over the same refinement. Both are "correct" depth; only one of them is a
|
|
343
|
+
*surface a turn can be built on*, and the difference is entirely in the input.
|
|
344
|
+
|
|
345
|
+
🚨 **So a map traced straight off a rendered normal or a photogrammetry pass is
|
|
346
|
+
the dome case, not the cosine case.** Where the part curves away to its
|
|
347
|
+
silhouette is where every such map goes vertical. Flattening it there — letting
|
|
348
|
+
z reach its floor *before* the outline rather than at it — is the edit that buys
|
|
349
|
+
the angle, and it is an edit to the sheet. rigc will not do it for you: the
|
|
350
|
+
compiler never invents a value that is not in the spec, and a depth map is a
|
|
351
|
+
measurement.
|
|
352
|
+
|
|
353
|
+
⚠️ **This does not touch §2.1's convergence.** A finer lattice still evaluates
|
|
354
|
+
the map's own surface more faithfully; it also finds steeper slopes in it. Those
|
|
355
|
+
are two different quantities and both are true — §2.1 measures the first, `A39`
|
|
356
|
+
refuses the second. What is new here is that the second is a fact about the
|
|
357
|
+
sheet, and can be fixed there.
|
|
358
|
+
|
|
359
|
+
⭐ **And you no longer have to find the ceiling by building into it.** `build`
|
|
360
|
+
and `explain` print it for any mesh with a depth map — per axis, per direction,
|
|
361
|
+
naming the triangle that goes first — from `tan t = A₀/A_axis` on the mesh's own
|
|
362
|
+
geometry ([AUTHORING §3.4](AUTHORING.md)):
|
|
363
|
+
|
|
364
|
+
```
|
|
365
|
+
turn ceiling yaw +31.41° / -32.01° pitch +32.01° / -31.41°
|
|
366
|
+
first to fold: yaw + at 31.41°, triangle 960 [113,112,593]
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
Read it as a fact about **the sheet**. If the number is too small, the fix is in
|
|
370
|
+
the map — flatten it where the part curves away — and not in the lattice.
|
|
371
|
+
|
|
372
|
+
📐 Method, harness and the full ladders live in the repository rather than in
|
|
373
|
+
this package, as
|
|
374
|
+
[`bench/studies/2026-09-05-density`](https://github.com/firejune/rigc/tree/main/bench/studies/2026-09-05-density) —
|
|
375
|
+
the same study also measures why `contour` is the wrong generator for a turn: it
|
|
376
|
+
saturates at 868 vertices however fine the tolerance, its vertices all sit on the
|
|
377
|
+
silhouette so it samples **2 %** of the depth range, and its ear-clipped interior
|
|
378
|
+
holds triangles three orders of magnitude apart in area, the smallest of which
|
|
379
|
+
reverse under a fraction of a pixel.
|
|
380
|
+
|
|
248
381
|
---
|
|
249
382
|
|
|
250
383
|
## 3. One shared shift, then residuals
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spine-rigc",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.0",
|
|
4
4
|
"description": "Rig compiler for Spine — declarative rig specs in, Spine 4.3 skeleton data out, verified by a spine-core round-trip. Built so AI agents can author rigs and check their own work; the output imports into the Spine editor.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|