@doki-land/live2d-renderer 0.0.18 → 0.0.19
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/dist/index.d.ts +12 -2
- package/dist/index.js +510 -136
- package/package.json +2 -2
- package/src/cpu/evaluate.ts +63 -4
- package/src/moc/moc2-to-program.ts +89 -3
- package/src/moc/moc2.ts +151 -75
- package/src/moc/moc3-to-program.ts +193 -3
- package/src/moc/moc3.ts +160 -81
- package/src/runtime/model-runtime.ts +6 -0
- package/src/types.ts +1 -1
|
@@ -32,23 +32,39 @@ import {
|
|
|
32
32
|
moc3SectionStrings,
|
|
33
33
|
} from "./moc3-reader.js";
|
|
34
34
|
|
|
35
|
-
function
|
|
35
|
+
function normalizePositionsInto(
|
|
36
36
|
positions: Float32Array,
|
|
37
37
|
canvasWidth: number,
|
|
38
38
|
canvasHeight: number,
|
|
39
39
|
pixelsPerUnit: number,
|
|
40
|
-
|
|
40
|
+
out: Float32Array,
|
|
41
|
+
): void {
|
|
41
42
|
// Deformer output uses logical canvas/ppu units around the model center.
|
|
42
43
|
// Convert the decoded authoring orientation once into the renderer's shared
|
|
43
44
|
// Y-up NDC contract, matching the moc2 path after top-left normalization.
|
|
44
45
|
const ppu = pixelsPerUnit > 0 ? pixelsPerUnit : 1;
|
|
45
46
|
const hw = canvasWidth > 0 ? (canvasWidth / ppu) * 0.5 : 0.5;
|
|
46
47
|
const hh = canvasHeight > 0 ? (canvasHeight / ppu) * 0.5 : 0.5;
|
|
47
|
-
const out = new Float32Array(positions.length);
|
|
48
48
|
for (let i = 0; i + 1 < positions.length; i += 2) {
|
|
49
49
|
out[i] = positions[i]! / hw;
|
|
50
50
|
out[i + 1] = -positions[i + 1]! / hh;
|
|
51
51
|
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function normalizePositions(
|
|
55
|
+
positions: Float32Array,
|
|
56
|
+
canvasWidth: number,
|
|
57
|
+
canvasHeight: number,
|
|
58
|
+
pixelsPerUnit: number,
|
|
59
|
+
): Float32Array {
|
|
60
|
+
const out = new Float32Array(positions.length);
|
|
61
|
+
normalizePositionsInto(
|
|
62
|
+
positions,
|
|
63
|
+
canvasWidth,
|
|
64
|
+
canvasHeight,
|
|
65
|
+
pixelsPerUnit,
|
|
66
|
+
out,
|
|
67
|
+
);
|
|
52
68
|
return out;
|
|
53
69
|
}
|
|
54
70
|
|
|
@@ -301,3 +317,177 @@ export function moc3DocumentToProgram(
|
|
|
301
317
|
drawables,
|
|
302
318
|
};
|
|
303
319
|
}
|
|
320
|
+
|
|
321
|
+
/** Art-mesh indices in the same order {@link moc3DocumentToProgram} assigns drawable indices. */
|
|
322
|
+
export function moc3ArtMeshIndicesInProgramOrder(
|
|
323
|
+
doc: Moc3Document,
|
|
324
|
+
getParamByIndex?: (index: number) => number,
|
|
325
|
+
): number[] {
|
|
326
|
+
const meshCount = doc.counts[CountIdx.ART_MESHES] ?? 0;
|
|
327
|
+
const defaultValues = moc3SectionF32(doc, "parameter.default_values");
|
|
328
|
+
const gp =
|
|
329
|
+
getParamByIndex ?? ((index: number) => defaultValues[index] ?? 0);
|
|
330
|
+
const keyTables = loadMoc3KeyTables(doc);
|
|
331
|
+
const enables = moc3SectionI32(doc, "art_mesh.enables");
|
|
332
|
+
const vertexCounts = moc3SectionI32(doc, "art_mesh.vertex_counts");
|
|
333
|
+
const indexCounts = moc3SectionI32(doc, "art_mesh.position_index_counts");
|
|
334
|
+
const keyformBegins = moc3SectionI32(doc, "art_mesh.keyform_begin_indices");
|
|
335
|
+
const keyformCounts = moc3SectionI32(doc, "art_mesh.keyform_counts");
|
|
336
|
+
const bandIndices = moc3SectionI32(
|
|
337
|
+
doc,
|
|
338
|
+
"art_mesh.keyform_binding_band_indices",
|
|
339
|
+
);
|
|
340
|
+
const keyformDrawOrders = moc3SectionF32(
|
|
341
|
+
doc,
|
|
342
|
+
"art_mesh_keyform.draw_orders",
|
|
343
|
+
);
|
|
344
|
+
|
|
345
|
+
const drafts: { artMeshIndex: number; renderOrder: number }[] = [];
|
|
346
|
+
for (let i = 0; i < meshCount; i++) {
|
|
347
|
+
if ((enables[i] ?? 1) === 0) continue;
|
|
348
|
+
const vertexCount = vertexCounts[i] ?? 0;
|
|
349
|
+
const indexCount = indexCounts[i] ?? 0;
|
|
350
|
+
if (vertexCount <= 0 || indexCount < 3) continue;
|
|
351
|
+
const kfBegin = keyformBegins[i] ?? 0;
|
|
352
|
+
const kfCount = keyformCounts[i] ?? 0;
|
|
353
|
+
if (kfCount <= 0) continue;
|
|
354
|
+
const band = bandIndices[i] ?? -1;
|
|
355
|
+
const blend = resolveMoc3KeyformBlend(keyTables, band, gp);
|
|
356
|
+
drafts.push({
|
|
357
|
+
artMeshIndex: i,
|
|
358
|
+
renderOrder: Math.round(
|
|
359
|
+
blendKeyformScalar(
|
|
360
|
+
keyformDrawOrders,
|
|
361
|
+
kfBegin,
|
|
362
|
+
kfCount,
|
|
363
|
+
blend,
|
|
364
|
+
i,
|
|
365
|
+
),
|
|
366
|
+
),
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
drafts.sort((a, b) => a.renderOrder - b.renderOrder);
|
|
370
|
+
return drafts.map((d) => d.artMeshIndex);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Evaluate moc3 keyforms/deformers/glue into resident meshes keyed by art-mesh index.
|
|
375
|
+
* Topology buffers (`uvs`/`indices`/`maskIndices`) stay shared; positions are written in place.
|
|
376
|
+
*/
|
|
377
|
+
export function evaluateMoc3PoseInto(
|
|
378
|
+
doc: Moc3Document,
|
|
379
|
+
getParamByIndex: (index: number) => number,
|
|
380
|
+
byArtMesh: ReadonlyMap<number, import("../types.js").DrawableMesh>,
|
|
381
|
+
poseOpacity: Float32Array,
|
|
382
|
+
): void {
|
|
383
|
+
const meshCount = doc.counts[CountIdx.ART_MESHES] ?? 0;
|
|
384
|
+
const keyTables = loadMoc3KeyTables(doc);
|
|
385
|
+
const deformers = bakeMoc3Deformers(doc, keyTables, getParamByIndex);
|
|
386
|
+
const glues = loadMoc3Glues(doc);
|
|
387
|
+
const glueIntensities = moc3SectionF32(doc, "glue_keyform.intensities");
|
|
388
|
+
|
|
389
|
+
const visibles = moc3SectionI32(doc, "art_mesh.visibles");
|
|
390
|
+
const enables = moc3SectionI32(doc, "art_mesh.enables");
|
|
391
|
+
const vertexCounts = moc3SectionI32(doc, "art_mesh.vertex_counts");
|
|
392
|
+
const indexCounts = moc3SectionI32(doc, "art_mesh.position_index_counts");
|
|
393
|
+
const keyformBegins = moc3SectionI32(doc, "art_mesh.keyform_begin_indices");
|
|
394
|
+
const keyformCounts = moc3SectionI32(doc, "art_mesh.keyform_counts");
|
|
395
|
+
const bandIndices = moc3SectionI32(
|
|
396
|
+
doc,
|
|
397
|
+
"art_mesh.keyform_binding_band_indices",
|
|
398
|
+
);
|
|
399
|
+
const parentDeformers = moc3SectionI32(
|
|
400
|
+
doc,
|
|
401
|
+
"art_mesh.parent_deformer_indices",
|
|
402
|
+
);
|
|
403
|
+
const keyformOpacities = moc3SectionF32(doc, "art_mesh_keyform.opacities");
|
|
404
|
+
const keyformDrawOrders = moc3SectionF32(
|
|
405
|
+
doc,
|
|
406
|
+
"art_mesh_keyform.draw_orders",
|
|
407
|
+
);
|
|
408
|
+
const keyformPosBegins = moc3SectionI32(
|
|
409
|
+
doc,
|
|
410
|
+
"art_mesh_keyform.keyform_position_begin_indices",
|
|
411
|
+
);
|
|
412
|
+
const keyformPositions = moc3SectionF32(doc, "keyform_position.xys");
|
|
413
|
+
|
|
414
|
+
const cw = doc.canvas.canvasWidth;
|
|
415
|
+
const ch = doc.canvas.canvasHeight;
|
|
416
|
+
const ppu = doc.canvas.pixelsPerUnit;
|
|
417
|
+
|
|
418
|
+
const worldByMesh = new Map<number, Float32Array>();
|
|
419
|
+
const opacityByMesh = new Map<number, number>();
|
|
420
|
+
const orderByMesh = new Map<number, number>();
|
|
421
|
+
const visibleByMesh = new Map<number, boolean>();
|
|
422
|
+
|
|
423
|
+
for (let i = 0; i < meshCount; i++) {
|
|
424
|
+
if (!byArtMesh.has(i)) continue;
|
|
425
|
+
if ((enables[i] ?? 1) === 0) continue;
|
|
426
|
+
const vertexCount = vertexCounts[i] ?? 0;
|
|
427
|
+
const indexCount = indexCounts[i] ?? 0;
|
|
428
|
+
if (vertexCount <= 0 || indexCount < 3) continue;
|
|
429
|
+
const kfBegin = keyformBegins[i] ?? 0;
|
|
430
|
+
const kfCount = keyformCounts[i] ?? 0;
|
|
431
|
+
if (kfCount <= 0) continue;
|
|
432
|
+
|
|
433
|
+
const band = bandIndices[i] ?? -1;
|
|
434
|
+
const blend = resolveMoc3KeyformBlend(keyTables, band, getParamByIndex);
|
|
435
|
+
const local = blendKeyformFloats(
|
|
436
|
+
keyformPositions,
|
|
437
|
+
keyformPosBegins,
|
|
438
|
+
kfBegin,
|
|
439
|
+
kfCount,
|
|
440
|
+
vertexCount * 2,
|
|
441
|
+
blend,
|
|
442
|
+
);
|
|
443
|
+
const parentIndex = parentDeformers[i] ?? -1;
|
|
444
|
+
const parent =
|
|
445
|
+
parentIndex >= 0 ? (deformers[parentIndex] ?? null) : null;
|
|
446
|
+
const world = parent ? applyParentToPoints(local, parent) : local;
|
|
447
|
+
worldByMesh.set(i, world);
|
|
448
|
+
opacityByMesh.set(
|
|
449
|
+
i,
|
|
450
|
+
blendKeyformScalar(keyformOpacities, kfBegin, kfCount, blend, 1),
|
|
451
|
+
);
|
|
452
|
+
orderByMesh.set(
|
|
453
|
+
i,
|
|
454
|
+
Math.round(
|
|
455
|
+
blendKeyformScalar(
|
|
456
|
+
keyformDrawOrders,
|
|
457
|
+
kfBegin,
|
|
458
|
+
kfCount,
|
|
459
|
+
blend,
|
|
460
|
+
i,
|
|
461
|
+
),
|
|
462
|
+
),
|
|
463
|
+
);
|
|
464
|
+
visibleByMesh.set(i, (visibles[i] ?? 1) !== 0);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
applyMoc3Glues(
|
|
468
|
+
worldByMesh,
|
|
469
|
+
glues,
|
|
470
|
+
keyTables,
|
|
471
|
+
getParamByIndex,
|
|
472
|
+
glueIntensities,
|
|
473
|
+
);
|
|
474
|
+
|
|
475
|
+
for (const [artMeshIndex, world] of worldByMesh) {
|
|
476
|
+
const sink = byArtMesh.get(artMeshIndex);
|
|
477
|
+
if (!sink) continue;
|
|
478
|
+
let pos = sink.vertexPositions;
|
|
479
|
+
if (pos.length !== world.length) {
|
|
480
|
+
pos = new Float32Array(world.length);
|
|
481
|
+
sink.vertexPositions = pos;
|
|
482
|
+
}
|
|
483
|
+
normalizePositionsInto(world, cw, ch, ppu, pos);
|
|
484
|
+
const opacity = opacityByMesh.get(artMeshIndex) ?? 1;
|
|
485
|
+
sink.renderOrder = orderByMesh.get(artMeshIndex) ?? sink.renderOrder;
|
|
486
|
+
sink.visible = visibleByMesh.get(artMeshIndex) ?? true;
|
|
487
|
+
const slot = sink.index;
|
|
488
|
+
if (slot >= 0 && slot < poseOpacity.length) {
|
|
489
|
+
poseOpacity[slot] = opacity;
|
|
490
|
+
}
|
|
491
|
+
sink.opacity = opacity;
|
|
492
|
+
}
|
|
493
|
+
}
|
package/src/moc/moc3.ts
CHANGED
|
@@ -10,7 +10,7 @@ import { detectModelSettingsFormat } from "@doki-land/live2d-core";
|
|
|
10
10
|
import { isCpuProgramBytes, parseCpuProgram } from "../cpu/cpu-program.js";
|
|
11
11
|
import {
|
|
12
12
|
createModelInstance,
|
|
13
|
-
|
|
13
|
+
evaluateFrameInto,
|
|
14
14
|
setParameterValue,
|
|
15
15
|
} from "../cpu/evaluate.js";
|
|
16
16
|
import type {
|
|
@@ -21,13 +21,34 @@ import type {
|
|
|
21
21
|
import type { BlendMode, DrawableMesh } from "../types.js";
|
|
22
22
|
import { cascadedPartOpacity, readMoc3PartTables } from "./moc3-parts.js";
|
|
23
23
|
import { type Moc3Document, parseMoc3Document } from "./moc3-reader.js";
|
|
24
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
evaluateMoc3PoseInto,
|
|
26
|
+
moc3ArtMeshIndicesInProgramOrder,
|
|
27
|
+
moc3DocumentToProgram,
|
|
28
|
+
} from "./moc3-to-program.js";
|
|
25
29
|
|
|
26
|
-
|
|
27
|
-
|
|
30
|
+
interface Moc3State {
|
|
31
|
+
/** Present for binary MOC3 input; null for cpu-program fixtures. */
|
|
32
|
+
doc: Moc3Document | null;
|
|
33
|
+
instance: ModelInstance;
|
|
34
|
+
meshes: DrawableMesh[];
|
|
35
|
+
drawView: DrawableMesh[];
|
|
36
|
+
/** art-mesh index → mesh (binary path); empty for cpu-program. */
|
|
37
|
+
byArtMesh: Map<number, DrawableMesh>;
|
|
38
|
+
poseOpacity: Float32Array;
|
|
39
|
+
poseDirty: boolean;
|
|
40
|
+
partOpacity: Map<string, number>;
|
|
41
|
+
bindings: ParameterBinding[];
|
|
42
|
+
paramIndexById: Map<string, number>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const stateByModel = new WeakMap<InternalModel, Moc3State>();
|
|
46
|
+
|
|
47
|
+
function allocateMeshesFromProgram(program: ModelProgram): DrawableMesh[] {
|
|
48
|
+
return program.drawables.map((d) => ({
|
|
28
49
|
index: d.index,
|
|
29
50
|
textureIndex: d.textureIndex,
|
|
30
|
-
vertexPositions: d.positions,
|
|
51
|
+
vertexPositions: new Float32Array(d.positions),
|
|
31
52
|
uvs: d.uvs,
|
|
32
53
|
indices: d.indices,
|
|
33
54
|
opacity: d.opacity,
|
|
@@ -37,49 +58,96 @@ function toDrawableMesh(d: FrameDrawable): DrawableMesh {
|
|
|
37
58
|
dynamicFlag: true,
|
|
38
59
|
maskIndices: [...d.maskIndices],
|
|
39
60
|
visible: d.visible,
|
|
40
|
-
};
|
|
61
|
+
}));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function buildBindings(instance: ModelInstance): {
|
|
65
|
+
bindings: ParameterBinding[];
|
|
66
|
+
paramIndexById: Map<string, number>;
|
|
67
|
+
} {
|
|
68
|
+
const bindings: ParameterBinding[] = [];
|
|
69
|
+
const paramIndexById = new Map<string, number>();
|
|
70
|
+
for (let i = 0; i < instance.program.parameters.length; i++) {
|
|
71
|
+
const p = instance.program.parameters[i]!;
|
|
72
|
+
bindings.push({
|
|
73
|
+
id: p.id,
|
|
74
|
+
min: p.min,
|
|
75
|
+
max: p.max,
|
|
76
|
+
defaultValue: p.defaultValue,
|
|
77
|
+
value: instance.parameterValues[i] ?? p.defaultValue,
|
|
78
|
+
});
|
|
79
|
+
paramIndexById.set(p.id, i);
|
|
80
|
+
}
|
|
81
|
+
return { bindings, paramIndexById };
|
|
41
82
|
}
|
|
42
83
|
|
|
43
|
-
function
|
|
44
|
-
let
|
|
45
|
-
|
|
46
|
-
|
|
84
|
+
function syncBindingValues(state: Moc3State): void {
|
|
85
|
+
for (let i = 0; i < state.bindings.length; i++) {
|
|
86
|
+
const b = state.bindings[i]!;
|
|
87
|
+
(b as { value: number }).value =
|
|
88
|
+
state.instance.parameterValues[i] ?? b.defaultValue;
|
|
47
89
|
}
|
|
48
|
-
return s;
|
|
49
90
|
}
|
|
50
91
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
bakedFingerprint: string;
|
|
57
|
-
/** Runtime PartOpacity overrides (id → 0..1). */
|
|
58
|
-
partOpacity: Map<string, number>;
|
|
92
|
+
function refreshDrawView(state: Moc3State): void {
|
|
93
|
+
const view = state.drawView;
|
|
94
|
+
view.length = 0;
|
|
95
|
+
for (const m of state.meshes) view.push(m);
|
|
96
|
+
view.sort((a, b) => a.renderOrder - b.renderOrder || a.index - b.index);
|
|
59
97
|
}
|
|
60
98
|
|
|
61
|
-
|
|
99
|
+
function applyPartOpacity(state: Moc3State): void {
|
|
100
|
+
const tables = state.doc ? readMoc3PartTables(state.doc) : null;
|
|
101
|
+
for (const mesh of state.meshes) {
|
|
102
|
+
const base = state.poseOpacity[mesh.index] ?? mesh.opacity;
|
|
103
|
+
if (!tables || state.partOpacity.size === 0) {
|
|
104
|
+
mesh.opacity = base;
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
mesh.opacity =
|
|
108
|
+
base * cascadedPartOpacity(tables, mesh.index, state.partOpacity);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
62
111
|
|
|
63
112
|
function bakePose(state: Moc3State): void {
|
|
64
|
-
if (!state.
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
113
|
+
if (!state.poseDirty) return;
|
|
114
|
+
if (state.doc) {
|
|
115
|
+
const values = state.instance.parameterValues;
|
|
116
|
+
evaluateMoc3PoseInto(
|
|
117
|
+
state.doc,
|
|
118
|
+
(i) => values[i] ?? 0,
|
|
119
|
+
state.byArtMesh,
|
|
120
|
+
state.poseOpacity,
|
|
121
|
+
);
|
|
122
|
+
} else {
|
|
123
|
+
evaluateFrameInto(state.instance, state.meshes, state.poseOpacity);
|
|
124
|
+
}
|
|
125
|
+
refreshDrawView(state);
|
|
126
|
+
syncBindingValues(state);
|
|
127
|
+
state.poseDirty = false;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function frameFromMeshes(
|
|
131
|
+
meshes: readonly DrawableMesh[],
|
|
132
|
+
timeSeconds: number,
|
|
133
|
+
): FrameSnapshot {
|
|
134
|
+
const drawables: FrameDrawable[] = [];
|
|
135
|
+
for (const m of meshes) {
|
|
136
|
+
drawables.push({
|
|
137
|
+
index: m.index,
|
|
138
|
+
textureIndex: m.textureIndex,
|
|
139
|
+
positions: m.vertexPositions,
|
|
140
|
+
uvs: m.uvs,
|
|
141
|
+
indices: m.indices,
|
|
142
|
+
opacity: m.opacity,
|
|
143
|
+
blendMode: m.blendMode,
|
|
144
|
+
renderOrder: m.renderOrder,
|
|
145
|
+
visible: m.visible,
|
|
146
|
+
invertedMask: m.invertedMask,
|
|
147
|
+
maskIndices: m.maskIndices,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
return { timeSeconds, drawables };
|
|
83
151
|
}
|
|
84
152
|
|
|
85
153
|
/** moc3 model backend for binary `.moc3` input or CPU `.program.json` fixtures. */
|
|
@@ -121,18 +189,39 @@ export class Moc3Backend implements ModelBackend {
|
|
|
121
189
|
}
|
|
122
190
|
|
|
123
191
|
const instance = createModelInstance(program);
|
|
192
|
+
const meshes = allocateMeshesFromProgram(program);
|
|
193
|
+
const byArtMesh = new Map<number, DrawableMesh>();
|
|
194
|
+
if (doc) {
|
|
195
|
+
const artOrder = moc3ArtMeshIndicesInProgramOrder(doc);
|
|
196
|
+
for (let i = 0; i < artOrder.length; i++) {
|
|
197
|
+
const art = artOrder[i]!;
|
|
198
|
+
const mesh = meshes[i];
|
|
199
|
+
if (mesh) byArtMesh.set(art, mesh);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
const poseOpacity = new Float32Array(meshes.length);
|
|
203
|
+
for (const m of meshes) poseOpacity[m.index] = m.opacity;
|
|
204
|
+
const { bindings, paramIndexById } = buildBindings(instance);
|
|
124
205
|
const model: InternalModel = {
|
|
125
206
|
id: settings.name ?? settings.url,
|
|
126
207
|
settings,
|
|
127
208
|
format: "moc3",
|
|
128
209
|
};
|
|
129
|
-
|
|
210
|
+
const state: Moc3State = {
|
|
130
211
|
doc,
|
|
131
212
|
instance,
|
|
132
|
-
|
|
133
|
-
|
|
213
|
+
meshes,
|
|
214
|
+
drawView: [],
|
|
215
|
+
byArtMesh,
|
|
216
|
+
poseOpacity,
|
|
217
|
+
poseDirty: true,
|
|
134
218
|
partOpacity: new Map(),
|
|
135
|
-
|
|
219
|
+
bindings,
|
|
220
|
+
paramIndexById,
|
|
221
|
+
};
|
|
222
|
+
bakePose(state);
|
|
223
|
+
applyPartOpacity(state);
|
|
224
|
+
stateByModel.set(model, state);
|
|
136
225
|
return model;
|
|
137
226
|
}
|
|
138
227
|
|
|
@@ -141,54 +230,39 @@ export class Moc3Backend implements ModelBackend {
|
|
|
141
230
|
if (!state) return;
|
|
142
231
|
state.instance.timeSeconds += deltaTimeSeconds;
|
|
143
232
|
bakePose(state);
|
|
144
|
-
|
|
145
|
-
state.lastFrame = {
|
|
146
|
-
...state.lastFrame,
|
|
147
|
-
timeSeconds: state.instance.timeSeconds,
|
|
148
|
-
};
|
|
149
|
-
}
|
|
233
|
+
applyPartOpacity(state);
|
|
150
234
|
}
|
|
151
235
|
|
|
152
236
|
getDrawables(model: InternalModel): DrawableMesh[] {
|
|
153
237
|
const state = stateByModel.get(model);
|
|
154
238
|
if (!state) return [];
|
|
155
239
|
bakePose(state);
|
|
156
|
-
|
|
157
|
-
state.
|
|
158
|
-
const tables = state.doc ? readMoc3PartTables(state.doc) : null;
|
|
159
|
-
return frame.drawables.map((d) => {
|
|
160
|
-
const mesh = toDrawableMesh(d);
|
|
161
|
-
if (!tables || state.partOpacity.size === 0) return mesh;
|
|
162
|
-
const mul = cascadedPartOpacity(tables, d.index, state.partOpacity);
|
|
163
|
-
return { ...mesh, opacity: mesh.opacity * mul };
|
|
164
|
-
});
|
|
240
|
+
applyPartOpacity(state);
|
|
241
|
+
return state.drawView;
|
|
165
242
|
}
|
|
166
243
|
|
|
167
244
|
captureFrame(model: InternalModel): FrameSnapshot | null {
|
|
168
245
|
const state = stateByModel.get(model);
|
|
169
246
|
if (!state) return null;
|
|
170
247
|
bakePose(state);
|
|
171
|
-
|
|
172
|
-
state.
|
|
173
|
-
const tables = state.doc ? readMoc3PartTables(state.doc) : null;
|
|
174
|
-
if (!tables || state.partOpacity.size === 0) return frame;
|
|
175
|
-
return {
|
|
176
|
-
...frame,
|
|
177
|
-
drawables: frame.drawables.map((d) => ({
|
|
178
|
-
...d,
|
|
179
|
-
opacity:
|
|
180
|
-
d.opacity *
|
|
181
|
-
cascadedPartOpacity(tables, d.index, state.partOpacity),
|
|
182
|
-
})),
|
|
183
|
-
};
|
|
248
|
+
applyPartOpacity(state);
|
|
249
|
+
return frameFromMeshes(state.drawView, state.instance.timeSeconds);
|
|
184
250
|
}
|
|
185
251
|
|
|
186
252
|
setParameter(model: InternalModel, id: string, value: number): void {
|
|
187
253
|
const state = stateByModel.get(model);
|
|
188
254
|
if (!state) return;
|
|
189
255
|
setParameterValue(state.instance, id, value);
|
|
190
|
-
|
|
191
|
-
|
|
256
|
+
const index = state.paramIndexById.get(id);
|
|
257
|
+
if (index !== undefined) {
|
|
258
|
+
const binding = state.bindings[index];
|
|
259
|
+
if (binding) {
|
|
260
|
+
(binding as { value: number }).value =
|
|
261
|
+
state.instance.parameterValues[index] ??
|
|
262
|
+
binding.defaultValue;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
state.poseDirty = true;
|
|
192
266
|
}
|
|
193
267
|
|
|
194
268
|
setPartOpacity(model: InternalModel, id: string, value: number): void {
|
|
@@ -196,19 +270,20 @@ export class Moc3Backend implements ModelBackend {
|
|
|
196
270
|
if (!state) return;
|
|
197
271
|
const v = Number.isFinite(value) ? Math.min(1, Math.max(0, value)) : 1;
|
|
198
272
|
state.partOpacity.set(id, v);
|
|
199
|
-
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
resolveParameter(model: InternalModel, id: string): number | undefined {
|
|
276
|
+
const state = stateByModel.get(model);
|
|
277
|
+
if (!state) return undefined;
|
|
278
|
+
return state.paramIndexById.get(id);
|
|
200
279
|
}
|
|
201
280
|
|
|
202
281
|
listParameters(model: InternalModel): readonly ParameterBinding[] {
|
|
203
282
|
const state = stateByModel.get(model);
|
|
204
283
|
if (!state) return [];
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
max: p.max,
|
|
209
|
-
defaultValue: p.defaultValue,
|
|
210
|
-
value: state.instance.parameterValues[i] ?? p.defaultValue,
|
|
211
|
-
}));
|
|
284
|
+
bakePose(state);
|
|
285
|
+
syncBindingValues(state);
|
|
286
|
+
return state.bindings;
|
|
212
287
|
}
|
|
213
288
|
|
|
214
289
|
hitTest(_model: InternalModel, _x: number, _y: number): string | null {
|
|
@@ -218,6 +293,10 @@ export class Moc3Backend implements ModelBackend {
|
|
|
218
293
|
destroyModel(model: InternalModel): void {
|
|
219
294
|
stateByModel.delete(model);
|
|
220
295
|
}
|
|
296
|
+
|
|
297
|
+
getResidentInstance(model: InternalModel): ModelInstance | null {
|
|
298
|
+
return stateByModel.get(model)?.instance ?? null;
|
|
299
|
+
}
|
|
221
300
|
}
|
|
222
301
|
|
|
223
302
|
export function createMoc3Backend(): Moc3Backend {
|
|
@@ -69,6 +69,12 @@ export interface ModelBackend {
|
|
|
69
69
|
/** Optional PartOpacity override (moc3 parts / pose / motion). */
|
|
70
70
|
setPartOpacity?(model: InternalModel, id: string, value: number): void;
|
|
71
71
|
|
|
72
|
+
/**
|
|
73
|
+
* Resolve parameter id → index (O(1) after load).
|
|
74
|
+
* Used by motion blend / hosts instead of `listParameters().find`.
|
|
75
|
+
*/
|
|
76
|
+
resolveParameter?(model: InternalModel, id: string): number | undefined;
|
|
77
|
+
|
|
72
78
|
listParameters?(model: InternalModel): readonly ParameterBinding[];
|
|
73
79
|
}
|
|
74
80
|
|
package/src/types.ts
CHANGED
|
@@ -36,7 +36,7 @@ export interface TextureData {
|
|
|
36
36
|
export interface ModelDrawPass {
|
|
37
37
|
setTextures(textures: TextureData[]): void;
|
|
38
38
|
|
|
39
|
-
draw(drawables: DrawableMesh[], modelMatrix: Float32Array): void;
|
|
39
|
+
draw(drawables: readonly DrawableMesh[], modelMatrix: Float32Array): void;
|
|
40
40
|
|
|
41
41
|
destroy(): void;
|
|
42
42
|
}
|