@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.
@@ -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
+ }
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
- evaluateFrame,
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 { moc3DocumentToProgram } from "./moc3-to-program.js";
24
+ import {
25
+ evaluateMoc3PoseInto,
26
+ moc3ArtMeshIndicesInProgramOrder,
27
+ moc3DocumentToProgram,
28
+ } from "./moc3-to-program.js";
25
29
 
26
- function toDrawableMesh(d: FrameDrawable): DrawableMesh {
27
- return {
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 paramFingerprint(values: ArrayLike<number>): string {
44
- let s = "";
45
- for (let i = 0; i < values.length; i++) {
46
- s += `${values[i]?.toFixed(5)},`;
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
- interface Moc3State {
52
- /** Present for binary MOC3 input; null for cpu-program fixtures. */
53
- doc: Moc3Document | null;
54
- instance: ModelInstance;
55
- lastFrame: FrameSnapshot | null;
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
- const stateByModel = new WeakMap<InternalModel, Moc3State>();
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.doc) {
65
- state.lastFrame = evaluateFrame(state.instance);
66
- return;
67
- }
68
-
69
- const values = Float32Array.from(state.instance.parameterValues);
70
- const timeSeconds = state.instance.timeSeconds;
71
- const fp = paramFingerprint(values);
72
- if (fp === state.bakedFingerprint && state.lastFrame) return;
73
-
74
- const program = moc3DocumentToProgram(state.doc, {
75
- getParamByIndex: (i) => values[i] ?? 0,
76
- });
77
- const next = createModelInstance(program);
78
- next.parameterValues.set(values);
79
- next.timeSeconds = timeSeconds;
80
- state.instance = next;
81
- state.bakedFingerprint = fp;
82
- state.lastFrame = evaluateFrame(next);
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
- stateByModel.set(model, {
210
+ const state: Moc3State = {
130
211
  doc,
131
212
  instance,
132
- lastFrame: null,
133
- bakedFingerprint: paramFingerprint(instance.parameterValues),
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
- if (state.lastFrame) {
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
- const frame = state.lastFrame ?? evaluateFrame(state.instance);
157
- state.lastFrame = frame;
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
- const frame = state.lastFrame ?? evaluateFrame(state.instance);
172
- state.lastFrame = frame;
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
- state.lastFrame = null;
191
- state.bakedFingerprint = "";
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
- // Opacity is applied in getDrawables; no need to rebake deform.
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
- return state.instance.program.parameters.map((p, i) => ({
206
- id: p.id,
207
- min: p.min,
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
  }