@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.
- package/README.md +1 -1
- package/dist/index.d.ts +540 -0
- package/dist/index.js +5582 -0
- package/package.json +45 -3
- package/src/backends/canvas2d.ts +447 -0
- package/src/backends/webgl2.ts +685 -0
- package/src/backends/webgpu.ts +1336 -0
- package/src/blend.ts +93 -0
- package/src/clipping.ts +423 -0
- package/src/coords.ts +24 -0
- package/src/cpu/cpu-program.ts +176 -0
- package/src/cpu/evaluate.ts +101 -0
- package/src/create-renderer.ts +143 -0
- package/src/detect-format.ts +19 -0
- package/src/index.ts +121 -0
- package/src/moc/decode.ts +73 -0
- package/src/moc/drawable-flags.ts +42 -0
- package/src/moc/moc2-deform.ts +394 -0
- package/src/moc/moc2-keyforms.ts +178 -0
- package/src/moc/moc2-objects.ts +464 -0
- package/src/moc/moc2-reader.ts +224 -0
- package/src/moc/moc2-to-program.ts +213 -0
- package/src/moc/moc2.ts +184 -0
- package/src/moc/moc3-deform.ts +394 -0
- package/src/moc/moc3-glue.ts +151 -0
- package/src/moc/moc3-keyforms.ts +230 -0
- package/src/moc/moc3-layout.ts +534 -0
- package/src/moc/moc3-parts.ts +45 -0
- package/src/moc/moc3-reader.ts +215 -0
- package/src/moc/moc3-to-program.ts +305 -0
- package/src/moc/moc3.ts +222 -0
- package/src/model-runtime.ts +73 -0
- package/src/preview-style.ts +33 -0
- package/src/types.ts +71 -0
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* moc3 deformer bake: warp grids + rotation affines along parent chains.
|
|
3
|
+
*
|
|
4
|
+
* Coordinate contract (observed on official MOC3 / Wanko):
|
|
5
|
+
* - Root rotations author scale ≈ 1/pixelsPerUnit; children stay in pixels.
|
|
6
|
+
* - Nested rotation under rotation: mulAffine; origins/children share parent units.
|
|
7
|
+
* - Rotation under warp: origin is warp UV; fold 1/ppu into the local linear
|
|
8
|
+
* part before Jacobian composition so pixel children map UV → logical.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { warpPointsByMesh } from "./moc2-deform.js";
|
|
12
|
+
import {
|
|
13
|
+
blendKeyformFloats,
|
|
14
|
+
blendKeyformScalar,
|
|
15
|
+
type Moc3KeyTables,
|
|
16
|
+
resolveMoc3KeyformBlend,
|
|
17
|
+
} from "./moc3-keyforms.js";
|
|
18
|
+
import { CountIdx } from "./moc3-layout.js";
|
|
19
|
+
import type { Moc3Document } from "./moc3-reader.js";
|
|
20
|
+
import { moc3SectionF32, moc3SectionI32 } from "./moc3-reader.js";
|
|
21
|
+
|
|
22
|
+
const DEG2RAD = Math.PI / 180;
|
|
23
|
+
const WARP = 0;
|
|
24
|
+
const ROTATION = 1;
|
|
25
|
+
|
|
26
|
+
export interface Moc3Affine {
|
|
27
|
+
originX: number;
|
|
28
|
+
originY: number;
|
|
29
|
+
/** Column 0 of linear part. */
|
|
30
|
+
m00: number;
|
|
31
|
+
m10: number;
|
|
32
|
+
/** Column 1 of linear part. */
|
|
33
|
+
m01: number;
|
|
34
|
+
m11: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type Moc3DeformerWorld =
|
|
38
|
+
| { kind: "warp"; grid: Float32Array; rows: number; cols: number }
|
|
39
|
+
| { kind: "rotation"; affine: Moc3Affine };
|
|
40
|
+
|
|
41
|
+
function mulAffine(parent: Moc3Affine, local: Moc3Affine): Moc3Affine {
|
|
42
|
+
return {
|
|
43
|
+
originX:
|
|
44
|
+
parent.originX +
|
|
45
|
+
parent.m00 * local.originX +
|
|
46
|
+
parent.m01 * local.originY,
|
|
47
|
+
originY:
|
|
48
|
+
parent.originY +
|
|
49
|
+
parent.m10 * local.originX +
|
|
50
|
+
parent.m11 * local.originY,
|
|
51
|
+
m00: parent.m00 * local.m00 + parent.m01 * local.m10,
|
|
52
|
+
m10: parent.m10 * local.m00 + parent.m11 * local.m10,
|
|
53
|
+
m01: parent.m00 * local.m01 + parent.m01 * local.m11,
|
|
54
|
+
m11: parent.m10 * local.m01 + parent.m11 * local.m11,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function affineFromRotationKeyform(
|
|
59
|
+
baseAngle: number,
|
|
60
|
+
angle: number,
|
|
61
|
+
originX: number,
|
|
62
|
+
originY: number,
|
|
63
|
+
scale: number,
|
|
64
|
+
reflectX: boolean,
|
|
65
|
+
reflectY: boolean,
|
|
66
|
+
): Moc3Affine {
|
|
67
|
+
const rad = (baseAngle + angle) * DEG2RAD;
|
|
68
|
+
const c = Math.cos(rad);
|
|
69
|
+
const s = Math.sin(rad);
|
|
70
|
+
const sx = reflectX ? -scale : scale;
|
|
71
|
+
const sy = reflectY ? -scale : scale;
|
|
72
|
+
return {
|
|
73
|
+
originX,
|
|
74
|
+
originY,
|
|
75
|
+
m00: c * sx,
|
|
76
|
+
m10: s * sx,
|
|
77
|
+
m01: -s * sy,
|
|
78
|
+
m11: c * sy,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function applyAffineToPoints(src: Float32Array, a: Moc3Affine): Float32Array {
|
|
83
|
+
// Root rotations author scale ≈ 1/ppu so pixel-sized children land in
|
|
84
|
+
// logical units; nested rotations with scale≈1 keep parent-local units.
|
|
85
|
+
// No magnitude heuristic — trust the authored scale.
|
|
86
|
+
const out = new Float32Array(src.length);
|
|
87
|
+
for (let i = 0; i + 1 < src.length; i += 2) {
|
|
88
|
+
const x = src[i]!;
|
|
89
|
+
const y = src[i + 1]!;
|
|
90
|
+
out[i] = a.originX + a.m00 * x + a.m01 * y;
|
|
91
|
+
out[i + 1] = a.originY + a.m10 * x + a.m11 * y;
|
|
92
|
+
}
|
|
93
|
+
return out;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Transform local points through a parent deformer's world state. */
|
|
97
|
+
export function applyParentToPoints(
|
|
98
|
+
local: Float32Array,
|
|
99
|
+
parent: Moc3DeformerWorld,
|
|
100
|
+
): Float32Array {
|
|
101
|
+
if (parent.kind === "warp") {
|
|
102
|
+
return warpPointsByMesh(local, parent.grid, parent.rows, parent.cols);
|
|
103
|
+
}
|
|
104
|
+
return applyAffineToPoints(local, parent.affine);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
interface DeformerTables {
|
|
108
|
+
types: Int32Array;
|
|
109
|
+
specific: Int32Array;
|
|
110
|
+
parents: Int32Array;
|
|
111
|
+
bandIndex: Int32Array;
|
|
112
|
+
enables: Int32Array;
|
|
113
|
+
warpBand: Int32Array;
|
|
114
|
+
warpKfBegin: Int32Array;
|
|
115
|
+
warpKfCount: Int32Array;
|
|
116
|
+
warpVertexCounts: Int32Array;
|
|
117
|
+
warpRows: Int32Array;
|
|
118
|
+
warpCols: Int32Array;
|
|
119
|
+
warpPosBegins: Int32Array;
|
|
120
|
+
rotBand: Int32Array;
|
|
121
|
+
rotKfBegin: Int32Array;
|
|
122
|
+
rotKfCount: Int32Array;
|
|
123
|
+
rotBaseAngles: Float32Array;
|
|
124
|
+
rotAngles: Float32Array;
|
|
125
|
+
rotOriginX: Float32Array;
|
|
126
|
+
rotOriginY: Float32Array;
|
|
127
|
+
rotScales: Float32Array;
|
|
128
|
+
rotReflectX: Int32Array;
|
|
129
|
+
rotReflectY: Int32Array;
|
|
130
|
+
positions: Float32Array;
|
|
131
|
+
keyTables: Moc3KeyTables;
|
|
132
|
+
deformerCount: number;
|
|
133
|
+
pixelsPerUnit: number;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function loadDeformerTables(
|
|
137
|
+
doc: Moc3Document,
|
|
138
|
+
keyTables: Moc3KeyTables,
|
|
139
|
+
): DeformerTables {
|
|
140
|
+
return {
|
|
141
|
+
types: moc3SectionI32(doc, "deformer.types"),
|
|
142
|
+
specific: moc3SectionI32(doc, "deformer.specific_indices"),
|
|
143
|
+
parents: moc3SectionI32(doc, "deformer.parent_deformer_indices"),
|
|
144
|
+
bandIndex: moc3SectionI32(doc, "deformer.keyform_binding_band_indices"),
|
|
145
|
+
enables: moc3SectionI32(doc, "deformer.enables"),
|
|
146
|
+
warpBand: moc3SectionI32(
|
|
147
|
+
doc,
|
|
148
|
+
"warp_deformer.keyform_binding_band_indices",
|
|
149
|
+
),
|
|
150
|
+
warpKfBegin: moc3SectionI32(doc, "warp_deformer.keyform_begin_indices"),
|
|
151
|
+
warpKfCount: moc3SectionI32(doc, "warp_deformer.keyform_counts"),
|
|
152
|
+
warpVertexCounts: moc3SectionI32(doc, "warp_deformer.vertex_counts"),
|
|
153
|
+
warpRows: moc3SectionI32(doc, "warp_deformer.rows"),
|
|
154
|
+
warpCols: moc3SectionI32(doc, "warp_deformer.cols"),
|
|
155
|
+
warpPosBegins: moc3SectionI32(
|
|
156
|
+
doc,
|
|
157
|
+
"warp_deformer_keyform.keyform_position_begin_indices",
|
|
158
|
+
),
|
|
159
|
+
rotBand: moc3SectionI32(
|
|
160
|
+
doc,
|
|
161
|
+
"rotation_deformer.keyform_binding_band_indices",
|
|
162
|
+
),
|
|
163
|
+
rotKfBegin: moc3SectionI32(
|
|
164
|
+
doc,
|
|
165
|
+
"rotation_deformer.keyform_begin_indices",
|
|
166
|
+
),
|
|
167
|
+
rotKfCount: moc3SectionI32(doc, "rotation_deformer.keyform_counts"),
|
|
168
|
+
rotBaseAngles: moc3SectionF32(doc, "rotation_deformer.base_angles"),
|
|
169
|
+
rotAngles: moc3SectionF32(doc, "rotation_deformer_keyform.angles"),
|
|
170
|
+
rotOriginX: moc3SectionF32(doc, "rotation_deformer_keyform.origin_xs"),
|
|
171
|
+
rotOriginY: moc3SectionF32(doc, "rotation_deformer_keyform.origin_ys"),
|
|
172
|
+
rotScales: moc3SectionF32(doc, "rotation_deformer_keyform.scales"),
|
|
173
|
+
rotReflectX: moc3SectionI32(
|
|
174
|
+
doc,
|
|
175
|
+
"rotation_deformer_keyform.reflect_xs",
|
|
176
|
+
),
|
|
177
|
+
rotReflectY: moc3SectionI32(
|
|
178
|
+
doc,
|
|
179
|
+
"rotation_deformer_keyform.reflect_ys",
|
|
180
|
+
),
|
|
181
|
+
positions: moc3SectionF32(doc, "keyform_position.xys"),
|
|
182
|
+
keyTables,
|
|
183
|
+
deformerCount: doc.counts[CountIdx.DEFORMERS] ?? 0,
|
|
184
|
+
pixelsPerUnit: doc.canvas.pixelsPerUnit,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function evalDeformer(
|
|
189
|
+
index: number,
|
|
190
|
+
tables: DeformerTables,
|
|
191
|
+
getParamByIndex: (i: number) => number,
|
|
192
|
+
cache: (Moc3DeformerWorld | null)[],
|
|
193
|
+
): Moc3DeformerWorld | null {
|
|
194
|
+
if (index < 0 || index >= tables.deformerCount) return null;
|
|
195
|
+
const hit = cache[index];
|
|
196
|
+
if (hit) return hit;
|
|
197
|
+
if ((tables.enables[index] ?? 1) === 0) {
|
|
198
|
+
cache[index] = null;
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const type = tables.types[index] ?? WARP;
|
|
203
|
+
const specific = tables.specific[index] ?? 0;
|
|
204
|
+
const parentIndex = tables.parents[index] ?? -1;
|
|
205
|
+
const parentWorld =
|
|
206
|
+
parentIndex >= 0
|
|
207
|
+
? evalDeformer(parentIndex, tables, getParamByIndex, cache)
|
|
208
|
+
: null;
|
|
209
|
+
|
|
210
|
+
let world: Moc3DeformerWorld | null = null;
|
|
211
|
+
|
|
212
|
+
if (type === ROTATION) {
|
|
213
|
+
const band = tables.rotBand[specific] ?? -1;
|
|
214
|
+
const kfBegin = tables.rotKfBegin[specific] ?? 0;
|
|
215
|
+
const kfCount = tables.rotKfCount[specific] ?? 0;
|
|
216
|
+
const blend = resolveMoc3KeyformBlend(
|
|
217
|
+
tables.keyTables,
|
|
218
|
+
band,
|
|
219
|
+
getParamByIndex,
|
|
220
|
+
);
|
|
221
|
+
const angle = blendKeyformScalar(
|
|
222
|
+
tables.rotAngles,
|
|
223
|
+
kfBegin,
|
|
224
|
+
kfCount,
|
|
225
|
+
blend,
|
|
226
|
+
0,
|
|
227
|
+
);
|
|
228
|
+
const ox = blendKeyformScalar(
|
|
229
|
+
tables.rotOriginX,
|
|
230
|
+
kfBegin,
|
|
231
|
+
kfCount,
|
|
232
|
+
blend,
|
|
233
|
+
0,
|
|
234
|
+
);
|
|
235
|
+
const oy = blendKeyformScalar(
|
|
236
|
+
tables.rotOriginY,
|
|
237
|
+
kfBegin,
|
|
238
|
+
kfCount,
|
|
239
|
+
blend,
|
|
240
|
+
0,
|
|
241
|
+
);
|
|
242
|
+
const scale = blendKeyformScalar(
|
|
243
|
+
tables.rotScales,
|
|
244
|
+
kfBegin,
|
|
245
|
+
kfCount,
|
|
246
|
+
blend,
|
|
247
|
+
1,
|
|
248
|
+
);
|
|
249
|
+
const reflectX =
|
|
250
|
+
blendKeyformScalar(
|
|
251
|
+
tables.rotReflectX,
|
|
252
|
+
kfBegin,
|
|
253
|
+
kfCount,
|
|
254
|
+
blend,
|
|
255
|
+
0,
|
|
256
|
+
) >= 0.5;
|
|
257
|
+
const reflectY =
|
|
258
|
+
blendKeyformScalar(
|
|
259
|
+
tables.rotReflectY,
|
|
260
|
+
kfBegin,
|
|
261
|
+
kfCount,
|
|
262
|
+
blend,
|
|
263
|
+
0,
|
|
264
|
+
) >= 0.5;
|
|
265
|
+
|
|
266
|
+
// Origins stay in the same units as sibling children. Root rotations
|
|
267
|
+
// use scale≈1/ppu so pixel origins compose correctly via mulAffine;
|
|
268
|
+
// do not /ppu here (that double-scales under small parent scale).
|
|
269
|
+
const local = affineFromRotationKeyform(
|
|
270
|
+
tables.rotBaseAngles[specific] ?? 0,
|
|
271
|
+
angle,
|
|
272
|
+
ox,
|
|
273
|
+
oy,
|
|
274
|
+
scale,
|
|
275
|
+
reflectX,
|
|
276
|
+
reflectY,
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
if (!parentWorld) {
|
|
280
|
+
world = { kind: "rotation", affine: local };
|
|
281
|
+
} else if (parentWorld.kind === "rotation") {
|
|
282
|
+
world = {
|
|
283
|
+
kind: "rotation",
|
|
284
|
+
affine: mulAffine(parentWorld.affine, local),
|
|
285
|
+
};
|
|
286
|
+
} else {
|
|
287
|
+
// Parent warp: rotation origin is in warp UV [0,1], but children
|
|
288
|
+
// (and nested rotation origins) are authored in pixels — same as
|
|
289
|
+
// root rotations that bake scale≈1/ppu. Fold 1/ppu into the local
|
|
290
|
+
// linear part before Jacobian composition so pixel children land
|
|
291
|
+
// in UV, then J maps UV → logical.
|
|
292
|
+
const ppu = tables.pixelsPerUnit > 0 ? tables.pixelsPerUnit : 1;
|
|
293
|
+
const alreadyPixelScale = Math.hypot(local.m00, local.m11) < 0.05;
|
|
294
|
+
const s = alreadyPixelScale ? 1 : 1 / ppu;
|
|
295
|
+
const localPx: Moc3Affine = {
|
|
296
|
+
originX: local.originX,
|
|
297
|
+
originY: local.originY,
|
|
298
|
+
m00: local.m00 * s,
|
|
299
|
+
m10: local.m10 * s,
|
|
300
|
+
m01: local.m01 * s,
|
|
301
|
+
m11: local.m11 * s,
|
|
302
|
+
};
|
|
303
|
+
const eps = 1 / 64;
|
|
304
|
+
const probe = new Float32Array([
|
|
305
|
+
localPx.originX,
|
|
306
|
+
localPx.originY,
|
|
307
|
+
localPx.originX + eps,
|
|
308
|
+
localPx.originY,
|
|
309
|
+
localPx.originX,
|
|
310
|
+
localPx.originY + eps,
|
|
311
|
+
]);
|
|
312
|
+
const warped = applyParentToPoints(probe, parentWorld);
|
|
313
|
+
const j00 = (warped[2]! - warped[0]!) / eps;
|
|
314
|
+
const j10 = (warped[3]! - warped[1]!) / eps;
|
|
315
|
+
const j01 = (warped[4]! - warped[0]!) / eps;
|
|
316
|
+
const j11 = (warped[5]! - warped[1]!) / eps;
|
|
317
|
+
world = {
|
|
318
|
+
kind: "rotation",
|
|
319
|
+
affine: {
|
|
320
|
+
originX: warped[0]!,
|
|
321
|
+
originY: warped[1]!,
|
|
322
|
+
m00: j00 * localPx.m00 + j01 * localPx.m10,
|
|
323
|
+
m10: j10 * localPx.m00 + j11 * localPx.m10,
|
|
324
|
+
m01: j00 * localPx.m01 + j01 * localPx.m11,
|
|
325
|
+
m11: j10 * localPx.m01 + j11 * localPx.m11,
|
|
326
|
+
},
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
} else {
|
|
330
|
+
const band = tables.warpBand[specific] ?? -1;
|
|
331
|
+
const kfBegin = tables.warpKfBegin[specific] ?? 0;
|
|
332
|
+
const kfCount = tables.warpKfCount[specific] ?? 0;
|
|
333
|
+
const vertexCount = tables.warpVertexCounts[specific] ?? 0;
|
|
334
|
+
const rows = tables.warpRows[specific] ?? 0;
|
|
335
|
+
const cols = tables.warpCols[specific] ?? 0;
|
|
336
|
+
const blend = resolveMoc3KeyformBlend(
|
|
337
|
+
tables.keyTables,
|
|
338
|
+
band,
|
|
339
|
+
getParamByIndex,
|
|
340
|
+
);
|
|
341
|
+
let grid = blendKeyformFloats(
|
|
342
|
+
tables.positions,
|
|
343
|
+
tables.warpPosBegins,
|
|
344
|
+
kfBegin,
|
|
345
|
+
kfCount,
|
|
346
|
+
vertexCount * 2,
|
|
347
|
+
blend,
|
|
348
|
+
);
|
|
349
|
+
if (parentWorld) {
|
|
350
|
+
grid = applyParentToPoints(grid, parentWorld);
|
|
351
|
+
}
|
|
352
|
+
// moc2-style: rows/cols are cell counts → (rows+1)*(cols+1) points.
|
|
353
|
+
world = { kind: "warp", grid, rows, cols };
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
cache[index] = world;
|
|
357
|
+
return world;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/** Bake all deformers to world space at the given parameters. */
|
|
361
|
+
export function bakeMoc3Deformers(
|
|
362
|
+
doc: Moc3Document,
|
|
363
|
+
keyTables: Moc3KeyTables,
|
|
364
|
+
getParamByIndex: (i: number) => number,
|
|
365
|
+
): (Moc3DeformerWorld | null)[] {
|
|
366
|
+
const tables = loadDeformerTables(doc, keyTables);
|
|
367
|
+
const cache: (Moc3DeformerWorld | null)[] = new Array(
|
|
368
|
+
tables.deformerCount,
|
|
369
|
+
).fill(null);
|
|
370
|
+
for (let i = 0; i < tables.deformerCount; i++) {
|
|
371
|
+
evalDeformer(i, tables, getParamByIndex, cache);
|
|
372
|
+
}
|
|
373
|
+
return cache;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
export function loadMoc3KeyTables(doc: Moc3Document): Moc3KeyTables {
|
|
377
|
+
return {
|
|
378
|
+
bindingIndex: moc3SectionI32(doc, "keyform_binding_index.indices"),
|
|
379
|
+
bandBegin: moc3SectionI32(doc, "keyform_binding_band.begin_indices"),
|
|
380
|
+
bandCount: moc3SectionI32(doc, "keyform_binding_band.counts"),
|
|
381
|
+
keysBegin: moc3SectionI32(doc, "keyform_binding.keys_begin_indices"),
|
|
382
|
+
keysCount: moc3SectionI32(doc, "keyform_binding.keys_counts"),
|
|
383
|
+
keys: moc3SectionF32(doc, "keys.values"),
|
|
384
|
+
paramBindingBegin: moc3SectionI32(
|
|
385
|
+
doc,
|
|
386
|
+
"parameter.keyform_binding_begin_indices",
|
|
387
|
+
),
|
|
388
|
+
paramBindingCount: moc3SectionI32(
|
|
389
|
+
doc,
|
|
390
|
+
"parameter.keyform_binding_counts",
|
|
391
|
+
),
|
|
392
|
+
paramCount: doc.counts[CountIdx.PARAMETERS] ?? 0,
|
|
393
|
+
};
|
|
394
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* moc3 Glue: pull paired vertices on art meshes A/B together.
|
|
3
|
+
*
|
|
4
|
+
* Layout (observed on Mao): `glue_info.position_indices` and `.weights` are
|
|
5
|
+
* interleaved pairs `(idxA, idxB)` / `(weightA, weightB)` with weightA+weightB≈1.
|
|
6
|
+
* Keyform `intensities` are Cubism "compatibility" (0..1).
|
|
7
|
+
*
|
|
8
|
+
* target = wA·posA + wB·posB
|
|
9
|
+
* posA' = lerp(posA, target, intensity)
|
|
10
|
+
* posB' = lerp(posB, target, intensity)
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
blendKeyformScalar,
|
|
15
|
+
type Moc3KeyTables,
|
|
16
|
+
resolveMoc3KeyformBlend,
|
|
17
|
+
} from "./moc3-keyforms.js";
|
|
18
|
+
import { CountIdx } from "./moc3-layout.js";
|
|
19
|
+
import {
|
|
20
|
+
type Moc3Document,
|
|
21
|
+
moc3SectionF32,
|
|
22
|
+
moc3SectionI16,
|
|
23
|
+
moc3SectionI32,
|
|
24
|
+
} from "./moc3-reader.js";
|
|
25
|
+
|
|
26
|
+
export interface Moc3GluePair {
|
|
27
|
+
readonly indexA: number;
|
|
28
|
+
readonly indexB: number;
|
|
29
|
+
readonly weightA: number;
|
|
30
|
+
readonly weightB: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface Moc3GlueDef {
|
|
34
|
+
readonly meshA: number;
|
|
35
|
+
readonly meshB: number;
|
|
36
|
+
readonly pairs: readonly Moc3GluePair[];
|
|
37
|
+
readonly band: number;
|
|
38
|
+
readonly keyformBegin: number;
|
|
39
|
+
readonly keyformCount: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Parse glue tables from a MOC3 document (empty when model has no glues). */
|
|
43
|
+
export function loadMoc3Glues(doc: Moc3Document): Moc3GlueDef[] {
|
|
44
|
+
const glueCount = doc.counts[CountIdx.GLUES] ?? 0;
|
|
45
|
+
if (glueCount <= 0) return [];
|
|
46
|
+
|
|
47
|
+
const meshAs = moc3SectionI32(doc, "glue.art_mesh_index_as");
|
|
48
|
+
const meshBs = moc3SectionI32(doc, "glue.art_mesh_index_bs");
|
|
49
|
+
const infoBegins = moc3SectionI32(doc, "glue.info_begin_indices");
|
|
50
|
+
const infoCounts = moc3SectionI32(doc, "glue.info_counts");
|
|
51
|
+
const bands = moc3SectionI32(doc, "glue.keyform_binding_band_indices");
|
|
52
|
+
const kfBegins = moc3SectionI32(doc, "glue.keyform_begin_indices");
|
|
53
|
+
const kfCounts = moc3SectionI32(doc, "glue.keyform_counts");
|
|
54
|
+
const weights = moc3SectionF32(doc, "glue_info.weights");
|
|
55
|
+
const posIdx = moc3SectionI16(doc, "glue_info.position_indices");
|
|
56
|
+
|
|
57
|
+
const out: Moc3GlueDef[] = [];
|
|
58
|
+
for (let g = 0; g < glueCount; g++) {
|
|
59
|
+
const begin = infoBegins[g] ?? 0;
|
|
60
|
+
const count = infoCounts[g] ?? 0;
|
|
61
|
+
const pairs: Moc3GluePair[] = [];
|
|
62
|
+
for (let i = 0; i + 1 < count; i += 2) {
|
|
63
|
+
pairs.push({
|
|
64
|
+
indexA: posIdx[begin + i] ?? 0,
|
|
65
|
+
indexB: posIdx[begin + i + 1] ?? 0,
|
|
66
|
+
weightA: weights[begin + i] ?? 0.5,
|
|
67
|
+
weightB: weights[begin + i + 1] ?? 0.5,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
out.push({
|
|
71
|
+
meshA: meshAs[g] ?? -1,
|
|
72
|
+
meshB: meshBs[g] ?? -1,
|
|
73
|
+
pairs,
|
|
74
|
+
band: bands[g] ?? -1,
|
|
75
|
+
keyformBegin: kfBegins[g] ?? 0,
|
|
76
|
+
keyformCount: kfCounts[g] ?? 0,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
return out;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Apply all glues in-place to world-space per-mesh position buffers
|
|
84
|
+
* (length = vertexCount * 2). Missing meshes are skipped.
|
|
85
|
+
*/
|
|
86
|
+
export function applyMoc3Glues(
|
|
87
|
+
positionsByMesh: Map<number, Float32Array>,
|
|
88
|
+
glues: readonly Moc3GlueDef[],
|
|
89
|
+
keyTables: Moc3KeyTables,
|
|
90
|
+
getParamByIndex: (index: number) => number,
|
|
91
|
+
intensities: Float32Array,
|
|
92
|
+
): void {
|
|
93
|
+
for (const glue of glues) {
|
|
94
|
+
const posA = positionsByMesh.get(glue.meshA);
|
|
95
|
+
const posB = positionsByMesh.get(glue.meshB);
|
|
96
|
+
if (!posA || !posB) continue;
|
|
97
|
+
|
|
98
|
+
const blend = resolveMoc3KeyformBlend(
|
|
99
|
+
keyTables,
|
|
100
|
+
glue.band,
|
|
101
|
+
getParamByIndex,
|
|
102
|
+
);
|
|
103
|
+
const intensity = blendKeyformScalar(
|
|
104
|
+
intensities,
|
|
105
|
+
glue.keyformBegin,
|
|
106
|
+
glue.keyformCount,
|
|
107
|
+
blend,
|
|
108
|
+
1,
|
|
109
|
+
);
|
|
110
|
+
if (intensity <= 0) continue;
|
|
111
|
+
|
|
112
|
+
for (const p of glue.pairs) {
|
|
113
|
+
const oa = p.indexA * 2;
|
|
114
|
+
const ob = p.indexB * 2;
|
|
115
|
+
if (oa + 1 >= posA.length || ob + 1 >= posB.length) continue;
|
|
116
|
+
|
|
117
|
+
const ax = posA[oa]!;
|
|
118
|
+
const ay = posA[oa + 1]!;
|
|
119
|
+
const bx = posB[ob]!;
|
|
120
|
+
const by = posB[ob + 1]!;
|
|
121
|
+
const tx = p.weightA * ax + p.weightB * bx;
|
|
122
|
+
const ty = p.weightA * ay + p.weightB * by;
|
|
123
|
+
posA[oa] = ax + (tx - ax) * intensity;
|
|
124
|
+
posA[oa + 1] = ay + (ty - ay) * intensity;
|
|
125
|
+
posB[ob] = bx + (tx - bx) * intensity;
|
|
126
|
+
posB[ob + 1] = by + (ty - by) * intensity;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Mean Euclidean distance between glued vertex pairs (for tests). */
|
|
132
|
+
export function meanGlueSeamDistance(
|
|
133
|
+
positionsByMesh: ReadonlyMap<number, Float32Array>,
|
|
134
|
+
glue: Moc3GlueDef,
|
|
135
|
+
): number {
|
|
136
|
+
const posA = positionsByMesh.get(glue.meshA);
|
|
137
|
+
const posB = positionsByMesh.get(glue.meshB);
|
|
138
|
+
if (!posA || !posB || glue.pairs.length === 0) return 0;
|
|
139
|
+
let sum = 0;
|
|
140
|
+
let n = 0;
|
|
141
|
+
for (const p of glue.pairs) {
|
|
142
|
+
const oa = p.indexA * 2;
|
|
143
|
+
const ob = p.indexB * 2;
|
|
144
|
+
if (oa + 1 >= posA.length || ob + 1 >= posB.length) continue;
|
|
145
|
+
const dx = posA[oa]! - posB[ob]!;
|
|
146
|
+
const dy = posA[oa + 1]! - posB[ob + 1]!;
|
|
147
|
+
sum += Math.hypot(dx, dy);
|
|
148
|
+
n++;
|
|
149
|
+
}
|
|
150
|
+
return n > 0 ? sum / n : 0;
|
|
151
|
+
}
|