@doki-land/live2d-renderer 0.0.19 → 0.0.21
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 +51 -5
- package/dist/index.js +475 -203
- package/package.json +2 -2
- package/src/backends/canvas2d.ts +6 -7
- package/src/backends/webgl2.ts +54 -33
- package/src/backends/webgpu-mesh-cache.ts +206 -0
- package/src/backends/webgpu.ts +158 -186
- package/src/index.ts +9 -0
- package/src/render/clipping-plan.ts +140 -0
- package/src/render/clipping.ts +22 -6
- package/src/render/mesh-interleave.ts +28 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@doki-land/live2d-renderer",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.21",
|
|
4
4
|
"description": "Live2D rendering — moc2/moc3 decode + WebGPU, WebGL2, Canvas2D backends for live2d.ts.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"test": "vitest run"
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
|
-
"@doki-land/live2d-core": "0.0.
|
|
48
|
+
"@doki-land/live2d-core": "0.0.21"
|
|
49
49
|
},
|
|
50
50
|
"sideEffects": false
|
|
51
51
|
}
|
package/src/backends/canvas2d.ts
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
import { canvasCompositeForBlendMode } from "../render/blend.js";
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
type MaskLayoutRect,
|
|
5
|
-
partitionForClipping,
|
|
6
|
-
} from "../render/clipping.js";
|
|
2
|
+
import type { MaskLayoutRect } from "../render/clipping.js";
|
|
3
|
+
import { ResidentClippingPlan } from "../render/clipping-plan.js";
|
|
7
4
|
import {
|
|
8
5
|
modelXToCanvasPixelX,
|
|
9
6
|
modelYUpToCanvasPixelY,
|
|
@@ -159,6 +156,7 @@ class Canvas2DModelDrawPass implements ModelDrawPass {
|
|
|
159
156
|
readonly #ctx: CanvasRenderingContext2D;
|
|
160
157
|
readonly #options: Canvas2DRendererOptions;
|
|
161
158
|
#textures: (TextureData | null)[] = [];
|
|
159
|
+
readonly #clipPlan = new ResidentClippingPlan();
|
|
162
160
|
|
|
163
161
|
constructor(
|
|
164
162
|
ctx: CanvasRenderingContext2D,
|
|
@@ -182,10 +180,10 @@ class Canvas2DModelDrawPass implements ModelDrawPass {
|
|
|
182
180
|
const byIndex = new Map<number, DrawableMesh>();
|
|
183
181
|
for (const d of drawables) byIndex.set(d.index, d);
|
|
184
182
|
// UV-grid only: Canvas2D cannot isolate R/G/B/A channels.
|
|
185
|
-
const partitioned =
|
|
183
|
+
const partitioned = this.#clipPlan.resolveMeshes(drawables, {
|
|
186
184
|
mode: "uv-grid",
|
|
187
185
|
});
|
|
188
|
-
const contexts =
|
|
186
|
+
const contexts = partitioned.contexts;
|
|
189
187
|
const { maskOnly } = partitioned;
|
|
190
188
|
|
|
191
189
|
if (contexts.length === 0) {
|
|
@@ -380,6 +378,7 @@ class Canvas2DModelDrawPass implements ModelDrawPass {
|
|
|
380
378
|
|
|
381
379
|
destroy(): void {
|
|
382
380
|
this.#textures = [];
|
|
381
|
+
this.#clipPlan.clear();
|
|
383
382
|
}
|
|
384
383
|
}
|
|
385
384
|
|
package/src/backends/webgl2.ts
CHANGED
|
@@ -7,13 +7,16 @@
|
|
|
7
7
|
|
|
8
8
|
import { applyWebGl2BlendMode } from "../render/blend.js";
|
|
9
9
|
import {
|
|
10
|
-
fitClippingContexts,
|
|
11
10
|
type LaidOutClippingContext,
|
|
12
11
|
type MaskLayoutRect,
|
|
13
12
|
maskChannelVec4,
|
|
14
13
|
maskLayoutVec4,
|
|
15
|
-
partitionForClipping,
|
|
16
14
|
} from "../render/clipping.js";
|
|
15
|
+
import { ResidentClippingPlan } from "../render/clipping-plan.js";
|
|
16
|
+
import {
|
|
17
|
+
interleaveByteLength,
|
|
18
|
+
interleavePosUv,
|
|
19
|
+
} from "../render/mesh-interleave.js";
|
|
17
20
|
import {
|
|
18
21
|
PREVIEW_FILL,
|
|
19
22
|
PREVIEW_STROKE,
|
|
@@ -149,23 +152,6 @@ function linkProgram(
|
|
|
149
152
|
return program;
|
|
150
153
|
}
|
|
151
154
|
|
|
152
|
-
function interleavePosUv(
|
|
153
|
-
positions: Float32Array,
|
|
154
|
-
uvs: Float32Array,
|
|
155
|
-
): Float32Array {
|
|
156
|
-
const n = Math.floor(positions.length / 2);
|
|
157
|
-
const out = new Float32Array(n * 4);
|
|
158
|
-
for (let i = 0; i < n; i++) {
|
|
159
|
-
const o = i * 4;
|
|
160
|
-
const p = i * 2;
|
|
161
|
-
out[o] = positions[p]!;
|
|
162
|
-
out[o + 1] = positions[p + 1]!;
|
|
163
|
-
out[o + 2] = uvs[p] ?? 0;
|
|
164
|
-
out[o + 3] = uvs[p + 1] ?? 0;
|
|
165
|
-
}
|
|
166
|
-
return out;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
155
|
function requireUniform(
|
|
170
156
|
gl: WebGL2RenderingContext,
|
|
171
157
|
program: WebGLProgram,
|
|
@@ -208,6 +194,13 @@ class WebGl2ModelDrawPass implements ModelDrawPass {
|
|
|
208
194
|
#maskTex: WebGLTexture | null = null;
|
|
209
195
|
#maskW = 0;
|
|
210
196
|
#maskH = 0;
|
|
197
|
+
readonly #clipPlan = new ResidentClippingPlan();
|
|
198
|
+
#interleaved: Float32Array<ArrayBufferLike> = new Float32Array(64);
|
|
199
|
+
#indicesRef: Uint16Array | null = null;
|
|
200
|
+
readonly #layoutScratch = new Float32Array(4);
|
|
201
|
+
readonly #boundsScratch = new Float32Array(4);
|
|
202
|
+
readonly #channelScratch = new Float32Array(4);
|
|
203
|
+
readonly #defaultChannel = new Float32Array([0, 0, 0, 1]);
|
|
211
204
|
|
|
212
205
|
constructor(gl: WebGL2RenderingContext) {
|
|
213
206
|
this.#gl = gl;
|
|
@@ -395,13 +388,25 @@ class WebGl2ModelDrawPass implements ModelDrawPass {
|
|
|
395
388
|
gl.bindTexture(gl.TEXTURE_2D, null);
|
|
396
389
|
}
|
|
397
390
|
|
|
398
|
-
#uploadMesh(d: DrawableMesh): void {
|
|
391
|
+
#uploadMesh(d: DrawableMesh, uploadIndices = true): void {
|
|
399
392
|
const gl = this.#gl;
|
|
400
|
-
|
|
393
|
+
this.#interleaved = interleavePosUv(
|
|
394
|
+
d.vertexPositions,
|
|
395
|
+
d.uvs,
|
|
396
|
+
this.#interleaved,
|
|
397
|
+
);
|
|
398
|
+
const vBytes = interleaveByteLength(d.vertexPositions);
|
|
401
399
|
gl.bindBuffer(gl.ARRAY_BUFFER, this.#vbo);
|
|
402
|
-
gl.bufferData(
|
|
400
|
+
gl.bufferData(
|
|
401
|
+
gl.ARRAY_BUFFER,
|
|
402
|
+
this.#interleaved.subarray(0, vBytes / 4),
|
|
403
|
+
gl.DYNAMIC_DRAW,
|
|
404
|
+
);
|
|
403
405
|
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.#ibo);
|
|
404
|
-
|
|
406
|
+
if (uploadIndices && this.#indicesRef !== d.indices) {
|
|
407
|
+
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, d.indices, gl.STATIC_DRAW);
|
|
408
|
+
this.#indicesRef = d.indices;
|
|
409
|
+
}
|
|
405
410
|
}
|
|
406
411
|
|
|
407
412
|
#drawMeshColor(
|
|
@@ -434,19 +439,23 @@ class WebGl2ModelDrawPass implements ModelDrawPass {
|
|
|
434
439
|
);
|
|
435
440
|
gl.uniform1f(this.#useMaskLoc, useMask ? 1 : 0);
|
|
436
441
|
gl.uniform1f(this.#invertMaskLoc, invertMask ? 1 : 0);
|
|
437
|
-
|
|
438
|
-
|
|
442
|
+
gl.uniform4fv(
|
|
443
|
+
this.#maskLayoutLoc,
|
|
444
|
+
maskLayoutVec4(
|
|
445
|
+
layout ?? { x: 0, y: 0, width: 1, height: 1 },
|
|
446
|
+
this.#layoutScratch,
|
|
447
|
+
),
|
|
439
448
|
);
|
|
440
|
-
gl.uniform4fv(this.#maskLayoutLoc, layoutVec);
|
|
441
449
|
gl.uniform4fv(
|
|
442
450
|
this.#maskBoundsLoc,
|
|
443
451
|
maskLayoutVec4(
|
|
444
452
|
modelBounds ?? { x: -1, y: -1, width: 2, height: 2 },
|
|
453
|
+
this.#boundsScratch,
|
|
445
454
|
),
|
|
446
455
|
);
|
|
447
456
|
gl.uniform4fv(
|
|
448
457
|
this.#channelFlagLoc,
|
|
449
|
-
channelFlag ??
|
|
458
|
+
channelFlag ?? this.#defaultChannel,
|
|
450
459
|
);
|
|
451
460
|
if (useMask && this.#maskTex) {
|
|
452
461
|
gl.activeTexture(gl.TEXTURE1);
|
|
@@ -467,6 +476,7 @@ class WebGl2ModelDrawPass implements ModelDrawPass {
|
|
|
467
476
|
PREVIEW_STROKE.a * d.opacity,
|
|
468
477
|
);
|
|
469
478
|
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, lines, gl.DYNAMIC_DRAW);
|
|
479
|
+
this.#indicesRef = null;
|
|
470
480
|
gl.drawElements(gl.LINES, lines.length, gl.UNSIGNED_SHORT, 0);
|
|
471
481
|
}
|
|
472
482
|
}
|
|
@@ -486,8 +496,14 @@ class WebGl2ModelDrawPass implements ModelDrawPass {
|
|
|
486
496
|
gl.uniform1i(this.#maskTexLoc, 0);
|
|
487
497
|
gl.uniform1f(this.#maskUseTexLoc, gpuTex ? 1 : 0);
|
|
488
498
|
gl.uniform1f(this.#maskOpacityLoc, Math.max(d.opacity, 1));
|
|
489
|
-
gl.uniform4fv(
|
|
490
|
-
|
|
499
|
+
gl.uniform4fv(
|
|
500
|
+
this.#maskWriteLayoutLoc,
|
|
501
|
+
maskLayoutVec4(layout, this.#layoutScratch),
|
|
502
|
+
);
|
|
503
|
+
gl.uniform4fv(
|
|
504
|
+
this.#maskWriteBoundsLoc,
|
|
505
|
+
maskLayoutVec4(modelBounds, this.#boundsScratch),
|
|
506
|
+
);
|
|
491
507
|
gl.uniform4fv(this.#maskWriteChannelLoc, channelFlag);
|
|
492
508
|
gl.drawElements(gl.TRIANGLES, d.indices.length, gl.UNSIGNED_SHORT, 0);
|
|
493
509
|
}
|
|
@@ -503,8 +519,8 @@ class WebGl2ModelDrawPass implements ModelDrawPass {
|
|
|
503
519
|
const byIndex = new Map<number, DrawableMesh>();
|
|
504
520
|
for (const d of drawables) byIndex.set(d.index, d);
|
|
505
521
|
|
|
506
|
-
const partitioned =
|
|
507
|
-
const contexts =
|
|
522
|
+
const partitioned = this.#clipPlan.resolveMeshes(drawables);
|
|
523
|
+
const contexts = partitioned.contexts;
|
|
508
524
|
const { maskOnly } = partitioned;
|
|
509
525
|
|
|
510
526
|
gl.bindVertexArray(this.#vao);
|
|
@@ -522,7 +538,10 @@ class WebGl2ModelDrawPass implements ModelDrawPass {
|
|
|
522
538
|
gl.blendFuncSeparate(gl.ONE, gl.ONE, gl.ONE, gl.ONE);
|
|
523
539
|
|
|
524
540
|
for (const ctx of contexts) {
|
|
525
|
-
const flag = maskChannelVec4(
|
|
541
|
+
const flag = maskChannelVec4(
|
|
542
|
+
ctx.channelFlag,
|
|
543
|
+
this.#channelScratch,
|
|
544
|
+
);
|
|
526
545
|
for (const mi of ctx.maskIndices) {
|
|
527
546
|
const maskMesh = byIndex.get(mi);
|
|
528
547
|
if (maskMesh) {
|
|
@@ -570,7 +589,7 @@ class WebGl2ModelDrawPass implements ModelDrawPass {
|
|
|
570
589
|
clip.invertedMask,
|
|
571
590
|
clip.layout,
|
|
572
591
|
clip.modelBounds,
|
|
573
|
-
maskChannelVec4(clip.channelFlag),
|
|
592
|
+
maskChannelVec4(clip.channelFlag, this.#channelScratch),
|
|
574
593
|
);
|
|
575
594
|
} else {
|
|
576
595
|
this.#drawMeshColor(d, false, false, null, null, null);
|
|
@@ -587,6 +606,8 @@ class WebGl2ModelDrawPass implements ModelDrawPass {
|
|
|
587
606
|
destroy(): void {
|
|
588
607
|
const gl = this.#gl;
|
|
589
608
|
this.#clearGpuTextures();
|
|
609
|
+
this.#clipPlan.clear();
|
|
610
|
+
this.#indicesRef = null;
|
|
590
611
|
if (this.#maskTex) gl.deleteTexture(this.#maskTex);
|
|
591
612
|
if (this.#maskFbo) gl.deleteFramebuffer(this.#maskFbo);
|
|
592
613
|
gl.deleteBuffer(this.#vbo);
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-drawable WebGPU buffer / bind-group cache.
|
|
3
|
+
* Static index/uv topology stays resident; vertex positions + small UBOs use writeBuffer.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
interleaveByteLength,
|
|
8
|
+
interleavePosUv,
|
|
9
|
+
} from "../render/mesh-interleave.js";
|
|
10
|
+
|
|
11
|
+
/** WebGPU INDEX buffers must be a multiple of 4 bytes. */
|
|
12
|
+
export function indexBufferByteSize(byteLength: number): number {
|
|
13
|
+
return (byteLength + 3) & ~3;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface WebGpuMeshSlot {
|
|
17
|
+
index: number;
|
|
18
|
+
vbo: GPUBuffer;
|
|
19
|
+
vboBytes: number;
|
|
20
|
+
interleaved: Float32Array<ArrayBufferLike>;
|
|
21
|
+
ibo: GPUBuffer;
|
|
22
|
+
iboBytes: number;
|
|
23
|
+
indicesRef: Uint16Array | null;
|
|
24
|
+
uvsRef: Float32Array | null;
|
|
25
|
+
/** Uniform buffer (max 80 bytes covers all shader variants). */
|
|
26
|
+
ubo: GPUBuffer;
|
|
27
|
+
uboFloats: Float32Array<ArrayBufferLike>;
|
|
28
|
+
bgMask: GPUBindGroup | null;
|
|
29
|
+
bgTextured: GPUBindGroup | null;
|
|
30
|
+
bgClippedTextured: GPUBindGroup | null;
|
|
31
|
+
bgSolid: GPUBindGroup | null;
|
|
32
|
+
bgClippedSolid: GPUBindGroup | null;
|
|
33
|
+
texIndex: number;
|
|
34
|
+
useWhite: boolean;
|
|
35
|
+
maskGeneration: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function createOrGrowBuffer(
|
|
39
|
+
device: GPUDevice,
|
|
40
|
+
prev: GPUBuffer | null,
|
|
41
|
+
prevBytes: number,
|
|
42
|
+
needBytes: number,
|
|
43
|
+
usage: GPUBufferUsageFlags,
|
|
44
|
+
): { buffer: GPUBuffer; bytes: number } {
|
|
45
|
+
const bytes = Math.max(4, needBytes);
|
|
46
|
+
if (prev && prevBytes >= bytes) return { buffer: prev, bytes: prevBytes };
|
|
47
|
+
prev?.destroy();
|
|
48
|
+
return {
|
|
49
|
+
buffer: device.createBuffer({
|
|
50
|
+
size: bytes,
|
|
51
|
+
usage,
|
|
52
|
+
}),
|
|
53
|
+
bytes,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export class WebGpuMeshCache {
|
|
58
|
+
readonly #slots = new Map<number, WebGpuMeshSlot>();
|
|
59
|
+
#maskGeneration = 0;
|
|
60
|
+
|
|
61
|
+
bumpMaskGeneration(): void {
|
|
62
|
+
this.#maskGeneration += 1;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
get maskGeneration(): number {
|
|
66
|
+
return this.#maskGeneration;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
clear(): void {
|
|
70
|
+
for (const slot of this.#slots.values()) {
|
|
71
|
+
slot.vbo.destroy();
|
|
72
|
+
slot.ibo.destroy();
|
|
73
|
+
slot.ubo.destroy();
|
|
74
|
+
}
|
|
75
|
+
this.#slots.clear();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Drop bind groups that reference model textures (after setTextures). */
|
|
79
|
+
invalidateTextureBindGroups(): void {
|
|
80
|
+
for (const slot of this.#slots.values()) {
|
|
81
|
+
slot.bgMask = null;
|
|
82
|
+
slot.bgTextured = null;
|
|
83
|
+
slot.bgClippedTextured = null;
|
|
84
|
+
slot.bgSolid = null;
|
|
85
|
+
slot.bgClippedSolid = null;
|
|
86
|
+
slot.texIndex = -1;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
ensureSlot(device: GPUDevice, drawableIndex: number): WebGpuMeshSlot {
|
|
91
|
+
let slot = this.#slots.get(drawableIndex);
|
|
92
|
+
if (slot) return slot;
|
|
93
|
+
const vbo = device.createBuffer({
|
|
94
|
+
size: 64,
|
|
95
|
+
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
|
|
96
|
+
});
|
|
97
|
+
const ibo = device.createBuffer({
|
|
98
|
+
size: 4,
|
|
99
|
+
usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST,
|
|
100
|
+
});
|
|
101
|
+
const ubo = device.createBuffer({
|
|
102
|
+
size: 80,
|
|
103
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
104
|
+
});
|
|
105
|
+
slot = {
|
|
106
|
+
index: drawableIndex,
|
|
107
|
+
vbo,
|
|
108
|
+
vboBytes: 64,
|
|
109
|
+
interleaved: new Float32Array(16),
|
|
110
|
+
ibo,
|
|
111
|
+
iboBytes: 4,
|
|
112
|
+
indicesRef: null,
|
|
113
|
+
uvsRef: null,
|
|
114
|
+
ubo,
|
|
115
|
+
uboFloats: new Float32Array(20),
|
|
116
|
+
bgMask: null,
|
|
117
|
+
bgTextured: null,
|
|
118
|
+
bgClippedTextured: null,
|
|
119
|
+
bgSolid: null,
|
|
120
|
+
bgClippedSolid: null,
|
|
121
|
+
texIndex: -1,
|
|
122
|
+
useWhite: false,
|
|
123
|
+
maskGeneration: -1,
|
|
124
|
+
};
|
|
125
|
+
this.#slots.set(drawableIndex, slot);
|
|
126
|
+
return slot;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Upload interleaved pos/uv when positions change every frame; refresh
|
|
131
|
+
* index buffer only when the `indices` TypedArray identity changes.
|
|
132
|
+
*/
|
|
133
|
+
uploadMesh(
|
|
134
|
+
device: GPUDevice,
|
|
135
|
+
slot: WebGpuMeshSlot,
|
|
136
|
+
positions: Float32Array,
|
|
137
|
+
uvs: Float32Array,
|
|
138
|
+
indices: Uint16Array,
|
|
139
|
+
): void {
|
|
140
|
+
slot.interleaved = interleavePosUv(positions, uvs, slot.interleaved);
|
|
141
|
+
const vBytes = interleaveByteLength(positions);
|
|
142
|
+
const grownV = createOrGrowBuffer(
|
|
143
|
+
device,
|
|
144
|
+
slot.vbo,
|
|
145
|
+
slot.vboBytes,
|
|
146
|
+
vBytes,
|
|
147
|
+
GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
|
|
148
|
+
);
|
|
149
|
+
slot.vbo = grownV.buffer;
|
|
150
|
+
slot.vboBytes = grownV.bytes;
|
|
151
|
+
device.queue.writeBuffer(
|
|
152
|
+
slot.vbo,
|
|
153
|
+
0,
|
|
154
|
+
slot.interleaved.buffer as ArrayBuffer,
|
|
155
|
+
slot.interleaved.byteOffset,
|
|
156
|
+
vBytes,
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
const iBytes = indexBufferByteSize(indices.byteLength);
|
|
160
|
+
const grownI = createOrGrowBuffer(
|
|
161
|
+
device,
|
|
162
|
+
slot.ibo,
|
|
163
|
+
slot.iboBytes,
|
|
164
|
+
iBytes,
|
|
165
|
+
GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST,
|
|
166
|
+
);
|
|
167
|
+
if (grownI.buffer !== slot.ibo) {
|
|
168
|
+
slot.indicesRef = null;
|
|
169
|
+
}
|
|
170
|
+
slot.ibo = grownI.buffer;
|
|
171
|
+
slot.iboBytes = grownI.bytes;
|
|
172
|
+
if (slot.indicesRef !== indices || slot.uvsRef !== uvs) {
|
|
173
|
+
if (iBytes === indices.byteLength) {
|
|
174
|
+
device.queue.writeBuffer(
|
|
175
|
+
slot.ibo,
|
|
176
|
+
0,
|
|
177
|
+
indices.buffer as ArrayBuffer,
|
|
178
|
+
indices.byteOffset,
|
|
179
|
+
indices.byteLength,
|
|
180
|
+
);
|
|
181
|
+
} else {
|
|
182
|
+
const padded = new Uint16Array(iBytes / 2);
|
|
183
|
+
padded.set(indices);
|
|
184
|
+
device.queue.writeBuffer(slot.ibo, 0, padded);
|
|
185
|
+
}
|
|
186
|
+
slot.indicesRef = indices;
|
|
187
|
+
slot.uvsRef = uvs;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
writeUbo(
|
|
192
|
+
device: GPUDevice,
|
|
193
|
+
slot: WebGpuMeshSlot,
|
|
194
|
+
floats: ArrayLike<number>,
|
|
195
|
+
): void {
|
|
196
|
+
const n = floats.length;
|
|
197
|
+
for (let i = 0; i < n; i++) slot.uboFloats[i] = floats[i]!;
|
|
198
|
+
device.queue.writeBuffer(
|
|
199
|
+
slot.ubo,
|
|
200
|
+
0,
|
|
201
|
+
slot.uboFloats.buffer as ArrayBuffer,
|
|
202
|
+
slot.uboFloats.byteOffset,
|
|
203
|
+
Math.max(16, (n * 4 + 15) & ~15),
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
}
|