@doki-land/live2d-renderer 0.0.18 → 0.0.20

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.
@@ -49,11 +49,12 @@ export function moc2ParamGetterFromValues(
49
49
  return (id: string) => byId.get(id) ?? 0;
50
50
  }
51
51
 
52
- function normalizePositions(
52
+ function normalizePositionsInto(
53
53
  positions: Float32Array,
54
54
  canvasWidth: number,
55
55
  canvasHeight: number,
56
- ): Float32Array {
56
+ out: Float32Array,
57
+ ): void {
57
58
  // moc2 draw coords are canvas pixels with origin at the **top-left**
58
59
  // (Y increases downward), matching Live2D Cubism 2 canvas space.
59
60
  // Map to Y-up NDC so WebGL/WebGPU/Canvas2D share one convention:
@@ -61,14 +62,99 @@ function normalizePositions(
61
62
  // y: [0, h] → [1, -1] (top of canvas → NDC +Y)
62
63
  const w = canvasWidth > 0 ? canvasWidth : 1;
63
64
  const h = canvasHeight > 0 ? canvasHeight : 1;
64
- const out = new Float32Array(positions.length);
65
65
  for (let i = 0; i + 1 < positions.length; i += 2) {
66
66
  out[i] = (positions[i]! / w) * 2 - 1;
67
67
  out[i + 1] = 1 - (positions[i + 1]! / h) * 2;
68
68
  }
69
+ }
70
+
71
+ function normalizePositions(
72
+ positions: Float32Array,
73
+ canvasWidth: number,
74
+ canvasHeight: number,
75
+ ): Float32Array {
76
+ const out = new Float32Array(positions.length);
77
+ normalizePositionsInto(positions, canvasWidth, canvasHeight, out);
69
78
  return out;
70
79
  }
71
80
 
81
+ /**
82
+ * Evaluate moc2 keyforms/deformers into resident {@link DrawableMesh} buffers.
83
+ * Mesh object identity and shared `uvs`/`indices`/`maskIndices` are preserved
84
+ * when vertex counts are unchanged.
85
+ */
86
+ export function evaluateMoc2PoseInto(
87
+ model: Moc2ModelImpl,
88
+ getParam: (id: string) => number,
89
+ byId: ReadonlyMap<string, import("../types.js").DrawableMesh>,
90
+ ): void {
91
+ const ops = bakeDeformerOps(model, getParam);
92
+ const w = model.canvasWidth;
93
+ const h = model.canvasHeight;
94
+ for (const part of model.parts) {
95
+ for (const mesh of part.drawData) {
96
+ const sink = byId.get(mesh.id);
97
+ if (!sink) continue;
98
+ const floatCount = mesh.numPoints * 2;
99
+ const local = interpolateKeyforms(
100
+ mesh.keyforms,
101
+ mesh.pivotManager,
102
+ getParam,
103
+ floatCount,
104
+ );
105
+ const world = transformDrawablePositions(mesh, local, ops);
106
+ let pos = sink.vertexPositions;
107
+ if (pos.length !== world.length) {
108
+ pos = new Float32Array(world.length);
109
+ sink.vertexPositions = pos;
110
+ }
111
+ normalizePositionsInto(world, w, h, pos);
112
+ sink.opacity = interpolateScalarTable(
113
+ mesh.opacities,
114
+ mesh.pivotManager,
115
+ getParam,
116
+ 1,
117
+ );
118
+ sink.renderOrder = Math.round(
119
+ interpolateScalarTable(
120
+ mesh.drawOrders,
121
+ mesh.pivotManager,
122
+ getParam,
123
+ mesh.averageDrawOrder,
124
+ ),
125
+ );
126
+ sink.visible = part.visible;
127
+ }
128
+ }
129
+ }
130
+
131
+ /** Collect drawable ids in the same order {@link moc2ModelToProgram} assigns indices. */
132
+ export function moc2DrawableIdsInProgramOrder(
133
+ model: Moc2ModelImpl,
134
+ getParam?: (id: string) => number,
135
+ ): string[] {
136
+ const paramDefs = model.paramDefSet?.params ?? [];
137
+ const gp = getParam ?? buildParamGetter(paramDefs);
138
+ const drafts: { id: string; renderOrder: number }[] = [];
139
+ for (const part of model.parts) {
140
+ for (const mesh of part.drawData) {
141
+ drafts.push({
142
+ id: mesh.id,
143
+ renderOrder: Math.round(
144
+ interpolateScalarTable(
145
+ mesh.drawOrders,
146
+ mesh.pivotManager,
147
+ gp,
148
+ mesh.averageDrawOrder,
149
+ ),
150
+ ),
151
+ });
152
+ }
153
+ }
154
+ drafts.sort((a, b) => a.renderOrder - b.renderOrder);
155
+ return drafts.map((d) => d.id);
156
+ }
157
+
72
158
  interface DraftDrawable {
73
159
  id: string;
74
160
  textureIndex: number;
package/src/moc/moc2.ts CHANGED
@@ -6,11 +6,7 @@ import type {
6
6
  ModelSettings,
7
7
  } from "@doki-land/live2d-core";
8
8
  import { detectModelSettingsFormat } from "@doki-land/live2d-core";
9
- import {
10
- createModelInstance,
11
- evaluateFrame,
12
- setParameterValue,
13
- } from "../cpu/evaluate.js";
9
+ import { createModelInstance, setParameterValue } from "../cpu/evaluate.js";
14
10
  import type {
15
11
  ModelBackend,
16
12
  ModelBackendOptions,
@@ -19,65 +15,127 @@ import type {
19
15
  import type { BlendMode, DrawableMesh } from "../types.js";
20
16
  import { type Moc2ModelImpl, Moc2Parser } from "./moc2-objects.js";
21
17
  import {
18
+ evaluateMoc2PoseInto,
19
+ moc2DrawableIdsInProgramOrder,
22
20
  moc2ModelToProgram,
23
21
  moc2ParamGetterFromValues,
24
22
  } from "./moc2-to-program.js";
25
23
 
26
- function toDrawableMesh(d: FrameDrawable): DrawableMesh {
27
- return {
28
- index: d.index,
29
- textureIndex: d.textureIndex,
30
- vertexPositions: d.positions,
31
- uvs: d.uvs,
32
- indices: d.indices,
33
- opacity: d.opacity,
34
- blendMode: d.blendMode as BlendMode,
35
- invertedMask: d.invertedMask,
36
- renderOrder: d.renderOrder,
37
- dynamicFlag: true,
38
- maskIndices: [...d.maskIndices],
39
- visible: d.visible,
40
- };
24
+ interface Moc2State {
25
+ moc: Moc2ModelImpl;
26
+ /** Stable program + instance (never replaced after load). */
27
+ instance: ModelInstance;
28
+ /** Resident meshes; objects and topology buffers stay identity-stable. */
29
+ meshes: DrawableMesh[];
30
+ /** Draw-order view (same mesh refs, re-sorted when pose changes). */
31
+ drawView: DrawableMesh[];
32
+ byId: Map<string, DrawableMesh>;
33
+ poseDirty: boolean;
34
+ bindings: ParameterBinding[];
35
+ paramIndexById: Map<string, number>;
41
36
  }
42
37
 
43
- function paramFingerprint(values: ArrayLike<number>): string {
44
- let s = "";
45
- for (let i = 0; i < values.length; i++) {
46
- s += `${values[i]?.toFixed(5)},`;
38
+ const stateByModel = new WeakMap<InternalModel, Moc2State>();
39
+
40
+ function allocateMeshesFromProgram(
41
+ program: ReturnType<typeof moc2ModelToProgram>,
42
+ drawableIds: readonly string[],
43
+ ): { meshes: DrawableMesh[]; byId: Map<string, DrawableMesh> } {
44
+ const meshes: DrawableMesh[] = [];
45
+ const byId = new Map<string, DrawableMesh>();
46
+ for (let i = 0; i < program.drawables.length; i++) {
47
+ const d = program.drawables[i]!;
48
+ const mesh: DrawableMesh = {
49
+ index: d.index,
50
+ textureIndex: d.textureIndex,
51
+ vertexPositions: new Float32Array(d.positions),
52
+ uvs: d.uvs,
53
+ indices: d.indices,
54
+ opacity: d.opacity,
55
+ blendMode: d.blendMode as BlendMode,
56
+ invertedMask: d.invertedMask,
57
+ renderOrder: d.renderOrder,
58
+ dynamicFlag: true,
59
+ maskIndices: [...d.maskIndices],
60
+ visible: d.visible,
61
+ };
62
+ meshes.push(mesh);
63
+ const id = drawableIds[i];
64
+ if (id) byId.set(id, mesh);
47
65
  }
48
- return s;
66
+ return { meshes, byId };
49
67
  }
50
68
 
51
- interface Moc2State {
52
- moc: Moc2ModelImpl;
53
- instance: ModelInstance;
54
- lastFrame: FrameSnapshot | null;
55
- bakedFingerprint: string;
69
+ function buildBindings(instance: ModelInstance): {
70
+ bindings: ParameterBinding[];
71
+ paramIndexById: Map<string, number>;
72
+ } {
73
+ const bindings: ParameterBinding[] = [];
74
+ const paramIndexById = new Map<string, number>();
75
+ for (let i = 0; i < instance.program.parameters.length; i++) {
76
+ const p = instance.program.parameters[i]!;
77
+ const binding: ParameterBinding = {
78
+ id: p.id,
79
+ min: p.min,
80
+ max: p.max,
81
+ defaultValue: p.defaultValue,
82
+ value: instance.parameterValues[i] ?? p.defaultValue,
83
+ };
84
+ bindings.push(binding);
85
+ paramIndexById.set(p.id, i);
86
+ }
87
+ return { bindings, paramIndexById };
56
88
  }
57
89
 
58
- const stateByModel = new WeakMap<InternalModel, Moc2State>();
90
+ function syncBindingValues(state: Moc2State): void {
91
+ for (let i = 0; i < state.bindings.length; i++) {
92
+ const b = state.bindings[i]!;
93
+ (b as { value: number }).value =
94
+ state.instance.parameterValues[i] ?? b.defaultValue;
95
+ }
96
+ }
97
+
98
+ function refreshDrawView(state: Moc2State): void {
99
+ const view = state.drawView;
100
+ view.length = 0;
101
+ for (const m of state.meshes) view.push(m);
102
+ view.sort((a, b) => a.renderOrder - b.renderOrder || a.index - b.index);
103
+ }
59
104
 
60
- /** Re-bake moc2 geometry for current parameter values. */
105
+ /** Re-bake moc2 geometry for current parameter values into resident meshes. */
61
106
  function bakePose(state: Moc2State): void {
62
- const values = Float32Array.from(state.instance.parameterValues);
63
- const timeSeconds = state.instance.timeSeconds;
64
- const fp = paramFingerprint(values);
65
- if (fp === state.bakedFingerprint && state.lastFrame) {
66
- return;
67
- }
107
+ if (!state.poseDirty) return;
108
+ const getParam = moc2ParamGetterFromValues(
109
+ state.instance.program.parameters,
110
+ state.instance.parameterValues,
111
+ );
112
+ evaluateMoc2PoseInto(state.moc, getParam, state.byId);
113
+ refreshDrawView(state);
114
+ syncBindingValues(state);
115
+ state.poseDirty = false;
116
+ }
68
117
 
69
- const program = moc2ModelToProgram(state.moc, {
70
- getParam: moc2ParamGetterFromValues(
71
- state.moc.paramDefSet.params,
72
- values,
73
- ),
74
- });
75
- const next = createModelInstance(program);
76
- next.parameterValues.set(values);
77
- next.timeSeconds = timeSeconds;
78
- state.instance = next;
79
- state.bakedFingerprint = fp;
80
- state.lastFrame = evaluateFrame(next);
118
+ function frameFromMeshes(
119
+ meshes: readonly DrawableMesh[],
120
+ timeSeconds: number,
121
+ ): FrameSnapshot {
122
+ const drawables: FrameDrawable[] = [];
123
+ for (const m of meshes) {
124
+ drawables.push({
125
+ index: m.index,
126
+ textureIndex: m.textureIndex,
127
+ positions: m.vertexPositions,
128
+ uvs: m.uvs,
129
+ indices: m.indices,
130
+ opacity: m.opacity,
131
+ blendMode: m.blendMode,
132
+ renderOrder: m.renderOrder,
133
+ visible: m.visible,
134
+ invertedMask: m.invertedMask,
135
+ maskIndices: m.maskIndices,
136
+ });
137
+ }
138
+ return { timeSeconds, drawables };
81
139
  }
82
140
 
83
141
  /** moc2 (`.moc`) model backend — pure-TS decode → CPU evaluate. */
@@ -108,17 +166,30 @@ export class Moc2Backend implements ModelBackend {
108
166
  new Moc2Parser(bytes).parseModel();
109
167
  const program = moc2ModelToProgram(moc);
110
168
  const instance = createModelInstance(program);
169
+ const drawableIds = moc2DrawableIdsInProgramOrder(moc);
170
+ const { meshes, byId } = allocateMeshesFromProgram(
171
+ program,
172
+ drawableIds,
173
+ );
174
+ const { bindings, paramIndexById } = buildBindings(instance);
175
+ const drawView: DrawableMesh[] = [];
111
176
  const model: InternalModel = {
112
177
  id: settings.name ?? settings.url,
113
178
  settings,
114
179
  format: "moc2",
115
180
  };
116
- stateByModel.set(model, {
181
+ const state: Moc2State = {
117
182
  moc,
118
183
  instance,
119
- lastFrame: null,
120
- bakedFingerprint: paramFingerprint(instance.parameterValues),
121
- });
184
+ meshes,
185
+ drawView,
186
+ byId,
187
+ poseDirty: true,
188
+ bindings,
189
+ paramIndexById,
190
+ };
191
+ bakePose(state);
192
+ stateByModel.set(model, state);
122
193
  return model;
123
194
  }
124
195
 
@@ -127,50 +198,50 @@ export class Moc2Backend implements ModelBackend {
127
198
  if (!state) return;
128
199
  state.instance.timeSeconds += deltaTimeSeconds;
129
200
  bakePose(state);
130
- if (state.lastFrame) {
131
- state.lastFrame = {
132
- ...state.lastFrame,
133
- timeSeconds: state.instance.timeSeconds,
134
- };
135
- }
136
201
  }
137
202
 
138
203
  getDrawables(model: InternalModel): DrawableMesh[] {
139
204
  const state = stateByModel.get(model);
140
205
  if (!state) return [];
141
206
  bakePose(state);
142
- const frame = state.lastFrame ?? evaluateFrame(state.instance);
143
- state.lastFrame = frame;
144
- return frame.drawables.map(toDrawableMesh);
207
+ return state.drawView;
145
208
  }
146
209
 
147
210
  captureFrame(model: InternalModel): FrameSnapshot | null {
148
211
  const state = stateByModel.get(model);
149
212
  if (!state) return null;
150
213
  bakePose(state);
151
- const frame = state.lastFrame ?? evaluateFrame(state.instance);
152
- state.lastFrame = frame;
153
- return frame;
214
+ return frameFromMeshes(state.drawView, state.instance.timeSeconds);
154
215
  }
155
216
 
156
217
  setParameter(model: InternalModel, id: string, value: number): void {
157
218
  const state = stateByModel.get(model);
158
219
  if (!state) return;
159
220
  setParameterValue(state.instance, id, value);
160
- state.lastFrame = null;
161
- state.bakedFingerprint = "";
221
+ const index = state.paramIndexById.get(id);
222
+ if (index !== undefined) {
223
+ const binding = state.bindings[index];
224
+ if (binding) {
225
+ (binding as { value: number }).value =
226
+ state.instance.parameterValues[index] ??
227
+ binding.defaultValue;
228
+ }
229
+ }
230
+ state.poseDirty = true;
231
+ }
232
+
233
+ resolveParameter(model: InternalModel, id: string): number | undefined {
234
+ const state = stateByModel.get(model);
235
+ if (!state) return undefined;
236
+ return state.paramIndexById.get(id);
162
237
  }
163
238
 
164
239
  listParameters(model: InternalModel): readonly ParameterBinding[] {
165
240
  const state = stateByModel.get(model);
166
241
  if (!state) return [];
167
- return state.instance.program.parameters.map((p, i) => ({
168
- id: p.id,
169
- min: p.min,
170
- max: p.max,
171
- defaultValue: p.defaultValue,
172
- value: state.instance.parameterValues[i] ?? p.defaultValue,
173
- }));
242
+ bakePose(state);
243
+ syncBindingValues(state);
244
+ return state.bindings;
174
245
  }
175
246
 
176
247
  hitTest(_model: InternalModel, _x: number, _y: number): string | null {
@@ -180,6 +251,11 @@ export class Moc2Backend implements ModelBackend {
180
251
  destroyModel(model: InternalModel): void {
181
252
  stateByModel.delete(model);
182
253
  }
254
+
255
+ /** Test/harness: stable program + instance refs. */
256
+ getResidentInstance(model: InternalModel): ModelInstance | null {
257
+ return stateByModel.get(model)?.instance ?? null;
258
+ }
183
259
  }
184
260
 
185
261
  export function createMoc2Backend(): Moc2Backend {
@@ -32,23 +32,39 @@ import {
32
32
  moc3SectionStrings,
33
33
  } from "./moc3-reader.js";
34
34
 
35
- function normalizePositions(
35
+ function normalizePositionsInto(
36
36
  positions: Float32Array,
37
37
  canvasWidth: number,
38
38
  canvasHeight: number,
39
39
  pixelsPerUnit: number,
40
- ): Float32Array {
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
+ }