@rig-c/pose 0.38.0 → 0.40.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/README.md +64 -9
- package/cli.ts +64 -10
- package/package.json +1 -1
- package/src/clean.ts +24 -2
- package/src/depth.ts +190 -0
- package/src/draworder.ts +290 -0
- package/src/legs.ts +189 -0
- package/src/outdir.ts +54 -0
- package/src/pelvis.ts +100 -15
- package/src/pose.ts +150 -18
- package/src/reduce.ts +21 -12
- package/src/retarget.ts +82 -3
- package/src/run.ts +162 -35
- package/src/runcompare.ts +6 -7
- package/src/switch.ts +64 -2
- package/src/verify.ts +90 -9
package/src/depth.ts
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Derived depth (rig-c/pose#281): which way each limb segment leans out of the
|
|
3
|
+
* picture, read off its 2D length, and the limbs' front-to-back order from it.
|
|
4
|
+
*
|
|
5
|
+
* A sequence carries x and y only, so nothing downstream can say which limb is
|
|
6
|
+
* nearer the camera. A segment drawn shorter than its rest length is tilted out
|
|
7
|
+
* of the picture plane, and by how much its length says: `ratio` = drawn length
|
|
8
|
+
* ÷ rest length, the rest length being the larger of the two sides' 95th
|
|
9
|
+
* percentile picture lengths of that segment over the clip — the straight length
|
|
10
|
+
* the leg scale reads (`STRAIGHT_PERCENTILE`, #148), per segment. Which way it
|
|
11
|
+
* tilts the length cannot say; the sign comes from the body:
|
|
12
|
+
*
|
|
13
|
+
* | segment | sense when foreshortened | why |
|
|
14
|
+
* | --------- | ------------------------ | --- |
|
|
15
|
+
* | thigh | forward | a hip flexes forward; it extends back too little to foreshorten past the band |
|
|
16
|
+
* | shin | walk: forward · run: back | the gait rule (rig-c/pose#281, HQ): a walk's lifted foot comes forward, a run's is kicked back — the knee alone cannot say, since it bends forward in both |
|
|
17
|
+
* | upper arm | with the opposite leg | in a gait each arm swings with the opposite leg: forward when the other side's thigh is the more foreshortened of the two, back when its own is; forward (a shoulder flexes forward) when neither thigh is foreshortened |
|
|
18
|
+
* | forearm | forward | an elbow bends toward the back of the body, so the forearm folds forward |
|
|
19
|
+
*
|
|
20
|
+
* "Forward" is the trunk's facing: toward the camera (+1) when the figure faces
|
|
21
|
+
* it (its left shoulder and hip drawn right of its right ones), away (−1) when
|
|
22
|
+
* it faces away, and undecided (sign 0) when the trunk is side-on — shoulders
|
|
23
|
+
* and hips drawn less than `SIDE_ON` trunk lengths wide — since forward is then
|
|
24
|
+
* across the picture, not into it. A ratio within `FLAT_BAND` is flat, sign 0,
|
|
25
|
+
* and so is one above it (drawn longer than rest is not a tilt).
|
|
26
|
+
*
|
|
27
|
+
* The layers: each joint's depth is its parent's plus its segment's,
|
|
28
|
+
* sign × rest × √(1 − ratio²) px (rest = drawn ÷ ratio), from the shoulders and
|
|
29
|
+
* hips at 0; a limb's depth is the mean of its middle and end joints'; the limbs
|
|
30
|
+
* are ordered nearest first, equal depths (to 0.01 px) sharing a layer. A limb
|
|
31
|
+
* whose segments are not both named on a frame is not ordered on it.
|
|
32
|
+
*
|
|
33
|
+
* Pure: no clock, no randomness, no disk.
|
|
34
|
+
*/
|
|
35
|
+
import type { Body18 } from './joints.ts';
|
|
36
|
+
import { percentile, STRAIGHT_PERCENTILE } from './legs.ts';
|
|
37
|
+
import { DEPTH_SEGMENT_NAMES, DEPTH_SEGMENTS, type DepthGait, type DepthSegment, type DepthSource, type FrameDepth, type Joint, type SegmentDepth } from './pose.ts';
|
|
38
|
+
|
|
39
|
+
/** A ratio within this band is flat (sign 0): cos 18.2° = 0.95, so a segment within about 18° of the picture plane reads flat. */
|
|
40
|
+
export const FLAT_BAND: readonly [number, number] = [0.95, 1.05];
|
|
41
|
+
/** The trunk is side-on when its shoulders and hips are drawn, on average, less than this many trunk lengths (neck → mid_hip) wide. */
|
|
42
|
+
export const SIDE_ON = 0.1;
|
|
43
|
+
/** The limbs `layers` orders, each by its segments from the trunk out. */
|
|
44
|
+
export const LIMBS = {
|
|
45
|
+
r_arm: ['r_upper_arm', 'r_forearm'],
|
|
46
|
+
l_arm: ['l_upper_arm', 'l_forearm'],
|
|
47
|
+
r_leg: ['r_thigh', 'r_shin'],
|
|
48
|
+
l_leg: ['l_thigh', 'l_shin'],
|
|
49
|
+
} as const satisfies Record<string, readonly [DepthSegment, DepthSegment]>;
|
|
50
|
+
export type Limb = keyof typeof LIMBS;
|
|
51
|
+
export const LIMB_NAMES = Object.keys(LIMBS) as Limb[];
|
|
52
|
+
|
|
53
|
+
type Pt = [number, number];
|
|
54
|
+
type Joints = Partial<Record<Body18, Joint>>;
|
|
55
|
+
const at = (j: Joint | undefined): Pt | null => (j === undefined || j.state === 'missing' || j.at === undefined ? null : j.at);
|
|
56
|
+
const kindOf = (s: DepthSegment) => s.slice(2) as 'upper_arm' | 'forearm' | 'thigh' | 'shin';
|
|
57
|
+
const other = (s: DepthSegment): DepthSegment => `${s.startsWith('r_') ? 'l' : 'r'}_${s.slice(2)}` as DepthSegment;
|
|
58
|
+
|
|
59
|
+
/** A segment's drawn length on a frame; null where a joint is not placed. */
|
|
60
|
+
function drawn(joints: Joints, s: DepthSegment): number | null {
|
|
61
|
+
const [a, b] = DEPTH_SEGMENTS[s];
|
|
62
|
+
const pa = at(joints[a]);
|
|
63
|
+
const pb = at(joints[b]);
|
|
64
|
+
return pa === null || pb === null ? null : Math.hypot(pb[0] - pa[0], pb[1] - pa[1]);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** The trunk's facing on a frame: +1 toward the camera, −1 away, 0 side-on or not placed. */
|
|
68
|
+
export function facingOf(joints: Joints): -1 | 0 | 1 {
|
|
69
|
+
const p = (n: Body18) => at(joints[n]);
|
|
70
|
+
const [rs, ls, rh, lh, neck] = [p('r_shoulder'), p('l_shoulder'), p('r_hip'), p('l_hip'), p('neck')];
|
|
71
|
+
if (rs === null || ls === null || rh === null || lh === null || neck === null) return 0;
|
|
72
|
+
const trunk = Math.hypot(neck[0] - (rh[0] + lh[0]) / 2, neck[1] - (rh[1] + lh[1]) / 2);
|
|
73
|
+
if (!(trunk > 0)) return 0;
|
|
74
|
+
const width = (ls[0] - rs[0] + (lh[0] - rh[0])) / 2 / trunk;
|
|
75
|
+
return width > SIDE_ON ? 1 : width < -SIDE_ON ? -1 : 0;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface DerivedDepth {
|
|
79
|
+
/** Per frame, each segment whose joints are placed. */
|
|
80
|
+
frames: FrameDepth[];
|
|
81
|
+
/** Each segment kind's rest length, px (the larger side's 95th percentile); null where neither side is ever placed. */
|
|
82
|
+
rest: Record<'upper_arm' | 'forearm' | 'thigh' | 'shin', number | null>;
|
|
83
|
+
/** Frames the trunk faced the camera, faced away, and was side-on (or not placed). */
|
|
84
|
+
facing: { toward: number; away: number; side: number };
|
|
85
|
+
/** Segment-frames by sign. */
|
|
86
|
+
signs: { toward: number; away: number; flat: number };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Each frame's depth, derived from the picture's lengths for `gait` (the table above). */
|
|
90
|
+
export function deriveDepth(track: readonly Joints[], gait: DepthGait): DerivedDepth {
|
|
91
|
+
const lengths = new Map(DEPTH_SEGMENT_NAMES.map((s) => [s, track.map((j) => drawn(j, s))]));
|
|
92
|
+
const rest = {} as DerivedDepth['rest'];
|
|
93
|
+
for (const k of ['upper_arm', 'forearm', 'thigh', 'shin'] as const) {
|
|
94
|
+
const sides = (['r', 'l'] as const).map((side) => percentile(lengths.get(`${side}_${k}`)!.filter((v): v is number => v !== null), STRAIGHT_PERCENTILE)).filter((v) => Number.isFinite(v) && v > 0);
|
|
95
|
+
rest[k] = sides.length === 0 ? null : Math.max(...sides);
|
|
96
|
+
}
|
|
97
|
+
const facing = { toward: 0, away: 0, side: 0 };
|
|
98
|
+
const signs = { toward: 0, away: 0, flat: 0 };
|
|
99
|
+
const frames = track.map((joints, i) => {
|
|
100
|
+
const face = facingOf(joints);
|
|
101
|
+
if (face === 1) facing.toward++;
|
|
102
|
+
else if (face === -1) facing.away++;
|
|
103
|
+
else facing.side++;
|
|
104
|
+
const ratio = (s: DepthSegment): number | null => {
|
|
105
|
+
const d = lengths.get(s)![i];
|
|
106
|
+
const r = rest[kindOf(s)];
|
|
107
|
+
return d === null || d === undefined || r === null ? null : d / r;
|
|
108
|
+
};
|
|
109
|
+
const out: FrameDepth = {};
|
|
110
|
+
for (const s of DEPTH_SEGMENT_NAMES) {
|
|
111
|
+
const r = ratio(s);
|
|
112
|
+
if (r === null) continue;
|
|
113
|
+
let sense: -1 | 1;
|
|
114
|
+
const kind = kindOf(s);
|
|
115
|
+
if (kind === 'shin') sense = gait === 'walk' ? 1 : -1;
|
|
116
|
+
else if (kind === 'upper_arm') {
|
|
117
|
+
// The arm swings with the opposite leg: its own thigh and the other side's, by how foreshortened each is.
|
|
118
|
+
const own = ratio(`${s.slice(0, 2)}thigh` as DepthSegment);
|
|
119
|
+
const opp = ratio(other(`${s.slice(0, 2)}thigh` as DepthSegment));
|
|
120
|
+
sense = own === null || opp === null || Math.min(own, opp) >= FLAT_BAND[0] || own === opp ? 1 : opp < own ? 1 : -1;
|
|
121
|
+
} else sense = 1;
|
|
122
|
+
const sign = (r < FLAT_BAND[0] ? face * sense : 0) as -1 | 0 | 1;
|
|
123
|
+
if (sign === 1) signs.toward++;
|
|
124
|
+
else if (sign === -1) signs.away++;
|
|
125
|
+
else signs.flat++;
|
|
126
|
+
out[s] = { ratio: r, sign: sign === 0 ? 0 : sign, source: 'derived' };
|
|
127
|
+
}
|
|
128
|
+
return out;
|
|
129
|
+
});
|
|
130
|
+
return { frames, rest, facing, signs };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** One frame's layers: the limbs nearest first, equal depths sharing a layer, and each limb's depth (px, + toward the camera). */
|
|
134
|
+
export interface FrameLayers {
|
|
135
|
+
order: Limb[][];
|
|
136
|
+
depth: Partial<Record<Limb, number>>;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** The depth one segment adds to its distal joint over its proximal one, px: sign × rest × √(1 − ratio²). */
|
|
140
|
+
export function segmentDepth(d: SegmentDepth, length: number): number {
|
|
141
|
+
if (d.sign === 0 || !(d.ratio > 0)) return 0;
|
|
142
|
+
const r = Math.min(d.ratio, 1);
|
|
143
|
+
return d.sign * (length / d.ratio) * Math.sqrt(1 - r * r);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** The limbs' front-to-back order on one frame, from its depth and its joints' drawn lengths. */
|
|
147
|
+
export function layersOf(joints: Joints, depth: FrameDepth): FrameLayers {
|
|
148
|
+
const z: Partial<Record<Limb, number>> = {};
|
|
149
|
+
for (const limb of LIMB_NAMES) {
|
|
150
|
+
const [upper, lower] = LIMBS[limb];
|
|
151
|
+
const du = depth[upper];
|
|
152
|
+
const dl = depth[lower];
|
|
153
|
+
const lu = drawn(joints, upper);
|
|
154
|
+
const ll = drawn(joints, lower);
|
|
155
|
+
if (du === undefined || dl === undefined || lu === null || ll === null) continue;
|
|
156
|
+
const mid = segmentDepth(du, lu);
|
|
157
|
+
const end = mid + segmentDepth(dl, ll);
|
|
158
|
+
z[limb] = Math.round(((mid + end) / 2) * 100) / 100;
|
|
159
|
+
}
|
|
160
|
+
const placed = LIMB_NAMES.filter((l) => z[l] !== undefined).sort((a, b) => z[b]! - z[a]! || LIMB_NAMES.indexOf(a) - LIMB_NAMES.indexOf(b));
|
|
161
|
+
const order: Limb[][] = [];
|
|
162
|
+
for (const l of placed) {
|
|
163
|
+
const last = order[order.length - 1];
|
|
164
|
+
if (last !== undefined && z[last[0]!] === z[l]) last.push(l);
|
|
165
|
+
else order.push([l]);
|
|
166
|
+
}
|
|
167
|
+
return { order, depth: z };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** A frame's layers as text: "r_arm > l_arm = r_leg > l_leg". */
|
|
171
|
+
export const orderText = (order: Limb[][]): string => order.map((g) => g.join(' = ')).join(' > ');
|
|
172
|
+
|
|
173
|
+
/** The source a sequence's depth came from, or null when its frames carry none. */
|
|
174
|
+
export function depthSourceOf(frames: ReadonlyArray<{ depth?: FrameDepth }>): DepthSource | null {
|
|
175
|
+
for (const f of frames) for (const d of Object.values(f.depth ?? {})) return d.source;
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* retarget's DEPTH line (rig-c/pose#281): where the depth came from, on how many
|
|
181
|
+
* frames the limbs were ordered, and which limb was in front alone how often.
|
|
182
|
+
*/
|
|
183
|
+
export function depthLine(layers: readonly FrameLayers[], frames: ReadonlyArray<{ depth?: FrameDepth }>, gait: DepthGait | null): string {
|
|
184
|
+
const source = depthSourceOf(frames);
|
|
185
|
+
const from = source === null ? 'the sequence\'s depth (no segment named)' : source === 'derived' ? `the sequence's derived depth (clean --depth ${gait ?? '?'})` : 'the sequence\'s measured depth';
|
|
186
|
+
const ordered = layers.filter((l) => l.order.length > 0).length;
|
|
187
|
+
const front = LIMB_NAMES.map((limb) => `${limb} ${layers.filter((l) => l.order[0]?.length === 1 && l.order[0][0] === limb).length}`).join(', ');
|
|
188
|
+
const tied = layers.filter((l) => (l.order[0]?.length ?? 0) > 1).length;
|
|
189
|
+
return `DEPTH layers from ${from}: the limbs ordered front to back on ${ordered} of ${layers.length} frame(s); in front alone: ${front}; tied in front on ${tied} (the report's layers)`;
|
|
190
|
+
}
|
package/src/draworder.ts
ADDED
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Draw order from depth (rig-c/pose#304): `retarget --draw-order depth` keys
|
|
3
|
+
* rig-c's `drawOrder` timeline from the limbs' derived depth (`layers`, #281),
|
|
4
|
+
* so the limb that reads farther from the camera is drawn behind the nearer one
|
|
5
|
+
* on that frame.
|
|
6
|
+
*
|
|
7
|
+
* - **The format.** rig-c's motion spec carries one `drawOrder` timeline per
|
|
8
|
+
* animation, beside `tracks`: keys of `{ t, offsets? }`, each offset
|
|
9
|
+
* `{ slot, offset }` = how many places later than its SETUP index the slot is
|
|
10
|
+
* drawn, every key a complete statement against the setup order, and a key
|
|
11
|
+
* with no `offsets` the setup order again. Keys are stepped.
|
|
12
|
+
* - **The pairs.** The legs (`r_leg`, `l_leg`); the arms crossing the trunk are
|
|
13
|
+
* a later card. A limb is the map's chains naming its middle or end joint
|
|
14
|
+
* (knee, ankle), their bones and every bone under them no other chain maps;
|
|
15
|
+
* its slots are the slots on those bones, and an alternate set's slot on none
|
|
16
|
+
* of them goes with the limb whose chain the set depicts, when it depicts one
|
|
17
|
+
* of the two (a set keeps its own switch and takes the order of the limb it
|
|
18
|
+
* replaces). A pair is keyed when its slots overlap in the rig's draw order:
|
|
19
|
+
* together they fill one unbroken run of the setup order. A slot of anything
|
|
20
|
+
* else drawn between them would be re-layered by the swap — that is changing
|
|
21
|
+
* the setup order, not keying the pair — so the pair is not keyed and the line
|
|
22
|
+
* names the slot.
|
|
23
|
+
* - **The rule.** On each frame the limb whose depth is the lower (farther from
|
|
24
|
+
* the camera) is drawn behind: its slots first in the pair's run, in their
|
|
25
|
+
* setup order, then the other's — when the two depths differ by at least
|
|
26
|
+
* #281's tie: the depth a limb can hold while both its segments read flat,
|
|
27
|
+
* √(1 − 0.95²) × (upper + lower ÷ 2) of its rest lengths, the smaller limb's of
|
|
28
|
+
* the two (a flat segment carries no depth, so closer than that the band
|
|
29
|
+
* cannot tell them). A frame inside the tie, or where either limb is not
|
|
30
|
+
* ordered, holds the order before; before the first decided frame, the setup
|
|
31
|
+
* order — or, on a looped sequence, the order the clip ends in, held across
|
|
32
|
+
* the seam. A key is written where the order changes, and none while it is the
|
|
33
|
+
* setup order from the start.
|
|
34
|
+
*
|
|
35
|
+
* Pure: no clock, no randomness, no disk.
|
|
36
|
+
*/
|
|
37
|
+
import type { Body18 } from './joints.ts';
|
|
38
|
+
import { FLAT_BAND, LIMBS, type FrameLayers, type Limb } from './depth.ts';
|
|
39
|
+
import { knownSetsOf, type JointMap, type Rest, type RigSets } from './map.ts';
|
|
40
|
+
import { DEPTH_SEGMENTS, type DepthSegment, type FrameDepth, type Joint } from './pose.ts';
|
|
41
|
+
|
|
42
|
+
/** The limb pairs `--draw-order depth` keys. */
|
|
43
|
+
export const DRAW_ORDER_PAIRS: ReadonlyArray<readonly [Limb, Limb]> = [['r_leg', 'l_leg']];
|
|
44
|
+
/** What one segment inside the flat band can hide out of the picture, per unit of its rest length: √(1 − 0.95²). */
|
|
45
|
+
export const TIE_PER_LENGTH = Math.sqrt(1 - FLAT_BAND[0] * FLAT_BAND[0]);
|
|
46
|
+
/** The DRAWORDER line's cap, characters (the span list is cut to it; the report carries every span). */
|
|
47
|
+
export const DRAW_ORDER_LINE_MAX = 260;
|
|
48
|
+
|
|
49
|
+
export interface DrawOrderOffset {
|
|
50
|
+
slot: string;
|
|
51
|
+
offset: number;
|
|
52
|
+
}
|
|
53
|
+
/** One key as written into the animation (`t` the frame's time). */
|
|
54
|
+
export interface DrawOrderKey {
|
|
55
|
+
t: number;
|
|
56
|
+
offsets?: DrawOrderOffset[];
|
|
57
|
+
}
|
|
58
|
+
export interface DrawOrderPair {
|
|
59
|
+
/** The map chain names standing for the two limbs (the first chain of each). */
|
|
60
|
+
a: string;
|
|
61
|
+
b: string;
|
|
62
|
+
limbs: [Limb, Limb];
|
|
63
|
+
/** Each limb's slots, in setup order. */
|
|
64
|
+
slots: { a: string[]; b: string[] };
|
|
65
|
+
keyed: boolean;
|
|
66
|
+
/** #281's tie for the pair, px: depths closer than this hold the order before (null where a rest length is unread: no tie, equal depths only). */
|
|
67
|
+
tie: number | null;
|
|
68
|
+
/** Why the pair is not keyed. */
|
|
69
|
+
why?: string;
|
|
70
|
+
}
|
|
71
|
+
export interface DrawOrderSpan {
|
|
72
|
+
/** The chain drawn behind, and the one in front. */
|
|
73
|
+
behind: string;
|
|
74
|
+
front: string;
|
|
75
|
+
frames: [number, number];
|
|
76
|
+
/** False where the setup order already draws it so (no key needed). */
|
|
77
|
+
keyed: boolean;
|
|
78
|
+
}
|
|
79
|
+
export interface DrawOrderReading {
|
|
80
|
+
pairs: DrawOrderPair[];
|
|
81
|
+
/** The `drawOrder` timeline, with the frame each key sits on. */
|
|
82
|
+
keys: Array<DrawOrderKey & { frame: number }>;
|
|
83
|
+
/** Every span of one decided order, setup or keyed, in frame order per pair. */
|
|
84
|
+
spans: DrawOrderSpan[];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
interface Resolved {
|
|
88
|
+
pair: DrawOrderPair;
|
|
89
|
+
/** Setup index of every slot of the pair's run. */
|
|
90
|
+
lo: number;
|
|
91
|
+
hi: number;
|
|
92
|
+
/** Offsets with `a` behind, and with `b` behind; empty = the setup order. */
|
|
93
|
+
aBehind: DrawOrderOffset[];
|
|
94
|
+
bBehind: DrawOrderOffset[];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** The joints of a limb that name its chains: the middle and the end. */
|
|
98
|
+
function limbJoints(limb: Limb): string[] {
|
|
99
|
+
const [upper, lower] = LIMBS[limb];
|
|
100
|
+
return [DEPTH_SEGMENTS[upper][1], DEPTH_SEGMENTS[lower][1]];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** The pair's run with `behind`'s slots first: each moved slot's offset from its setup index. */
|
|
104
|
+
function arrangement(order: readonly string[], lo: number, behind: readonly string[], front: readonly string[]): DrawOrderOffset[] {
|
|
105
|
+
const placed = [...behind, ...front];
|
|
106
|
+
const out: DrawOrderOffset[] = [];
|
|
107
|
+
placed.forEach((slot, k) => {
|
|
108
|
+
const offset = lo + k - order.indexOf(slot);
|
|
109
|
+
if (offset !== 0) out.push({ slot, offset });
|
|
110
|
+
});
|
|
111
|
+
return out.sort((x, y) => order.indexOf(x.slot) - order.indexOf(y.slot));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function resolvePair(pairLimbs: readonly [Limb, Limb], map: JointMap, rest: Rest, order: ReadonlyArray<{ name: string; bone: string }>, sets: RigSets | undefined): Resolved {
|
|
115
|
+
const names = order.map((s) => s.name);
|
|
116
|
+
const chainsOf = (limb: Limb) => map.chains.filter((c) => limbJoints(limb).some((j) => (c.joints as readonly string[]).includes(j)));
|
|
117
|
+
const mapped = new Map(map.chains.flatMap((c) => c.bones.map((b) => [b, c.name] as const)));
|
|
118
|
+
const [ca, cb] = [chainsOf(pairLimbs[0]), chainsOf(pairLimbs[1])];
|
|
119
|
+
const pair: DrawOrderPair = { a: ca[0]?.name ?? pairLimbs[0], b: cb[0]?.name ?? pairLimbs[1], limbs: [pairLimbs[0], pairLimbs[1]], slots: { a: [], b: [] }, keyed: false, tie: null };
|
|
120
|
+
const none = (why: string): Resolved => ({ pair: { ...pair, why }, lo: 0, hi: -1, aBehind: [], bBehind: [] });
|
|
121
|
+
for (const [limb, cs] of [[pairLimbs[0], ca], [pairLimbs[1], cb]] as const) {
|
|
122
|
+
if (cs.length === 0) return none(`no chain of the map names ${limbJoints(limb).join(' or ')} (the ${limb.replace('_', ' ')})`);
|
|
123
|
+
}
|
|
124
|
+
// The limb's bones: its chains' and every bone under them that no other chain maps.
|
|
125
|
+
const bonesOf = (cs: typeof ca): Set<string> => {
|
|
126
|
+
const own = new Set(cs.flatMap((c) => c.bones));
|
|
127
|
+
const chainNames = new Set(cs.map((c) => c.name));
|
|
128
|
+
const out = new Set(own);
|
|
129
|
+
for (const [b] of rest) {
|
|
130
|
+
if (out.has(b)) continue;
|
|
131
|
+
const m = mapped.get(b);
|
|
132
|
+
if (m !== undefined && !chainNames.has(m)) continue;
|
|
133
|
+
for (let p = rest.get(b)?.parent ?? null; p !== null; p = rest.get(p)?.parent ?? null) {
|
|
134
|
+
if (own.has(p)) {
|
|
135
|
+
out.add(b);
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
138
|
+
const pm = mapped.get(p);
|
|
139
|
+
if (pm !== undefined && !chainNames.has(pm)) break;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return out;
|
|
143
|
+
};
|
|
144
|
+
const [ba, bb] = [bonesOf(ca), bonesOf(cb)];
|
|
145
|
+
const slotsA = new Set(order.filter((s) => ba.has(s.bone)).map((s) => s.name));
|
|
146
|
+
const slotsB = new Set(order.filter((s) => bb.has(s.bone)).map((s) => s.name));
|
|
147
|
+
// An alternate set's slot on neither limb's bones goes with the limb whose chain the set depicts (one of the two).
|
|
148
|
+
const known = knownSetsOf(map, sets);
|
|
149
|
+
const [na, nb] = [new Set(ca.map((c) => c.name)), new Set(cb.map((c) => c.name))];
|
|
150
|
+
for (const alt of map.alternates) {
|
|
151
|
+
const depictsA = alt.chains.some((c) => na.has(c));
|
|
152
|
+
const depictsB = alt.chains.some((c) => nb.has(c));
|
|
153
|
+
if (depictsA === depictsB) continue;
|
|
154
|
+
for (const slot of Object.keys(known.get(alt.name)?.slots ?? alt.slots)) {
|
|
155
|
+
if (!names.includes(slot) || slotsA.has(slot) || slotsB.has(slot)) continue;
|
|
156
|
+
(depictsA ? slotsA : slotsB).add(slot);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
const a = names.filter((n) => slotsA.has(n));
|
|
160
|
+
const b = names.filter((n) => slotsB.has(n));
|
|
161
|
+
pair.slots = { a, b };
|
|
162
|
+
if (a.length === 0 || b.length === 0) return none(`${a.length === 0 ? pair.a : pair.b} has no slot (no slot on its bones or on a set depicting it)`);
|
|
163
|
+
const idx = [...a, ...b].map((n) => names.indexOf(n)).sort((x, y) => x - y);
|
|
164
|
+
const lo = idx[0]!;
|
|
165
|
+
const hi = idx[idx.length - 1]!;
|
|
166
|
+
const between = names.slice(lo, hi + 1).filter((n) => !slotsA.has(n) && !slotsB.has(n));
|
|
167
|
+
if (between.length > 0) return none(`their slots do not overlap in the draw order: ${between.map((n) => `"${n}"`).join(', ')} ${between.length === 1 ? 'is' : 'are'} drawn between them, and a swap would re-layer ${between.length === 1 ? 'it' : 'them'} (the setup order is not changed here)`);
|
|
168
|
+
return { pair: { ...pair, keyed: true }, lo, hi, aBehind: arrangement(names, lo, a, b), bBehind: arrangement(names, lo, b, a) };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
type Joints = Partial<Record<Body18, Joint>>;
|
|
172
|
+
|
|
173
|
+
/** A segment's rest length as the depth states it (drawn ÷ ratio), the median over the frames that name it; null where none does. */
|
|
174
|
+
function restOf(track: readonly Joints[], depth: ReadonlyArray<FrameDepth | undefined>, s: DepthSegment): number | null {
|
|
175
|
+
const [p, q] = DEPTH_SEGMENTS[s];
|
|
176
|
+
const vals: number[] = [];
|
|
177
|
+
track.forEach((j, i) => {
|
|
178
|
+
const d = depth[i]?.[s];
|
|
179
|
+
const a = j[p];
|
|
180
|
+
const b = j[q];
|
|
181
|
+
if (d === undefined || !(d.ratio > 0) || a?.at === undefined || b?.at === undefined || a.state === 'missing' || b.state === 'missing') return;
|
|
182
|
+
vals.push(Math.hypot(b.at[0] - a.at[0], b.at[1] - a.at[1]) / d.ratio);
|
|
183
|
+
});
|
|
184
|
+
if (vals.length === 0) return null;
|
|
185
|
+
vals.sort((x, y) => x - y);
|
|
186
|
+
const m = vals.length >> 1;
|
|
187
|
+
return vals.length % 2 === 1 ? vals[m]! : (vals[m - 1]! + vals[m]!) / 2;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** #281's tie for two limbs, px: √(1 − 0.95²) × (upper + lower ÷ 2), the smaller limb's; null where a rest length is unread. */
|
|
191
|
+
function tieOf(track: readonly Joints[], depth: ReadonlyArray<FrameDepth | undefined>, limbs: readonly [Limb, Limb]): number | null {
|
|
192
|
+
const hidden = limbs.map((l) => {
|
|
193
|
+
const [u, w] = LIMBS[l].map((s) => restOf(track, depth, s));
|
|
194
|
+
return u === null || u === undefined || w === null || w === undefined ? null : u + w / 2;
|
|
195
|
+
});
|
|
196
|
+
return hidden.some((h) => h === null) ? null : Math.round(TIE_PER_LENGTH * Math.min(hidden[0]!, hidden[1]!) * 100) / 100;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* The draw-order keys from the frames' layers. `track` and `depth` are the sequence's
|
|
201
|
+
* joints and depth per frame (the rest lengths the tie reads); `order` is the rig's
|
|
202
|
+
* setup draw order (the compiled model's slots); `times` the frames' times.
|
|
203
|
+
*/
|
|
204
|
+
export function drawOrderFromDepth(layers: readonly FrameLayers[], track: readonly Joints[], depth: ReadonlyArray<FrameDepth | undefined>, times: readonly number[], map: JointMap, rest: Rest, order: ReadonlyArray<{ name: string; bone: string }>, sets?: RigSets, loop = false): DrawOrderReading {
|
|
205
|
+
const resolved = DRAW_ORDER_PAIRS.map((p) => resolvePair(p, map, rest, order, sets));
|
|
206
|
+
for (const r of resolved) r.pair.tie = tieOf(track, depth, r.pair.limbs);
|
|
207
|
+
const names = order.map((s) => s.name);
|
|
208
|
+
// Per pair, per frame: who is behind ('a' | 'b'), or null for the setup order before any decision.
|
|
209
|
+
const states = resolved.map((r) => {
|
|
210
|
+
const tie = r.pair.tie ?? 0;
|
|
211
|
+
const pass = (start: 'a' | 'b' | null): Array<'a' | 'b' | null> => {
|
|
212
|
+
let state = start;
|
|
213
|
+
return layers.map((l) => {
|
|
214
|
+
if (!r.pair.keyed) return null;
|
|
215
|
+
const za = l.depth[r.pair.limbs[0]];
|
|
216
|
+
const zb = l.depth[r.pair.limbs[1]];
|
|
217
|
+
if (za !== undefined && zb !== undefined && za !== zb && Math.abs(za - zb) >= tie) state = za < zb ? 'a' : 'b';
|
|
218
|
+
return state;
|
|
219
|
+
});
|
|
220
|
+
};
|
|
221
|
+
const first = pass(null);
|
|
222
|
+
// A loop's first frames continue its last: a tie at the start holds the order the clip ends in.
|
|
223
|
+
return loop && first.length > 0 ? pass(first[first.length - 1]!) : first;
|
|
224
|
+
});
|
|
225
|
+
const offsetsAt = (f: number): DrawOrderOffset[] =>
|
|
226
|
+
resolved
|
|
227
|
+
.flatMap((r, k) => {
|
|
228
|
+
const s = states[k]![f];
|
|
229
|
+
return s === null || s === undefined ? [] : s === 'a' ? r.aBehind : r.bBehind;
|
|
230
|
+
})
|
|
231
|
+
.sort((x, y) => names.indexOf(x.slot) - names.indexOf(y.slot));
|
|
232
|
+
const keys: DrawOrderReading['keys'] = [];
|
|
233
|
+
let last = '[]';
|
|
234
|
+
for (let f = 0; f < layers.length; f++) {
|
|
235
|
+
const offs = offsetsAt(f);
|
|
236
|
+
const text = JSON.stringify(offs);
|
|
237
|
+
if (text === last) continue;
|
|
238
|
+
last = text;
|
|
239
|
+
keys.push(offs.length === 0 ? { frame: f, t: times[f]! } : { frame: f, t: times[f]!, offsets: offs });
|
|
240
|
+
}
|
|
241
|
+
const spans: DrawOrderSpan[] = [];
|
|
242
|
+
resolved.forEach((r, k) => {
|
|
243
|
+
const st = states[k]!;
|
|
244
|
+
st.forEach((s, f) => {
|
|
245
|
+
if (s === null) return;
|
|
246
|
+
if (f > 0 && st[f - 1] === s) spans[spans.length - 1]!.frames[1] = f;
|
|
247
|
+
else spans.push({ behind: s === 'a' ? r.pair.a : r.pair.b, front: s === 'a' ? r.pair.b : r.pair.a, frames: [f, f], keyed: (s === 'a' ? r.aBehind : r.bBehind).length > 0 });
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
return { pairs: resolved.map((r) => r.pair), keys, spans };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** The animation's `drawOrder` value: the keys without their frames. */
|
|
254
|
+
export const drawOrderKeys = (d: DrawOrderReading): DrawOrderKey[] => d.keys.map(({ frame: _frame, ...k }) => k);
|
|
255
|
+
|
|
256
|
+
/** Frame runs as text: "3–12". */
|
|
257
|
+
const span = (f: [number, number]): string => (f[0] === f[1] ? `${f[0]}` : `${f[0]}–${f[1]}`);
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* The DRAWORDER lines (rig-c/pose#304): the keys and the spans they key, per
|
|
261
|
+
* "behind" relation, cut to `DRAW_ORDER_LINE_MAX` characters; then one line per
|
|
262
|
+
* pair not keyed, saying why.
|
|
263
|
+
*/
|
|
264
|
+
export function drawOrderLines(d: DrawOrderReading, prefix = 'DRAWORDER '): string[] {
|
|
265
|
+
const keyed = d.spans.filter((s) => s.keyed);
|
|
266
|
+
const head = `${prefix}from depth, ${d.keys.length} key(s) over ${keyed.length} span(s)`;
|
|
267
|
+
const groups = new Map<string, string[]>();
|
|
268
|
+
for (const s of keyed) {
|
|
269
|
+
const g = `${s.behind} behind ${s.front}`;
|
|
270
|
+
groups.set(g, [...(groups.get(g) ?? []), span(s.frames)]);
|
|
271
|
+
}
|
|
272
|
+
// The spans as text, grouped by relation, cut where the next one would pass the cap with its "more" tail.
|
|
273
|
+
const text = (n: number): string => {
|
|
274
|
+
const parts: string[] = [];
|
|
275
|
+
let left = n;
|
|
276
|
+
for (const [g, list] of groups) {
|
|
277
|
+
if (left === 0) break;
|
|
278
|
+
parts.push(`${g} ${list.slice(0, left).join(', ')}`);
|
|
279
|
+
left -= Math.min(left, list.length);
|
|
280
|
+
}
|
|
281
|
+
const more = keyed.length - n;
|
|
282
|
+
return `${head} (${parts.join('; ')}${more > 0 ? ` … +${more} more in the report's drawOrder` : ''})`;
|
|
283
|
+
};
|
|
284
|
+
let n = keyed.length;
|
|
285
|
+
while (n > 1 && text(n).length > DRAW_ORDER_LINE_MAX) n--;
|
|
286
|
+
const why = d.pairs.every((p) => !p.keyed) ? 'no pair keyed' : 'the setup order holds on every frame';
|
|
287
|
+
const lines = [keyed.length === 0 ? `${head} (${why})` : text(n)];
|
|
288
|
+
for (const p of d.pairs) if (!p.keyed) lines.push(`${prefix}${p.a} / ${p.b} not keyed: ${p.why}`.slice(0, DRAW_ORDER_LINE_MAX));
|
|
289
|
+
return lines;
|
|
290
|
+
}
|