@rig-c/parts 1.26.0 → 1.28.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/docs/AUTHORING.md +9 -4
- package/docs/STABILITY.md +3 -2
- package/package.json +1 -1
- package/src/assemble.ts +14 -2
- package/src/check.ts +23 -5
- package/src/cloth.ts +29 -2
- package/src/config.ts +54 -4
- package/src/coords.ts +1 -1
- package/src/motion.ts +15 -5
- package/src/propose.ts +430 -27
- package/src/rig.ts +13 -2
- package/src/turn.ts +665 -1
package/src/turn.ts
CHANGED
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
* that the depth maps do not carry relief inside a layer, so a part moves as
|
|
29
29
|
* one piece. Pure: no file, no clock.
|
|
30
30
|
*/
|
|
31
|
+
import { computeWorldTransforms, toBoneLocalVector, toWorld, type BoneTransform } from './coords.ts';
|
|
31
32
|
import { pyRound } from './round.ts';
|
|
32
33
|
|
|
33
34
|
/**
|
|
@@ -67,10 +68,108 @@ export const TURN_BONE = 'turn';
|
|
|
67
68
|
/** The places a pivot and a fraction are written to. */
|
|
68
69
|
export const TURN_PLACES = 6;
|
|
69
70
|
|
|
70
|
-
/**
|
|
71
|
+
/**
|
|
72
|
+
* The Set B ×2.5 face surface deformation table (issue #436): per-degree
|
|
73
|
+
* fractions of the face plate's opaque width `W` and height `H` (and the
|
|
74
|
+
* dimensionless coupling shares of the hair, ear and topknot parts that sample
|
|
75
|
+
* the same head field), with the sheet's measurements on a `106×128` plate at
|
|
76
|
+
* `+15°` beside each entry.
|
|
77
|
+
*/
|
|
78
|
+
export const FACE_DEFORM_FRACTIONS = {
|
|
79
|
+
/** Mid-face vertical shift per degree of pitch (`H/deg`; `-9.02 px` at `+15°` on `H = 128`). */
|
|
80
|
+
tilt_mid_y: -0.0047,
|
|
81
|
+
/** Lower-face chin vertical squash per degree of pitch (`H/deg`; `+5.04 px` squash, chin tip `-3.98 px` at `+15°` on `H = 128`). */
|
|
82
|
+
tilt_chin_squash_y: 0.002625,
|
|
83
|
+
/** Forehead/crown extra drop per degree of pitch (`H/deg`; crown `-11.28 px` at `+15°` on `H = 128`). */
|
|
84
|
+
tilt_crown_drop_y: -0.001175,
|
|
85
|
+
/** Forehead/jaw perspective horizontal taper per degree of pitch (`W/deg`; `±4.77 px` at `+15°` on `W = 106`). */
|
|
86
|
+
tilt_taper_x: 0.003,
|
|
87
|
+
/** Mid-face and chin horizontal shift per degree of yaw (`W/deg`; `-9.54 px` at `+15°` on `W = 106`). */
|
|
88
|
+
turn_mid_x: -0.006,
|
|
89
|
+
/** Far (leading) cheek inward compression per degree of yaw (`W/deg`; far cheek `+3.75 px` inward at `+15°` on `W = 106`). */
|
|
90
|
+
turn_lead_compress_x: 0.00835,
|
|
91
|
+
/** Near (trailing) cheek shift beyond mid-face per degree of yaw (`W/deg`; near cheek `-10.50 px` at `+15°` on `W = 106`, total narrowing `-14.25 px` = `-13.4% W`). */
|
|
92
|
+
turn_trail_shift_x: -0.0006,
|
|
93
|
+
/** Cheek-arch vertical lift/drop per degree of yaw (`H/deg`; `±1.20 px` at `+15°` on `H = 128`). */
|
|
94
|
+
turn_cheek_arch_y: 0.000625,
|
|
95
|
+
/** Forehead bang base vertical travel per degree of pitch (`H/deg`; `-3.00 px` at `+15°` on `H = 128`). */
|
|
96
|
+
tilt_bang_base_y: -0.0015625,
|
|
97
|
+
/** Share of the pitch vertical surface field carried by `hair_front` (`0.95×` of `dySurf`). */
|
|
98
|
+
tilt_hair_front_y: 0.95,
|
|
99
|
+
/** Share of the pitch vertical surface field carried by lower `hair_back` (`0.85×` of `dySurf`). */
|
|
100
|
+
tilt_hair_back_lower_y: 0.85,
|
|
101
|
+
/** Share of the pitch vertical surface field carried by upper `hair_back` (`0.45×` of `dySurf`). */
|
|
102
|
+
tilt_hair_back_bun_y: 0.45,
|
|
103
|
+
/** Share of the pitch horizontal taper carried at the top of `hair_back` (`0.60×` of `dxSurf`). */
|
|
104
|
+
tilt_hair_back_bun_x: 0.6,
|
|
105
|
+
/** Share of the pitch vertical surface field carried by `ears` (`0.55×` of `dySurf`, `-4.31 px` at `+15°`). */
|
|
106
|
+
tilt_ear_y: 0.55,
|
|
107
|
+
/** Share of the crown pitch vertical field carried by `topknot` (`0.50×` of crown `dySurf`, `-5.64 px` at `+15°`). */
|
|
108
|
+
tilt_topknot_y: 0.5,
|
|
109
|
+
/** Forehead bang forward parallax per degree of yaw (`W/deg`; `-3.50 px` at `+15°` on `W = 106`). */
|
|
110
|
+
turn_bang_parallax_x: -(3.5 / 15) / 106,
|
|
111
|
+
/** Share of the cheek yaw horizontal field carried by lower `hair_back` (`0.90×` of `dxCheek`). */
|
|
112
|
+
turn_hair_back_cheek_x: 0.9,
|
|
113
|
+
/** Rear bun counter-parallax per degree of yaw (`W/deg`; `+8.00 px` at `+15°` on `W = 106`). */
|
|
114
|
+
turn_bun_counter_x: 8.0 / 15 / 106,
|
|
115
|
+
/** Rear hairpin counter-parallax per degree of yaw (`W/deg`; `+6.00 px` at `+15°` on `W = 106`). */
|
|
116
|
+
turn_hairpin_counter_x: 6.0 / 15 / 106,
|
|
117
|
+
/** Share of the cheek-edge yaw horizontal field carried by `ear_r` and `ear_l` (`0.85×` of `dxCheekEdge`, `-8.92 px` / `+3.14 px` at `+15°`). */
|
|
118
|
+
turn_ear_x: 0.85,
|
|
119
|
+
} as const;
|
|
120
|
+
|
|
121
|
+
export type FaceDeformKey = keyof typeof FACE_DEFORM_FRACTIONS;
|
|
122
|
+
export type FaceDeformFractions = Record<FaceDeformKey, number>;
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Allowed `[min, max]` range for each key of {@link FACE_DEFORM_FRACTIONS}; an
|
|
126
|
+
* override in `config.turn.deform` outside its stated interval is refused by
|
|
127
|
+
* name (`CONFIG_TURN_DEFORM_RANGE`).
|
|
128
|
+
*/
|
|
129
|
+
export const FACE_DEFORM_RANGES: Readonly<Record<FaceDeformKey, readonly [number, number]>> = {
|
|
130
|
+
tilt_mid_y: [-0.02, 0],
|
|
131
|
+
tilt_chin_squash_y: [0, 0.015],
|
|
132
|
+
tilt_crown_drop_y: [-0.01, 0],
|
|
133
|
+
tilt_taper_x: [0, 0.015],
|
|
134
|
+
turn_mid_x: [-0.02, 0],
|
|
135
|
+
turn_lead_compress_x: [0, 0.025],
|
|
136
|
+
turn_trail_shift_x: [-0.01, 0.01],
|
|
137
|
+
turn_cheek_arch_y: [0, 0.01],
|
|
138
|
+
tilt_bang_base_y: [-0.01, 0],
|
|
139
|
+
tilt_hair_front_y: [0, 1.5],
|
|
140
|
+
tilt_hair_back_lower_y: [0, 1.5],
|
|
141
|
+
tilt_hair_back_bun_y: [0, 1.5],
|
|
142
|
+
tilt_hair_back_bun_x: [0, 1.5],
|
|
143
|
+
tilt_ear_y: [0, 1.5],
|
|
144
|
+
tilt_topknot_y: [0, 1.5],
|
|
145
|
+
turn_bang_parallax_x: [-0.015, 0],
|
|
146
|
+
turn_hair_back_cheek_x: [0, 1.5],
|
|
147
|
+
turn_bun_counter_x: [0, 0.02],
|
|
148
|
+
turn_hairpin_counter_x: [0, 0.02],
|
|
149
|
+
turn_ear_x: [0, 1.5],
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
/** Uneven 5×5 grid columns and rows for the `face` plate mesh (issue #436). */
|
|
153
|
+
export const FACE_DEFORM_GRID_US: readonly number[] = [0, 0.18, 0.5, 0.82, 1];
|
|
154
|
+
export const FACE_DEFORM_GRID_VS: readonly number[] = [0, 0.22, 0.5, 0.78, 1];
|
|
155
|
+
|
|
156
|
+
/** Resolve {@link FACE_DEFORM_FRACTIONS} with optional recipe overrides. */
|
|
157
|
+
export function resolveFaceDeformFractions(overrides?: Readonly<Partial<FaceDeformFractions>> | null): FaceDeformFractions {
|
|
158
|
+
const out: FaceDeformFractions = { ...FACE_DEFORM_FRACTIONS };
|
|
159
|
+
if (overrides !== undefined && overrides !== null) {
|
|
160
|
+
for (const k of Object.keys(FACE_DEFORM_FRACTIONS) as FaceDeformKey[]) {
|
|
161
|
+
const v = overrides[k];
|
|
162
|
+
if (typeof v === 'number') out[k] = v;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return out;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** `config.turn`: the pivot depth, each carried part's See-through `depth_median`, and optional face surface deform overrides. */
|
|
71
169
|
export interface TurnSpec {
|
|
72
170
|
pivot: number;
|
|
73
171
|
depths: Record<string, number>;
|
|
172
|
+
deform?: Partial<FaceDeformFractions>;
|
|
74
173
|
}
|
|
75
174
|
|
|
76
175
|
/** One carried part: its depth and the fraction of the control's offset it travels. */
|
|
@@ -128,3 +227,568 @@ export function turnTwin(part: string, bone: string): string {
|
|
|
128
227
|
export function turnConstraintName(part: string): string {
|
|
129
228
|
return `${TURN_BONE}_${part}`;
|
|
130
229
|
}
|
|
230
|
+
|
|
231
|
+
const FACE_DEFORM_MULT = 2.5;
|
|
232
|
+
const unmult = (v: number): number => pyRound(v / FACE_DEFORM_MULT, 8);
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Evaluate the pitch (`tilt`) surface deformation field `(dx, dy)` in `head`
|
|
236
|
+
* space at `(hx, hy)` for `deg` degrees of pitch.
|
|
237
|
+
*/
|
|
238
|
+
export function evalPitchFaceSurface(
|
|
239
|
+
hx: number,
|
|
240
|
+
hy: number,
|
|
241
|
+
deg: number,
|
|
242
|
+
W: number,
|
|
243
|
+
H: number,
|
|
244
|
+
X0: number,
|
|
245
|
+
Y0: number,
|
|
246
|
+
f: Readonly<FaceDeformFractions> = FACE_DEFORM_FRACTIONS,
|
|
247
|
+
): [number, number] {
|
|
248
|
+
if (deg === 0) return [0, 0];
|
|
249
|
+
const ux = Math.max(-1.35, Math.min(1.35, (hx - X0) / (W / 2)));
|
|
250
|
+
const uy = Math.max(-1.35, Math.min(1.35, (hy - Y0) / (H / 2)));
|
|
251
|
+
const uChin = Math.min(1.2, Math.max(0, -uy));
|
|
252
|
+
const uCrown = Math.min(1.2, Math.max(0, uy));
|
|
253
|
+
const fracYPerDeg = unmult(f.tilt_mid_y) + unmult(f.tilt_chin_squash_y) * Math.pow(uChin, 1.25) + unmult(f.tilt_crown_drop_y) * Math.pow(uCrown, 1.25);
|
|
254
|
+
const fracXPerDeg = unmult(f.tilt_taper_x) * ux * Math.max(-1.1, Math.min(1.1, uy));
|
|
255
|
+
return [FACE_DEFORM_MULT * deg * W * fracXPerDeg, FACE_DEFORM_MULT * deg * H * fracYPerDeg];
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Evaluate the yaw (`turn`) surface deformation field `(dx, dy)` in `head`
|
|
260
|
+
* space at `(hx, hy)` for `deg` degrees of yaw.
|
|
261
|
+
*/
|
|
262
|
+
export function evalYawFaceSurface(
|
|
263
|
+
hx: number,
|
|
264
|
+
hy: number,
|
|
265
|
+
deg: number,
|
|
266
|
+
W: number,
|
|
267
|
+
H: number,
|
|
268
|
+
X0: number,
|
|
269
|
+
Y0: number,
|
|
270
|
+
f: Readonly<FaceDeformFractions> = FACE_DEFORM_FRACTIONS,
|
|
271
|
+
): [number, number] {
|
|
272
|
+
if (deg === 0) return [0, 0];
|
|
273
|
+
const s = Math.sign(deg);
|
|
274
|
+
const ux = Math.max(-1.35, Math.min(1.35, (hx - X0) / (W / 2)));
|
|
275
|
+
const uy = Math.max(-1.35, Math.min(1.35, (hy - Y0) / (H / 2)));
|
|
276
|
+
const uLead = Math.min(1.2, Math.max(0, -s * ux));
|
|
277
|
+
const uTrail = Math.min(1.2, Math.max(0, s * ux));
|
|
278
|
+
const fracXPerDeg = unmult(f.turn_mid_x) + unmult(f.turn_lead_compress_x) * Math.pow(uLead, 1.2) + unmult(f.turn_trail_shift_x) * Math.pow(uTrail, 1.2);
|
|
279
|
+
const fracYPerDeg = unmult(f.turn_cheek_arch_y) * (-s * ux) * Math.max(0, 1 - Math.abs(uy));
|
|
280
|
+
return [FACE_DEFORM_MULT * deg * W * fracXPerDeg, FACE_DEFORM_MULT * deg * H * fracYPerDeg];
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export interface FaceSurfaceDeformOptions {
|
|
284
|
+
/** Padded `face.png` width in rig px (defaults to the mesh width or 114). */
|
|
285
|
+
imageWidth?: number;
|
|
286
|
+
/** Padded `face.png` height in rig px (defaults to the mesh height or 136). */
|
|
287
|
+
imageHeight?: number;
|
|
288
|
+
/** Opaque face plate width `W` in rig px (defaults to `imageWidth - 8`). */
|
|
289
|
+
opaqueWidth?: number;
|
|
290
|
+
/** Opaque face plate height `H` in rig px (defaults to `imageHeight - 8`). */
|
|
291
|
+
opaqueHeight?: number;
|
|
292
|
+
/** Recipe overrides for {@link FACE_DEFORM_FRACTIONS}. */
|
|
293
|
+
overrides?: Readonly<Partial<FaceDeformFractions>> | null;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
interface DeformWeight {
|
|
297
|
+
bone: string;
|
|
298
|
+
x: number;
|
|
299
|
+
y: number;
|
|
300
|
+
weight: number;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
interface DeformMeshAttachment {
|
|
304
|
+
type: 'mesh';
|
|
305
|
+
image: string;
|
|
306
|
+
width: number;
|
|
307
|
+
height: number;
|
|
308
|
+
uvs: number[];
|
|
309
|
+
triangles: number[];
|
|
310
|
+
hull: number;
|
|
311
|
+
weights: DeformWeight[][];
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
interface DeformRegionAttachment {
|
|
315
|
+
image: string;
|
|
316
|
+
x: number;
|
|
317
|
+
y: number;
|
|
318
|
+
rotation?: number;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
interface DeriveSpec {
|
|
322
|
+
kind: 'yaw' | 'pitch';
|
|
323
|
+
degrees: number;
|
|
324
|
+
about?: number;
|
|
325
|
+
depth?: number | Readonly<Record<string, number>>;
|
|
326
|
+
carried?: number | Readonly<Record<string, number>>;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
interface SliderKey {
|
|
330
|
+
t: number;
|
|
331
|
+
v?: number[];
|
|
332
|
+
derive?: DeriveSpec;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
interface SliderTrack {
|
|
336
|
+
bone?: string;
|
|
337
|
+
group?: string;
|
|
338
|
+
property: string;
|
|
339
|
+
keys: SliderKey[];
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
interface DeformTimelineEntry {
|
|
343
|
+
slot: string;
|
|
344
|
+
attachment: string;
|
|
345
|
+
keys: Array<{ t: number; vertices?: number[] }>;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
interface SliderAnimation {
|
|
349
|
+
duration: number;
|
|
350
|
+
tracks: SliderTrack[];
|
|
351
|
+
deform?: DeformTimelineEntry[];
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function buildFaceGrid(width: number, height: number): { points: Array<[number, number]>; uvs: number[]; triangles: number[]; hull: number } {
|
|
355
|
+
const us = FACE_DEFORM_GRID_US;
|
|
356
|
+
const vs = FACE_DEFORM_GRID_VS;
|
|
357
|
+
const cols = us.length - 1;
|
|
358
|
+
const rows = vs.length - 1;
|
|
359
|
+
const ring: Array<[number, number]> = [];
|
|
360
|
+
for (let c = 0; c <= cols; c++) ring.push([c, 0]);
|
|
361
|
+
for (let r = 1; r <= rows; r++) ring.push([cols, r]);
|
|
362
|
+
for (let c = cols - 1; c >= 0; c--) ring.push([c, rows]);
|
|
363
|
+
for (let r = rows - 1; r >= 1; r--) ring.push([0, r]);
|
|
364
|
+
const interior: Array<[number, number]> = [];
|
|
365
|
+
for (let r = 1; r < rows; r++) for (let c = 1; c < cols; c++) interior.push([c, r]);
|
|
366
|
+
const all = [...ring, ...interior];
|
|
367
|
+
const idx = new Map<string, number>();
|
|
368
|
+
all.forEach(([c, r], i) => idx.set(`${c},${r}`, i));
|
|
369
|
+
const points: Array<[number, number]> = [];
|
|
370
|
+
const uvs: number[] = [];
|
|
371
|
+
for (const [c, r] of all) {
|
|
372
|
+
const u = us[c];
|
|
373
|
+
const v = vs[r];
|
|
374
|
+
points.push([u * width, v * height]);
|
|
375
|
+
uvs.push(pyRound(u, 6), pyRound(v, 6));
|
|
376
|
+
}
|
|
377
|
+
const triangles: number[] = [];
|
|
378
|
+
for (let r = 0; r < rows; r++) {
|
|
379
|
+
for (let c = 0; c < cols; c++) {
|
|
380
|
+
const tl = idx.get(`${c},${r}`) as number;
|
|
381
|
+
const tr = idx.get(`${c + 1},${r}`) as number;
|
|
382
|
+
const bl = idx.get(`${c},${r + 1}`) as number;
|
|
383
|
+
const br = idx.get(`${c + 1},${r + 1}`) as number;
|
|
384
|
+
triangles.push(tl, bl, br, tl, br, tr);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
return { points, uvs, triangles, hull: ring.length };
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Promote the `face` region on `head` to a 5×5 grid mesh and populate the
|
|
392
|
+
* `tilt` and `turn` slider animations with the Set B ×2.5 face and hair mesh
|
|
393
|
+
* deform timelines and ear/topknot tracks (issue #436). When `motion` holds
|
|
394
|
+
* neither `tilt` nor `turn` with slider `derive` keys, neither `rig` nor
|
|
395
|
+
* `motion` is touched.
|
|
396
|
+
*/
|
|
397
|
+
export function applyFaceSurfaceDeform(
|
|
398
|
+
rig: {
|
|
399
|
+
bones: Array<{ name: string; parent?: string; x: number; y: number; rotation?: number; length?: number; inherit?: string }>;
|
|
400
|
+
skins: { default: Record<string, Record<string, DeformMeshAttachment | DeformRegionAttachment>> };
|
|
401
|
+
invariants?: Record<string, unknown>;
|
|
402
|
+
},
|
|
403
|
+
motion: {
|
|
404
|
+
groups?: Record<string, string[]>;
|
|
405
|
+
animations: Record<string, unknown>;
|
|
406
|
+
},
|
|
407
|
+
options?: FaceSurfaceDeformOptions | Readonly<Partial<FaceDeformFractions>> | null,
|
|
408
|
+
): void {
|
|
409
|
+
const rawTilt = motion.animations.tilt as SliderAnimation | undefined;
|
|
410
|
+
const rawTurn = motion.animations.turn as SliderAnimation | undefined;
|
|
411
|
+
if (rawTilt === undefined && rawTurn === undefined) return;
|
|
412
|
+
const hasDerive =
|
|
413
|
+
(rawTilt?.tracks?.some((tr) => tr.keys?.some((k) => k.derive !== undefined)) ?? false) ||
|
|
414
|
+
(rawTurn?.tracks?.some((tr) => tr.keys?.some((k) => k.derive !== undefined)) ?? false);
|
|
415
|
+
if (!hasDerive) return;
|
|
416
|
+
const tiltAnim = rawTilt === undefined ? undefined : structuredClone(rawTilt);
|
|
417
|
+
const turnAnim = rawTurn === undefined ? undefined : structuredClone(rawTurn);
|
|
418
|
+
if (tiltAnim !== undefined) motion.animations.tilt = tiltAnim;
|
|
419
|
+
if (turnAnim !== undefined) motion.animations.turn = turnAnim;
|
|
420
|
+
|
|
421
|
+
const isOpts =
|
|
422
|
+
options !== undefined &&
|
|
423
|
+
options !== null &&
|
|
424
|
+
('imageWidth' in options || 'imageHeight' in options || 'opaqueWidth' in options || 'opaqueHeight' in options || 'overrides' in options);
|
|
425
|
+
const opts: FaceSurfaceDeformOptions = isOpts ? (options as FaceSurfaceDeformOptions) : { overrides: options as Readonly<Partial<FaceDeformFractions>> | null | undefined };
|
|
426
|
+
const f = resolveFaceDeformFractions(opts.overrides);
|
|
427
|
+
|
|
428
|
+
const faceSlot = rig.skins.default.face;
|
|
429
|
+
if (faceSlot === undefined || faceSlot.face === undefined) return;
|
|
430
|
+
const faceAtt = faceSlot.face;
|
|
431
|
+
|
|
432
|
+
let faceW = opts.imageWidth ?? 114;
|
|
433
|
+
let faceH = opts.imageHeight ?? 136;
|
|
434
|
+
let faceRegX = -1;
|
|
435
|
+
let faceRegY = 49;
|
|
436
|
+
|
|
437
|
+
if (!('type' in faceAtt)) {
|
|
438
|
+
faceRegX = faceAtt.x;
|
|
439
|
+
faceRegY = faceAtt.y;
|
|
440
|
+
const grid = buildFaceGrid(faceW, faceH);
|
|
441
|
+
const faceWeights: DeformWeight[][] = grid.points.map(([px, py]) => [
|
|
442
|
+
{
|
|
443
|
+
bone: 'head',
|
|
444
|
+
x: pyRound(faceRegX - faceW / 2 + px, 6),
|
|
445
|
+
y: pyRound(faceRegY + faceH / 2 - py, 6),
|
|
446
|
+
weight: 1,
|
|
447
|
+
},
|
|
448
|
+
]);
|
|
449
|
+
faceSlot.face = {
|
|
450
|
+
type: 'mesh',
|
|
451
|
+
image: faceAtt.image,
|
|
452
|
+
width: faceW,
|
|
453
|
+
height: faceH,
|
|
454
|
+
uvs: grid.uvs,
|
|
455
|
+
triangles: grid.triangles,
|
|
456
|
+
hull: grid.hull,
|
|
457
|
+
weights: faceWeights,
|
|
458
|
+
};
|
|
459
|
+
if (rig.invariants !== undefined) {
|
|
460
|
+
if (typeof rig.invariants.meshSlots === 'number') rig.invariants.meshSlots += 1;
|
|
461
|
+
if (typeof rig.invariants.meshTriangles === 'number') rig.invariants.meshTriangles += grid.triangles.length / 3;
|
|
462
|
+
}
|
|
463
|
+
} else {
|
|
464
|
+
faceW = faceAtt.width;
|
|
465
|
+
faceH = faceAtt.height;
|
|
466
|
+
let sumX = 0;
|
|
467
|
+
let sumY = 0;
|
|
468
|
+
for (const w of faceAtt.weights) {
|
|
469
|
+
sumX += w[0]?.x ?? 0;
|
|
470
|
+
sumY += w[0]?.y ?? 0;
|
|
471
|
+
}
|
|
472
|
+
if (faceAtt.weights.length > 0) {
|
|
473
|
+
faceRegX = Math.round(sumX / faceAtt.weights.length);
|
|
474
|
+
faceRegY = Math.round(sumY / faceAtt.weights.length);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
const W = opts.opaqueWidth ?? faceW - 8;
|
|
479
|
+
const H = opts.opaqueHeight ?? faceH - 8;
|
|
480
|
+
const X0 = faceRegX;
|
|
481
|
+
const Y0 = faceRegY - 1;
|
|
482
|
+
|
|
483
|
+
const boneOrder = new Map<string, number>(rig.bones.map((b, i) => [b.name, i]));
|
|
484
|
+
for (const slotName of ['hair_front', 'hair_back']) {
|
|
485
|
+
const slotObj = rig.skins.default[slotName];
|
|
486
|
+
const att = slotObj?.[slotName];
|
|
487
|
+
if (att !== undefined && 'type' in att) {
|
|
488
|
+
for (const infls of att.weights) {
|
|
489
|
+
infls.sort((a, b) => (boneOrder.get(a.bone) ?? 0) - (boneOrder.get(b.bone) ?? 0));
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
const tf = computeWorldTransforms(rig.bones);
|
|
495
|
+
const headTf = tf.get('head') as BoneTransform | undefined;
|
|
496
|
+
if (headTf === undefined) return;
|
|
497
|
+
|
|
498
|
+
interface VertexSetup {
|
|
499
|
+
hx: number;
|
|
500
|
+
hy: number;
|
|
501
|
+
boneWeights: Record<string, number>;
|
|
502
|
+
infls: DeformWeight[];
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
const getMeshSetup = (att: DeformMeshAttachment): VertexSetup[] =>
|
|
506
|
+
att.weights.map((infls) => {
|
|
507
|
+
let wx = 0;
|
|
508
|
+
let wy = 0;
|
|
509
|
+
const boneWeights: Record<string, number> = {};
|
|
510
|
+
for (const inf of infls) {
|
|
511
|
+
const m = tf.get(inf.bone) as BoneTransform;
|
|
512
|
+
const [x, y] = toWorld(m, inf.x, inf.y);
|
|
513
|
+
wx += x * inf.weight;
|
|
514
|
+
wy += y * inf.weight;
|
|
515
|
+
boneWeights[inf.bone] = (boneWeights[inf.bone] ?? 0) + inf.weight;
|
|
516
|
+
}
|
|
517
|
+
return { hx: wx - headTf.worldX, hy: wy - headTf.worldY, boneWeights, infls };
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
const expandToDeform = (verts: readonly VertexSetup[], evalVertex: (v: VertexSetup) => [number, number]): number[] => {
|
|
521
|
+
const out: number[] = [];
|
|
522
|
+
for (const v of verts) {
|
|
523
|
+
const [dx, dy] = evalVertex(v);
|
|
524
|
+
for (const inf of v.infls) {
|
|
525
|
+
const m = tf.get(inf.bone) as BoneTransform;
|
|
526
|
+
const [bx, by] = toBoneLocalVector(m, dx, dy);
|
|
527
|
+
out.push(pyRound(bx, 4), pyRound(by, 4));
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
return out;
|
|
531
|
+
};
|
|
532
|
+
|
|
533
|
+
const boneMap = new Map(rig.bones.map((b) => [b.name, b]));
|
|
534
|
+
const evalDeriveKey = (boneName: string, k: SliderKey): number => {
|
|
535
|
+
if (Array.isArray(k.v)) return k.v[0] ?? 0;
|
|
536
|
+
const d = k.derive;
|
|
537
|
+
if (d === undefined) return 0;
|
|
538
|
+
const b = boneMap.get(boneName);
|
|
539
|
+
const coord = d.kind === 'yaw' ? (b?.x ?? 0) : (b?.y ?? 0);
|
|
540
|
+
const rad = (d.degrees * Math.PI) / 180;
|
|
541
|
+
const depth = typeof d.depth === 'number' ? d.depth : (d.depth?.[boneName] ?? 0);
|
|
542
|
+
const carried = typeof d.carried === 'number' ? d.carried : (d.carried?.[boneName] ?? 0);
|
|
543
|
+
const val = (coord - (d.about ?? 0)) * (Math.cos(rad) - 1) - (depth - carried) * Math.sin(rad);
|
|
544
|
+
const r = Math.round(val * 1e6) / 1e6;
|
|
545
|
+
return r === 0 ? 0 : r;
|
|
546
|
+
};
|
|
547
|
+
|
|
548
|
+
const mapParent: Readonly<Record<string, string>> = {
|
|
549
|
+
bang_r0: 'bang_r0_ctl',
|
|
550
|
+
bang_r1: 'bang_r0_ctl',
|
|
551
|
+
bang_c0: 'bang_c0_ctl',
|
|
552
|
+
bang_c1: 'bang_c0_ctl',
|
|
553
|
+
bang_l0: 'bang_l0_ctl',
|
|
554
|
+
bang_l1: 'bang_l0_ctl',
|
|
555
|
+
bun: 'bun_ctl',
|
|
556
|
+
};
|
|
557
|
+
|
|
558
|
+
const getBoneDeltaAtTime = (tracks: readonly SliderTrack[], t: number, boneName: string): [number, number] => {
|
|
559
|
+
const driver = mapParent[boneName] ?? boneName;
|
|
560
|
+
let dx = 0;
|
|
561
|
+
let dy = 0;
|
|
562
|
+
for (const tr of tracks) {
|
|
563
|
+
const inGroup = tr.group !== undefined && (motion.groups?.[tr.group]?.includes(driver) ?? false);
|
|
564
|
+
if (tr.bone !== driver && !inGroup) continue;
|
|
565
|
+
const key = tr.keys.find((k) => Math.abs(k.t - t) < 1e-6);
|
|
566
|
+
if (key === undefined) continue;
|
|
567
|
+
const val = evalDeriveKey(driver, key);
|
|
568
|
+
if (tr.property === 'translatex') dx = val;
|
|
569
|
+
if (tr.property === 'translatey') dy = val;
|
|
570
|
+
}
|
|
571
|
+
return [dx, dy];
|
|
572
|
+
};
|
|
573
|
+
|
|
574
|
+
const getVertexBoneDelta = (v: VertexSetup, tracks: readonly SliderTrack[], t: number): [number, number] => {
|
|
575
|
+
let bx = 0;
|
|
576
|
+
let by = 0;
|
|
577
|
+
for (const [bname, w] of Object.entries(v.boneWeights)) {
|
|
578
|
+
const [dx, dy] = getBoneDeltaAtTime(tracks, t, bname);
|
|
579
|
+
bx += dx * w;
|
|
580
|
+
by += dy * w;
|
|
581
|
+
}
|
|
582
|
+
return [bx, by];
|
|
583
|
+
};
|
|
584
|
+
|
|
585
|
+
const faceMesh = faceSlot.face as DeformMeshAttachment;
|
|
586
|
+
const faceVerts = getMeshSetup(faceMesh);
|
|
587
|
+
const hairFrontAtt = rig.skins.default.hair_front?.hair_front;
|
|
588
|
+
const hairBackAtt = rig.skins.default.hair_back?.hair_back;
|
|
589
|
+
const hairFrontVerts = hairFrontAtt !== undefined && 'type' in hairFrontAtt ? getMeshSetup(hairFrontAtt) : null;
|
|
590
|
+
const hairBackVerts = hairBackAtt !== undefined && 'type' in hairBackAtt ? getMeshSetup(hairBackAtt) : null;
|
|
591
|
+
|
|
592
|
+
const hyCrown = Y0 + (67 / 128) * H;
|
|
593
|
+
const hyBunSpan = (85 / 128) * H;
|
|
594
|
+
const hyBangBottom = Y0 - (68 / 128) * H;
|
|
595
|
+
const hyBangSpan = (130 / 128) * H;
|
|
596
|
+
const hyTurnBangBottom = Y0 - (28 / 128) * H;
|
|
597
|
+
const hyTurnBangSpan = (80 / 128) * H;
|
|
598
|
+
const hyEar = Y0 - (16 / 128) * H;
|
|
599
|
+
const earRx = boneMap.get('ear_r')?.x ?? -51;
|
|
600
|
+
const earLx = boneMap.get('ear_l')?.x ?? 47.5;
|
|
601
|
+
|
|
602
|
+
if (tiltAnim !== undefined) {
|
|
603
|
+
const origTiltTracks = tiltAnim.tracks.map((tr) => ({ ...tr, keys: [...tr.keys] }));
|
|
604
|
+
const tiltKeys: Array<{ t: number; deg: number }> = [
|
|
605
|
+
{ t: 0, deg: -25 },
|
|
606
|
+
{ t: 0.75, deg: -10 },
|
|
607
|
+
{ t: 1.25, deg: 0 },
|
|
608
|
+
{ t: 2.0, deg: 15 },
|
|
609
|
+
{ t: 2.5, deg: 25 },
|
|
610
|
+
];
|
|
611
|
+
const tiltDeform: DeformTimelineEntry[] = [
|
|
612
|
+
{
|
|
613
|
+
slot: 'face',
|
|
614
|
+
attachment: 'face',
|
|
615
|
+
keys: tiltKeys.map(({ t, deg }) =>
|
|
616
|
+
deg === 0 ? { t } : { t, vertices: expandToDeform(faceVerts, (v) => evalPitchFaceSurface(v.hx, v.hy, deg, W, H, X0, Y0, f)) },
|
|
617
|
+
),
|
|
618
|
+
},
|
|
619
|
+
];
|
|
620
|
+
if (hairFrontVerts !== null) {
|
|
621
|
+
tiltDeform.push({
|
|
622
|
+
slot: 'hair_front',
|
|
623
|
+
attachment: 'hair_front',
|
|
624
|
+
keys: tiltKeys.map(({ t, deg }) =>
|
|
625
|
+
deg === 0
|
|
626
|
+
? { t }
|
|
627
|
+
: {
|
|
628
|
+
t,
|
|
629
|
+
vertices: expandToDeform(hairFrontVerts, (v) => {
|
|
630
|
+
const [dxSurf, dySurf] = evalPitchFaceSurface(v.hx, v.hy, deg, W, H, X0, Y0, f);
|
|
631
|
+
const [bx, by] = getVertexBoneDelta(v, origTiltTracks, t);
|
|
632
|
+
const baseBangDy = (deg / 15) * (f.tilt_bang_base_y * 15 * H) * Math.max(0, Math.min(1, (v.hy - hyBangBottom) / hyBangSpan));
|
|
633
|
+
return [dxSurf - bx, baseBangDy + dySurf * f.tilt_hair_front_y - by];
|
|
634
|
+
}),
|
|
635
|
+
},
|
|
636
|
+
),
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
if (hairBackVerts !== null) {
|
|
640
|
+
tiltDeform.push({
|
|
641
|
+
slot: 'hair_back',
|
|
642
|
+
attachment: 'hair_back',
|
|
643
|
+
keys: tiltKeys.map(({ t, deg }) =>
|
|
644
|
+
deg === 0
|
|
645
|
+
? { t }
|
|
646
|
+
: {
|
|
647
|
+
t,
|
|
648
|
+
vertices: expandToDeform(hairBackVerts, (v) => {
|
|
649
|
+
const [bx, by] = getVertexBoneDelta(v, origTiltTracks, t);
|
|
650
|
+
const [dxSurf, dySurf] = evalPitchFaceSurface(v.hx, Math.min(hyCrown, v.hy), deg, W, H, X0, Y0, f);
|
|
651
|
+
const uBun = Math.max(0, Math.min(1, (v.hy - hyCrown) / hyBunSpan));
|
|
652
|
+
const targetDy = (1 - uBun) * (dySurf * f.tilt_hair_back_lower_y) + uBun * (dySurf * f.tilt_hair_back_bun_y);
|
|
653
|
+
return [dxSurf * (1 - pyRound(1 - f.tilt_hair_back_bun_x, 8) * uBun) - bx, targetDy - by];
|
|
654
|
+
}),
|
|
655
|
+
},
|
|
656
|
+
),
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
for (const tr of tiltAnim.tracks) {
|
|
660
|
+
if (tr.group === 'ears' && tr.property === 'translatey') {
|
|
661
|
+
tr.keys = tr.keys.map((k) => {
|
|
662
|
+
const deg = k.derive?.degrees ?? 0;
|
|
663
|
+
if (deg === 0) return { t: k.t, v: [0] };
|
|
664
|
+
const [, dyEar] = evalPitchFaceSurface(0, hyEar, deg, W, H, X0, Y0, f);
|
|
665
|
+
return { t: k.t, v: [pyRound(dyEar * f.tilt_ear_y, 4)] };
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
if (tr.group === 'topknot' && tr.property === 'translatey') {
|
|
669
|
+
tr.keys = tr.keys.map((k) => {
|
|
670
|
+
const deg = k.derive?.degrees ?? 0;
|
|
671
|
+
if (deg === 0) return { t: k.t, v: [0] };
|
|
672
|
+
const [, dyCrown] = evalPitchFaceSurface(0, hyCrown, deg, W, H, X0, Y0, f);
|
|
673
|
+
return { t: k.t, v: [pyRound(dyCrown * f.tilt_topknot_y, 4)] };
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
const refTiltKeys = origTiltTracks[0]?.keys ?? [];
|
|
678
|
+
for (const [earBone, earHx] of [
|
|
679
|
+
['ear_r', earRx],
|
|
680
|
+
['ear_l', earLx],
|
|
681
|
+
] as const) {
|
|
682
|
+
if (!boneMap.has(earBone)) continue;
|
|
683
|
+
tiltAnim.tracks.push({
|
|
684
|
+
bone: earBone,
|
|
685
|
+
property: 'translatex',
|
|
686
|
+
keys: refTiltKeys.map((k) => {
|
|
687
|
+
const deg = k.derive?.degrees ?? 0;
|
|
688
|
+
if (deg === 0) return { t: k.t, v: [0] };
|
|
689
|
+
const [dxEar] = evalPitchFaceSurface(earHx, hyEar, deg, W, H, X0, Y0, f);
|
|
690
|
+
return { t: k.t, v: [pyRound(dxEar, 4)] };
|
|
691
|
+
}),
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
tiltAnim.deform = tiltDeform;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
if (turnAnim !== undefined) {
|
|
698
|
+
const origTurnTracks = turnAnim.tracks.map((tr) => ({ ...tr, keys: [...tr.keys] }));
|
|
699
|
+
const turnKeys: Array<{ t: number; deg: number }> = [
|
|
700
|
+
{ t: 0, deg: -30 },
|
|
701
|
+
{ t: 0.75, deg: -15 },
|
|
702
|
+
{ t: 1.5, deg: 0 },
|
|
703
|
+
{ t: 2.25, deg: 15 },
|
|
704
|
+
{ t: 3.0, deg: 30 },
|
|
705
|
+
];
|
|
706
|
+
const turnDeform: DeformTimelineEntry[] = [
|
|
707
|
+
{
|
|
708
|
+
slot: 'face',
|
|
709
|
+
attachment: 'face',
|
|
710
|
+
keys: turnKeys.map(({ t, deg }) =>
|
|
711
|
+
deg === 0 ? { t } : { t, vertices: expandToDeform(faceVerts, (v) => evalYawFaceSurface(v.hx, v.hy, deg, W, H, X0, Y0, f)) },
|
|
712
|
+
),
|
|
713
|
+
},
|
|
714
|
+
];
|
|
715
|
+
if (hairFrontVerts !== null) {
|
|
716
|
+
turnDeform.push({
|
|
717
|
+
slot: 'hair_front',
|
|
718
|
+
attachment: 'hair_front',
|
|
719
|
+
keys: turnKeys.map(({ t, deg }) =>
|
|
720
|
+
deg === 0
|
|
721
|
+
? { t }
|
|
722
|
+
: {
|
|
723
|
+
t,
|
|
724
|
+
vertices: expandToDeform(hairFrontVerts, (v) => {
|
|
725
|
+
const [dxSurf, dySurf] = evalYawFaceSurface(v.hx, v.hy, deg, W, H, X0, Y0, f);
|
|
726
|
+
const [bx, by] = getVertexBoneDelta(v, origTurnTracks, t);
|
|
727
|
+
const uBang = Math.max(0, Math.min(1, (v.hy - hyTurnBangBottom) / hyTurnBangSpan));
|
|
728
|
+
const bangParallaxX = (deg / 15) * pyRound(f.turn_bang_parallax_x * 15 * W, 8) * uBang;
|
|
729
|
+
return [bangParallaxX + dxSurf - bx, dySurf - by];
|
|
730
|
+
}),
|
|
731
|
+
},
|
|
732
|
+
),
|
|
733
|
+
});
|
|
734
|
+
}
|
|
735
|
+
if (hairBackVerts !== null) {
|
|
736
|
+
turnDeform.push({
|
|
737
|
+
slot: 'hair_back',
|
|
738
|
+
attachment: 'hair_back',
|
|
739
|
+
keys: turnKeys.map(({ t, deg }) =>
|
|
740
|
+
deg === 0
|
|
741
|
+
? { t }
|
|
742
|
+
: {
|
|
743
|
+
t,
|
|
744
|
+
vertices: expandToDeform(hairBackVerts, (v) => {
|
|
745
|
+
const [bx, by] = getVertexBoneDelta(v, origTurnTracks, t);
|
|
746
|
+
const [dxCheek, dyCheek] = evalYawFaceSurface(v.hx, v.hy, deg, W, H, X0, Y0, f);
|
|
747
|
+
const uBun = Math.max(0, Math.min(1, (v.hy - hyCrown) / hyBunSpan));
|
|
748
|
+
const bunCounterX = FACE_DEFORM_MULT * (deg / 15) * pyRound((f.turn_bun_counter_x * 15 * W) / FACE_DEFORM_MULT, 8);
|
|
749
|
+
const targetDx = (1 - uBun) * (dxCheek * f.turn_hair_back_cheek_x) + uBun * bunCounterX;
|
|
750
|
+
return [targetDx - bx, (1 - uBun) * dyCheek - by];
|
|
751
|
+
}),
|
|
752
|
+
},
|
|
753
|
+
),
|
|
754
|
+
});
|
|
755
|
+
}
|
|
756
|
+
const refTurnKeys = origTurnTracks[0]?.keys ?? [];
|
|
757
|
+
turnAnim.tracks = turnAnim.tracks.filter(
|
|
758
|
+
(tr) => !(tr.group === 'ears' && tr.property === 'translatex') && !(tr.group === 'topknot' && tr.property === 'translatex'),
|
|
759
|
+
);
|
|
760
|
+
for (const [earBone, earHx] of [
|
|
761
|
+
['ear_r', earRx],
|
|
762
|
+
['ear_l', earLx],
|
|
763
|
+
] as const) {
|
|
764
|
+
if (!boneMap.has(earBone)) continue;
|
|
765
|
+
turnAnim.tracks.push({
|
|
766
|
+
bone: earBone,
|
|
767
|
+
property: 'translatex',
|
|
768
|
+
keys: refTurnKeys.map((k) => {
|
|
769
|
+
const deg = k.derive?.degrees ?? 0;
|
|
770
|
+
if (deg === 0) return { t: k.t, v: [0] };
|
|
771
|
+
const [dxCheekEdge] = evalYawFaceSurface(earHx, hyEar, deg, W, H, X0, Y0, f);
|
|
772
|
+
return { t: k.t, v: [pyRound(dxCheekEdge * f.turn_ear_x, 4)] };
|
|
773
|
+
}),
|
|
774
|
+
});
|
|
775
|
+
}
|
|
776
|
+
for (const [tkBone, tkFrac] of [
|
|
777
|
+
['bun_ctl', f.turn_bun_counter_x],
|
|
778
|
+
['hairpin', f.turn_hairpin_counter_x],
|
|
779
|
+
] as const) {
|
|
780
|
+
if (!boneMap.has(tkBone)) continue;
|
|
781
|
+
turnAnim.tracks.push({
|
|
782
|
+
bone: tkBone,
|
|
783
|
+
property: 'translatex',
|
|
784
|
+
keys: refTurnKeys.map((k) => {
|
|
785
|
+
const deg = k.derive?.degrees ?? 0;
|
|
786
|
+
if (deg === 0) return { t: k.t, v: [0] };
|
|
787
|
+
const tkPx = pyRound((tkFrac * 15 * W) / FACE_DEFORM_MULT, 8);
|
|
788
|
+
return { t: k.t, v: [pyRound(FACE_DEFORM_MULT * (deg / 15) * tkPx, 4)] };
|
|
789
|
+
}),
|
|
790
|
+
});
|
|
791
|
+
}
|
|
792
|
+
turnAnim.deform = turnDeform;
|
|
793
|
+
}
|
|
794
|
+
}
|