@doki-land/live2d-renderer 0.0.0 → 0.0.12

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.
@@ -0,0 +1,394 @@
1
+ /**
2
+ * moc2 default-pose deformer bake: affine + mesh-warp parent chain.
3
+ * Clean-room from public moc2 layout / observed keyform semantics.
4
+ */
5
+
6
+ import { interpolateKeyforms, resolveKeyformBlend } from "./moc2-keyforms.js";
7
+ import type {
8
+ Moc2Affine,
9
+ Moc2BaseDeformer,
10
+ Moc2DrawableMesh,
11
+ Moc2ModelImpl,
12
+ } from "./moc2-objects.js";
13
+
14
+ const DEG = Math.PI / 180;
15
+ const DST_BASE = "DST_BASE";
16
+
17
+ export function isRootBaseId(id: string | null | undefined): boolean {
18
+ return !id || id === DST_BASE;
19
+ }
20
+
21
+ const IDENTITY: Moc2Affine = {
22
+ kind: "affine",
23
+ originX: 0,
24
+ originY: 0,
25
+ scaleX: 1,
26
+ scaleY: 1,
27
+ rotation: 0,
28
+ reflectX: false,
29
+ reflectY: false,
30
+ };
31
+
32
+ function lerp(a: number, b: number, t: number): number {
33
+ return a + (b - a) * t;
34
+ }
35
+
36
+ /** Sample an affine deformer's keyforms at the given parameters. */
37
+ export function sampleAffine(
38
+ deformer: Moc2BaseDeformer,
39
+ getParam: (id: string) => number,
40
+ ): Moc2Affine {
41
+ const list = deformer.affines ?? [];
42
+ if (list.length === 0) return { ...IDENTITY };
43
+ if (list.length === 1) return { ...list[0]! };
44
+
45
+ const { indices, weights, lerpCount } = resolveKeyformBlend(
46
+ deformer.pivotManager,
47
+ getParam,
48
+ );
49
+
50
+ const pick = (i: number) => list[indices[i] ?? 0] ?? list[0]!;
51
+
52
+ if (lerpCount <= 0) return { ...pick(0) };
53
+
54
+ if (lerpCount === 1) {
55
+ const a = pick(0);
56
+ const b = pick(1);
57
+ const t = weights[0]!;
58
+ return {
59
+ kind: "affine",
60
+ originX: lerp(a.originX, b.originX, t),
61
+ originY: lerp(a.originY, b.originY, t),
62
+ scaleX: lerp(a.scaleX, b.scaleX, t),
63
+ scaleY: lerp(a.scaleY, b.scaleY, t),
64
+ rotation: lerp(a.rotation, b.rotation, t),
65
+ reflectX: a.reflectX,
66
+ reflectY: a.reflectY,
67
+ };
68
+ }
69
+
70
+ // Multilinear over corners (same weights as keyform blend).
71
+ const corners = 1 << lerpCount;
72
+ let ox = 0,
73
+ oy = 0,
74
+ sx = 0,
75
+ sy = 0,
76
+ rot = 0;
77
+ for (let c = 0; c < corners; c++) {
78
+ let w = 1;
79
+ for (let d = 0; d < lerpCount; d++) {
80
+ const t = weights[d]!;
81
+ w *= (c & (1 << d)) === 0 ? 1 - t : t;
82
+ }
83
+ const a = pick(c);
84
+ ox += w * a.originX;
85
+ oy += w * a.originY;
86
+ sx += w * a.scaleX;
87
+ sy += w * a.scaleY;
88
+ rot += w * a.rotation;
89
+ }
90
+ const base = pick(0);
91
+ return {
92
+ kind: "affine",
93
+ originX: ox,
94
+ originY: oy,
95
+ scaleX: sx,
96
+ scaleY: sy,
97
+ rotation: rot,
98
+ reflectX: base.reflectX,
99
+ reflectY: base.reflectY,
100
+ };
101
+ }
102
+
103
+ /** Apply a local affine (Core-compatible: totalScale ≈ scaleX). */
104
+ export function applyAffine(
105
+ positions: Float32Array,
106
+ aff: Moc2Affine,
107
+ totalScale = aff.scaleX,
108
+ ): Float32Array {
109
+ const out = new Float32Array(positions.length);
110
+ const sn = Math.sin(aff.rotation * DEG);
111
+ const cs = Math.cos(aff.rotation * DEG);
112
+ const rx = aff.reflectX ? -1 : 1;
113
+ const ry = aff.reflectY ? -1 : 1;
114
+ const m00 = cs * totalScale * rx;
115
+ const m01 = -sn * totalScale * ry;
116
+ const m10 = sn * totalScale * rx;
117
+ const m11 = cs * totalScale * ry;
118
+ const tx = aff.originX;
119
+ const ty = aff.originY;
120
+ for (let i = 0; i + 1 < positions.length; i += 2) {
121
+ const x = positions[i]!;
122
+ const y = positions[i + 1]!;
123
+ out[i] = m00 * x + m01 * y + tx;
124
+ out[i + 1] = m10 * x + m11 * y + ty;
125
+ }
126
+ return out;
127
+ }
128
+
129
+ /**
130
+ * Warp points through a Live2D mesh deformer grid (sdk2 bilinear).
131
+ * `rows` = _$o, `cols` = _$A; grid length = (rows+1)*(cols+1)*2.
132
+ */
133
+ export function warpPointsByMesh(
134
+ src: Float32Array,
135
+ grid: Float32Array,
136
+ rows: number,
137
+ cols: number,
138
+ ): Float32Array {
139
+ const out = new Float32Array(src.length);
140
+ const o = rows;
141
+ const A = cols;
142
+ // Core: index = ix + iy * (o + 1); o=_$o (rows), A=_$A (cols)
143
+ const stride = o + 1;
144
+
145
+ // Exterior basis (lazy).
146
+ let ready = false;
147
+ let cx = 0,
148
+ cy = 0,
149
+ bl = 0,
150
+ bk = 0,
151
+ bf = 0,
152
+ be = 0;
153
+
154
+ const g = (ix: number, iy: number): [number, number] => {
155
+ const i = (ix + iy * stride) * 2;
156
+ return [grid[i] ?? 0, grid[i + 1] ?? 0];
157
+ };
158
+
159
+ for (let i = 0; i + 1 < src.length; i += 2) {
160
+ const lx = src[i]!;
161
+ const ly = src[i + 1]!;
162
+ const bd = lx * o;
163
+ const a7 = ly * A;
164
+
165
+ if (bd < 0 || a7 < 0 || o <= bd || A <= a7) {
166
+ if (!ready) {
167
+ ready = true;
168
+ const [x00, y00] = g(0, 0);
169
+ const [x10, y10] = g(o, 0);
170
+ const [x01, y01] = g(0, A);
171
+ const [x11, y11] = g(o, A);
172
+ cx = 0.25 * (x00 + x10 + x01 + x11);
173
+ cy = 0.25 * (y00 + y10 + y01 + y11);
174
+ const aM = x11 - x00;
175
+ const aL = y11 - y00;
176
+ const bh = x10 - x01;
177
+ const bg = y10 - y01;
178
+ bl = (aM + bh) * 0.5;
179
+ bk = (aL + bg) * 0.5;
180
+ bf = (aM - bh) * 0.5;
181
+ be = (aL - bg) * 0.5;
182
+ cx -= 0.5 * (bl + bf);
183
+ cy -= 0.5 * (bk + be);
184
+ }
185
+ if (lx > -2 && lx < 3 && ly > -2 && ly < 3) {
186
+ // Clamp to nearest in-bound cell bilinear (good enough for bake).
187
+ const u = Math.min(1, Math.max(0, lx));
188
+ const v = Math.min(1, Math.max(0, ly));
189
+ const bd2 = u * o;
190
+ const a72 = v * A;
191
+ const ix = Math.min(o - 1, Math.max(0, bd2 | 0));
192
+ const iy = Math.min(A - 1, Math.max(0, a72 | 0));
193
+ const bn = bd2 - ix;
194
+ const bm = a72 - iy;
195
+ const base = 2 * (ix + iy * stride);
196
+ if (bn + bm < 1) {
197
+ out[i] =
198
+ (grid[base] ?? 0) * (1 - bn - bm) +
199
+ (grid[base + 2] ?? 0) * bn +
200
+ (grid[base + 2 * stride] ?? 0) * bm;
201
+ out[i + 1] =
202
+ (grid[base + 1] ?? 0) * (1 - bn - bm) +
203
+ (grid[base + 3] ?? 0) * bn +
204
+ (grid[base + 2 * stride + 1] ?? 0) * bm;
205
+ } else {
206
+ out[i] =
207
+ (grid[base + 2 * stride + 2] ?? 0) * (bn - 1 + bm) +
208
+ (grid[base + 2 * stride] ?? 0) * (1 - bn) +
209
+ (grid[base + 2] ?? 0) * (1 - bm);
210
+ out[i + 1] =
211
+ (grid[base + 2 * stride + 3] ?? 0) * (bn - 1 + bm) +
212
+ (grid[base + 2 * stride + 1] ?? 0) * (1 - bn) +
213
+ (grid[base + 3] ?? 0) * (1 - bm);
214
+ }
215
+ } else {
216
+ out[i] = cx + lx * bl + ly * bf;
217
+ out[i + 1] = cy + lx * bk + ly * be;
218
+ }
219
+ continue;
220
+ }
221
+
222
+ const bn = bd - (bd | 0);
223
+ const bm = a7 - (a7 | 0);
224
+ const base = 2 * ((bd | 0) + (a7 | 0) * stride);
225
+ if (bn + bm < 1) {
226
+ out[i] =
227
+ (grid[base] ?? 0) * (1 - bn - bm) +
228
+ (grid[base + 2] ?? 0) * bn +
229
+ (grid[base + 2 * stride] ?? 0) * bm;
230
+ out[i + 1] =
231
+ (grid[base + 1] ?? 0) * (1 - bn - bm) +
232
+ (grid[base + 3] ?? 0) * bn +
233
+ (grid[base + 2 * stride + 1] ?? 0) * bm;
234
+ } else {
235
+ out[i] =
236
+ (grid[base + 2 * stride + 2] ?? 0) * (bn - 1 + bm) +
237
+ (grid[base + 2 * stride] ?? 0) * (1 - bn) +
238
+ (grid[base + 2] ?? 0) * (1 - bm);
239
+ out[i + 1] =
240
+ (grid[base + 2 * stride + 3] ?? 0) * (bn - 1 + bm) +
241
+ (grid[base + 2 * stride + 1] ?? 0) * (1 - bn) +
242
+ (grid[base + 3] ?? 0) * (1 - bm);
243
+ }
244
+ }
245
+ return out;
246
+ }
247
+
248
+ type WorldOp =
249
+ | { kind: "affine"; aff: Moc2Affine; totalScale: number }
250
+ | { kind: "mesh"; grid: Float32Array; rows: number; cols: number };
251
+
252
+ function applyOp(positions: Float32Array, op: WorldOp): Float32Array {
253
+ if (op.kind === "affine") {
254
+ return applyAffine(positions, op.aff, op.totalScale);
255
+ }
256
+ return warpPointsByMesh(positions, op.grid, op.rows, op.cols);
257
+ }
258
+
259
+ function sampleMeshGrid(
260
+ deformer: Moc2BaseDeformer,
261
+ getParam: (id: string) => number,
262
+ ): Float32Array {
263
+ const rows = deformer.rows ?? 0;
264
+ const cols = deformer.cols ?? 0;
265
+ const floatCount = (rows + 1) * (cols + 1) * 2;
266
+ return interpolateKeyforms(
267
+ deformer.keyforms ?? [],
268
+ deformer.pivotManager,
269
+ getParam,
270
+ floatCount,
271
+ );
272
+ }
273
+
274
+ function collectDeformers(model: Moc2ModelImpl): Map<string, Moc2BaseDeformer> {
275
+ const map = new Map<string, Moc2BaseDeformer>();
276
+ for (const part of model.parts) {
277
+ for (const d of part.baseData) {
278
+ if (d.id) map.set(d.id, d);
279
+ }
280
+ }
281
+ return map;
282
+ }
283
+
284
+ function topoOrder(defs: Map<string, Moc2BaseDeformer>): string[] {
285
+ const visiting = new Set<string>();
286
+ const done = new Set<string>();
287
+ const out: string[] = [];
288
+
289
+ const visit = (id: string) => {
290
+ if (done.has(id) || !defs.has(id)) return;
291
+ if (visiting.has(id)) return; // cycle guard
292
+ visiting.add(id);
293
+ const def = defs.get(id)!;
294
+ if (!isRootBaseId(def.targetBaseId)) {
295
+ visit(def.targetBaseId!);
296
+ }
297
+ visiting.delete(id);
298
+ done.add(id);
299
+ out.push(id);
300
+ };
301
+
302
+ for (const id of defs.keys()) visit(id);
303
+ return out;
304
+ }
305
+
306
+ /** Build world-space deformer ops at the given parameter values. */
307
+ export function bakeDeformerOps(
308
+ model: Moc2ModelImpl,
309
+ getParam: (id: string) => number,
310
+ ): Map<string, WorldOp> {
311
+ const defs = collectDeformers(model);
312
+ const world = new Map<string, WorldOp>();
313
+
314
+ for (const id of topoOrder(defs)) {
315
+ const def = defs.get(id)!;
316
+ const parentId = def.targetBaseId;
317
+ const parentOp =
318
+ !isRootBaseId(parentId) && parentId
319
+ ? world.get(parentId)
320
+ : undefined;
321
+
322
+ if (def.kind === "affineDeformer") {
323
+ const local = sampleAffine(def, getParam);
324
+ if (!parentOp) {
325
+ world.set(id, {
326
+ kind: "affine",
327
+ aff: local,
328
+ totalScale: local.scaleX,
329
+ });
330
+ } else if (parentOp.kind === "affine") {
331
+ const origin = applyOp(
332
+ new Float32Array([local.originX, local.originY]),
333
+ parentOp,
334
+ );
335
+ const composed: Moc2Affine = {
336
+ ...local,
337
+ originX: origin[0]!,
338
+ originY: origin[1]!,
339
+ rotation: local.rotation + parentOp.aff.rotation,
340
+ };
341
+ world.set(id, {
342
+ kind: "affine",
343
+ aff: composed,
344
+ totalScale: parentOp.totalScale * local.scaleX,
345
+ });
346
+ } else {
347
+ // Parent is mesh: move origin through mesh; keep local rotation.
348
+ const origin = applyOp(
349
+ new Float32Array([local.originX, local.originY]),
350
+ parentOp,
351
+ );
352
+ world.set(id, {
353
+ kind: "affine",
354
+ aff: {
355
+ ...local,
356
+ originX: origin[0]!,
357
+ originY: origin[1]!,
358
+ },
359
+ totalScale: local.scaleX,
360
+ });
361
+ }
362
+ continue;
363
+ }
364
+
365
+ // mesh deformer
366
+ let grid = sampleMeshGrid(def, getParam);
367
+ if (parentOp) {
368
+ grid = applyOp(grid, parentOp);
369
+ }
370
+ world.set(id, {
371
+ kind: "mesh",
372
+ grid,
373
+ rows: def.rows ?? 0,
374
+ cols: def.cols ?? 0,
375
+ });
376
+ }
377
+
378
+ return world;
379
+ }
380
+
381
+ /** Transform drawable local positions into model/canvas space. */
382
+ export function transformDrawablePositions(
383
+ mesh: Moc2DrawableMesh,
384
+ local: Float32Array,
385
+ ops: Map<string, WorldOp>,
386
+ ): Float32Array {
387
+ const parent = mesh.targetBaseId;
388
+ if (isRootBaseId(parent) || !parent) return local;
389
+ const op = ops.get(parent);
390
+ if (!op) return local;
391
+ return applyOp(local, op);
392
+ }
393
+
394
+ export type { WorldOp };
@@ -0,0 +1,178 @@
1
+ /**
2
+ * Sample moc2 keyform tables at given parameter values (default-pose bake).
3
+ */
4
+
5
+ import type { Moc2Pivot, Moc2PivotManager } from "./moc2-objects.js";
6
+
7
+ const EPS = 0.001;
8
+
9
+ interface PivotSample {
10
+ index: number;
11
+ /** 0 → exact at index; (0,1) → lerp index..index+1 */
12
+ weight: number;
13
+ }
14
+
15
+ function samplePivot(pivot: Moc2Pivot, value: number): PivotSample {
16
+ const count = pivot.pivotCount;
17
+ const values = pivot.pivotValues;
18
+ if (count < 1) return { index: 0, weight: 0 };
19
+ if (count === 1) return { index: 0, weight: 0 };
20
+
21
+ const first = values[0] ?? 0;
22
+ if (value < first + EPS) return { index: 0, weight: 0 };
23
+
24
+ for (let i = 1; i < count; i++) {
25
+ const lo = values[i - 1] ?? 0;
26
+ const hi = values[i] ?? 0;
27
+ if (value < hi + EPS) {
28
+ if (value > hi - EPS) return { index: i, weight: 0 };
29
+ const span = hi - lo;
30
+ return {
31
+ index: i - 1,
32
+ weight: span === 0 ? 0 : (value - lo) / span,
33
+ };
34
+ }
35
+ }
36
+ return { index: count - 1, weight: 0 };
37
+ }
38
+
39
+ /**
40
+ * Build keyform indices + lerp weights for a pivot manager
41
+ * (multilinear blend over pivoting parameters).
42
+ */
43
+ export function resolveKeyformBlend(
44
+ manager: Moc2PivotManager | null | undefined,
45
+ getParam: (id: string) => number,
46
+ ): { indices: number[]; weights: number[]; lerpCount: number } {
47
+ if (!manager || manager.pivots.length === 0) {
48
+ return { indices: [0], weights: [], lerpCount: 0 };
49
+ }
50
+
51
+ const samples = manager.pivots.map((p) =>
52
+ samplePivot(p, getParam(p.paramId)),
53
+ );
54
+ let lerpCount = 0;
55
+ for (const s of samples) if (s.weight > 0) lerpCount++;
56
+
57
+ const n = 1 << lerpCount;
58
+ const indices = new Array<number>(n).fill(0);
59
+ const weights: number[] = [];
60
+
61
+ let stride = 1;
62
+ let lerpDim = 0;
63
+ for (let p = 0; p < samples.length; p++) {
64
+ const s = samples[p]!;
65
+ const pivotCount = Math.max(1, manager.pivots[p]?.pivotCount ?? 1);
66
+ if (s.weight === 0) {
67
+ const add = s.index * stride;
68
+ for (let i = 0; i < n; i++) indices[i]! += add;
69
+ } else {
70
+ const a = s.index * stride;
71
+ const b = (s.index + 1) * stride;
72
+ const dimMask = 1 << lerpDim;
73
+ for (let i = 0; i < n; i++) {
74
+ indices[i]! += (i & dimMask) === 0 ? a : b;
75
+ }
76
+ weights[lerpDim] = s.weight;
77
+ lerpDim++;
78
+ }
79
+ stride *= pivotCount;
80
+ }
81
+
82
+ return { indices, weights, lerpCount };
83
+ }
84
+
85
+ /** Interpolate a list of float keyforms (each length `floatCount`). */
86
+ export function interpolateKeyforms(
87
+ keyforms: readonly Float32Array[],
88
+ manager: Moc2PivotManager | null | undefined,
89
+ getParam: (id: string) => number,
90
+ floatCount: number,
91
+ ): Float32Array {
92
+ const out = new Float32Array(floatCount);
93
+ if (keyforms.length === 0) return out;
94
+
95
+ const { indices, weights, lerpCount } = resolveKeyformBlend(
96
+ manager,
97
+ getParam,
98
+ );
99
+
100
+ if (lerpCount <= 0) {
101
+ const src = keyforms[indices[0] ?? 0] ?? keyforms[0]!;
102
+ const n = Math.min(floatCount, src.length);
103
+ out.set(src.subarray(0, n));
104
+ return out;
105
+ }
106
+
107
+ if (lerpCount === 1) {
108
+ const a = keyforms[indices[0]!] ?? keyforms[0]!;
109
+ const b = keyforms[indices[1]!] ?? a;
110
+ const t = weights[0]!;
111
+ const u = 1 - t;
112
+ for (let i = 0; i < floatCount; i++) {
113
+ out[i] = (a[i] ?? 0) * u + (b[i] ?? 0) * t;
114
+ }
115
+ return out;
116
+ }
117
+
118
+ if (lerpCount === 2) {
119
+ const a = keyforms[indices[0]!] ?? keyforms[0]!;
120
+ const b = keyforms[indices[1]!] ?? a;
121
+ const c = keyforms[indices[2]!] ?? a;
122
+ const d = keyforms[indices[3]!] ?? a;
123
+ const t = weights[0]!;
124
+ const s = weights[1]!;
125
+ const u = 1 - t;
126
+ const v = 1 - s;
127
+ const w00 = v * u;
128
+ const w10 = v * t;
129
+ const w01 = s * u;
130
+ const w11 = s * t;
131
+ for (let i = 0; i < floatCount; i++) {
132
+ out[i] =
133
+ w00 * (a[i] ?? 0) +
134
+ w10 * (b[i] ?? 0) +
135
+ w01 * (c[i] ?? 0) +
136
+ w11 * (d[i] ?? 0);
137
+ }
138
+ return out;
139
+ }
140
+
141
+ const corners = 1 << lerpCount;
142
+ const cornerW = new Float32Array(corners);
143
+ for (let c = 0; c < corners; c++) {
144
+ let w = 1;
145
+ for (let d = 0; d < lerpCount; d++) {
146
+ const t = weights[d]!;
147
+ w *= (c & (1 << d)) === 0 ? 1 - t : t;
148
+ }
149
+ cornerW[c] = w;
150
+ }
151
+ for (let i = 0; i < floatCount; i++) {
152
+ let sum = 0;
153
+ for (let c = 0; c < corners; c++) {
154
+ const src = keyforms[indices[c]!] ?? keyforms[0]!;
155
+ sum += cornerW[c]! * (src[i] ?? 0);
156
+ }
157
+ out[i] = sum;
158
+ }
159
+ return out;
160
+ }
161
+
162
+ /** Interpolate a scalar table (draw order / opacity) parallel to keyform combos. */
163
+ export function interpolateScalarTable(
164
+ table: ArrayLike<number> | null | undefined,
165
+ manager: Moc2PivotManager | null | undefined,
166
+ getParam: (id: string) => number,
167
+ fallback: number,
168
+ ): number {
169
+ if (!table || table.length === 0) return fallback;
170
+ const perKey: Float32Array[] = [];
171
+ for (let i = 0; i < table.length; i++) {
172
+ const f = new Float32Array(1);
173
+ f[0] = table[i]!;
174
+ perKey.push(f);
175
+ }
176
+ const sampled = interpolateKeyforms(perKey, manager, getParam, 1);
177
+ return sampled[0] ?? fallback;
178
+ }