@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.
@@ -0,0 +1,1336 @@
1
+ /**
2
+ * WebGPU renderer with soft clipping-mask support (parity with WebGL2).
3
+ *
4
+ * Frame flow: beginFrame creates an encoder; drawMeshes writes all mask
5
+ * contexts into one atlas texture, then one color pass samples per-drawable
6
+ * atlas layout; endFrame submits.
7
+ */
8
+
9
+ import { webGpuBlendState } from "../blend.js";
10
+ import {
11
+ fitClippingContexts,
12
+ type LaidOutClippingContext,
13
+ type MaskLayoutRect,
14
+ maskChannelVec4,
15
+ maskLayoutVec4,
16
+ partitionForClipping,
17
+ } from "../clipping.js";
18
+ import {
19
+ PREVIEW_FILL,
20
+ PREVIEW_STROKE,
21
+ triangleEdgesToLineList,
22
+ } from "../preview-style.js";
23
+ import type {
24
+ DrawableMesh,
25
+ ModelDrawPass,
26
+ TextureData,
27
+ WebGpuRenderer,
28
+ } from "../types.js";
29
+ import { BlendMode } from "../types.js";
30
+
31
+ const SOLID_WGSL = /* wgsl */ `
32
+ struct Uniforms {
33
+ color: vec4f,
34
+ }
35
+
36
+ @group(0) @binding(0) var<uniform> u: Uniforms;
37
+
38
+ @vertex
39
+ fn vs_main(@location(0) pos: vec2f) -> @builtin(position) vec4f {
40
+ return vec4f(pos, 0.0, 1.0);
41
+ }
42
+
43
+ @fragment
44
+ fn fs_main() -> @location(0) vec4f {
45
+ return u.color;
46
+ }
47
+ `;
48
+
49
+ const TEXTURED_WGSL = /* wgsl */ `
50
+ struct Uniforms {
51
+ opacity: f32,
52
+ _pad0: f32,
53
+ _pad1: f32,
54
+ _pad2: f32,
55
+ }
56
+
57
+ @group(0) @binding(0) var<uniform> u: Uniforms;
58
+ @group(0) @binding(1) var samp: sampler;
59
+ @group(0) @binding(2) var tex: texture_2d<f32>;
60
+
61
+ struct VsOut {
62
+ @builtin(position) pos: vec4f,
63
+ @location(0) uv: vec2f,
64
+ }
65
+
66
+ @vertex
67
+ fn vs_main(@location(0) pos: vec2f, @location(1) uv: vec2f) -> VsOut {
68
+ var o: VsOut;
69
+ o.pos = vec4f(pos, 0.0, 1.0);
70
+ o.uv = uv;
71
+ return o;
72
+ }
73
+
74
+ @fragment
75
+ fn fs_main(input: VsOut) -> @location(0) vec4f {
76
+ let c = textureSample(tex, samp, input.uv);
77
+ return vec4f(c.rgb, c.a * u.opacity);
78
+ }
79
+ `;
80
+
81
+ const SOLID_CLIPPED_WGSL = /* wgsl */ `
82
+ struct Uniforms {
83
+ color: vec4f,
84
+ invertMask: f32,
85
+ _pad0: f32,
86
+ _pad1: f32,
87
+ _pad2: f32,
88
+ layout: vec4f,
89
+ channelFlag: vec4f,
90
+ modelBounds: vec4f,
91
+ }
92
+
93
+ @group(0) @binding(0) var<uniform> u: Uniforms;
94
+ @group(0) @binding(1) var samp: sampler;
95
+ @group(0) @binding(2) var maskTex: texture_2d<f32>;
96
+
97
+ struct VsOut {
98
+ @builtin(position) pos: vec4f,
99
+ @location(0) ndc: vec2f,
100
+ }
101
+
102
+ @vertex
103
+ fn vs_main(@location(0) pos: vec2f) -> VsOut {
104
+ var o: VsOut;
105
+ o.pos = vec4f(pos, 0.0, 1.0);
106
+ o.ndc = pos;
107
+ return o;
108
+ }
109
+
110
+ @fragment
111
+ fn fs_main(input: VsOut) -> @location(0) vec4f {
112
+ let local = (input.ndc - u.modelBounds.xy) / max(u.modelBounds.zw, vec2f(1e-6));
113
+ let muv = u.layout.xy + local * u.layout.zw;
114
+ var m = dot(textureSample(maskTex, samp, muv), u.channelFlag);
115
+ if (u.invertMask > 0.5) {
116
+ m = 1.0 - m;
117
+ }
118
+ return vec4f(u.color.rgb * m, u.color.a * m);
119
+ }
120
+ `;
121
+
122
+ const TEXTURED_CLIPPED_WGSL = /* wgsl */ `
123
+ struct Uniforms {
124
+ opacity: f32,
125
+ invertMask: f32,
126
+ _pad0: f32,
127
+ _pad1: f32,
128
+ layout: vec4f,
129
+ channelFlag: vec4f,
130
+ modelBounds: vec4f,
131
+ }
132
+
133
+ @group(0) @binding(0) var<uniform> u: Uniforms;
134
+ @group(0) @binding(1) var samp: sampler;
135
+ @group(0) @binding(2) var tex: texture_2d<f32>;
136
+ @group(0) @binding(3) var maskTex: texture_2d<f32>;
137
+
138
+ struct VsOut {
139
+ @builtin(position) pos: vec4f,
140
+ @location(0) uv: vec2f,
141
+ @location(1) ndc: vec2f,
142
+ }
143
+
144
+ @vertex
145
+ fn vs_main(@location(0) pos: vec2f, @location(1) uv: vec2f) -> VsOut {
146
+ var o: VsOut;
147
+ o.pos = vec4f(pos, 0.0, 1.0);
148
+ o.uv = uv;
149
+ o.ndc = pos;
150
+ return o;
151
+ }
152
+
153
+ @fragment
154
+ fn fs_main(input: VsOut) -> @location(0) vec4f {
155
+ let c = textureSample(tex, samp, input.uv);
156
+ let local = (input.ndc - u.modelBounds.xy) / max(u.modelBounds.zw, vec2f(1e-6));
157
+ let muv = u.layout.xy + local * u.layout.zw;
158
+ var m = dot(textureSample(maskTex, samp, muv), u.channelFlag);
159
+ if (u.invertMask > 0.5) {
160
+ m = 1.0 - m;
161
+ }
162
+ let a = c.a * u.opacity * m;
163
+ return vec4f(c.rgb * m, a);
164
+ }
165
+ `;
166
+
167
+ const MASK_WRITE_WGSL = /* wgsl */ `
168
+ struct Uniforms {
169
+ opacity: f32,
170
+ useTexture: f32,
171
+ _pad0: f32,
172
+ _pad1: f32,
173
+ layout: vec4f, // xy offset, zw size in 0..1
174
+ channelFlag: vec4f,
175
+ modelBounds: vec4f,
176
+ }
177
+
178
+ @group(0) @binding(0) var<uniform> u: Uniforms;
179
+ @group(0) @binding(1) var samp: sampler;
180
+ @group(0) @binding(2) var tex: texture_2d<f32>;
181
+
182
+ struct VsOut {
183
+ @builtin(position) pos: vec4f,
184
+ @location(0) uv: vec2f,
185
+ }
186
+
187
+ @vertex
188
+ fn vs_main(@location(0) pos: vec2f, @location(1) uv: vec2f) -> VsOut {
189
+ var o: VsOut;
190
+ let local = (pos - u.modelBounds.xy) / max(u.modelBounds.zw, vec2f(1e-6));
191
+ let atlasUv = u.layout.xy + local * u.layout.zw;
192
+ o.pos = vec4f(atlasUv * 2.0 - vec2f(1.0, 1.0), 0.0, 1.0);
193
+ o.uv = uv;
194
+ return o;
195
+ }
196
+
197
+ @fragment
198
+ fn fs_main(input: VsOut) -> @location(0) vec4f {
199
+ var a = u.opacity;
200
+ if (u.useTexture > 0.5) {
201
+ a = a * textureSample(tex, samp, input.uv).a;
202
+ }
203
+ return u.channelFlag * a;
204
+ }
205
+ `;
206
+
207
+ interface WebGpuPipelineSet {
208
+ fill: GPURenderPipeline;
209
+ line: GPURenderPipeline;
210
+ textured: GPURenderPipeline;
211
+ clippedFill: GPURenderPipeline;
212
+ clippedTextured: GPURenderPipeline;
213
+ }
214
+
215
+ const BLEND_MODES: BlendMode[] = [
216
+ BlendMode.Normal,
217
+ BlendMode.Additive,
218
+ BlendMode.Multiplicative,
219
+ ];
220
+
221
+ /** WebGPU INDEX buffers must be a multiple of 4 bytes. */
222
+ function indexBufferSize(byteLength: number): number {
223
+ return (byteLength + 3) & ~3;
224
+ }
225
+
226
+ function writeIndexBuffer(device: GPUDevice, indices: Uint16Array): GPUBuffer {
227
+ const size = indexBufferSize(indices.byteLength);
228
+ const ibo = device.createBuffer({
229
+ size: Math.max(4, size),
230
+ usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST,
231
+ mappedAtCreation: true,
232
+ });
233
+ new Uint16Array(ibo.getMappedRange()).set(indices);
234
+ ibo.unmap();
235
+ return ibo;
236
+ }
237
+
238
+ function interleavePosUv(
239
+ positions: Float32Array,
240
+ uvs: Float32Array,
241
+ ): Float32Array {
242
+ const n = Math.floor(positions.length / 2);
243
+ const out = new Float32Array(n * 4);
244
+ for (let i = 0; i < n; i++) {
245
+ const o = i * 4;
246
+ const p = i * 2;
247
+ out[o] = positions[p]!;
248
+ out[o + 1] = positions[p + 1]!;
249
+ out[o + 2] = uvs[p] ?? 0;
250
+ out[o + 3] = uvs[p + 1] ?? 0;
251
+ }
252
+ return out;
253
+ }
254
+
255
+ /** 1x1 white texture used when a mask mesh has no model texture. */
256
+ function createWhiteTexture(device: GPUDevice): GPUTexture {
257
+ const tex = device.createTexture({
258
+ size: [1, 1],
259
+ format: "rgba8unorm",
260
+ usage:
261
+ GPUTextureUsage.TEXTURE_BINDING |
262
+ GPUTextureUsage.COPY_DST |
263
+ GPUTextureUsage.RENDER_ATTACHMENT,
264
+ });
265
+ device.queue.writeTexture(
266
+ { texture: tex },
267
+ new Uint8Array([255, 255, 255, 255]),
268
+ { bytesPerRow: 4 },
269
+ [1, 1],
270
+ );
271
+ return tex;
272
+ }
273
+
274
+ class WebGpuModelDrawPass implements ModelDrawPass {
275
+ readonly #renderer: WebGpuRendererImpl;
276
+
277
+ constructor(renderer: WebGpuRendererImpl) {
278
+ this.#renderer = renderer;
279
+ }
280
+
281
+ setTextures(textures: TextureData[]): void {
282
+ this.#renderer.setTextures(textures);
283
+ }
284
+
285
+ draw(drawables: DrawableMesh[], _modelMatrix: Float32Array): void {
286
+ this.#renderer.drawMeshes(drawables);
287
+ }
288
+
289
+ destroy(): void {
290
+ this.#renderer.setTextures([]);
291
+ }
292
+ }
293
+
294
+ export interface WebGpuRendererOptions {
295
+ /** Prefer high-performance adapter when available. */
296
+ powerPreference?: GPUPowerPreference;
297
+ }
298
+
299
+ /** WebGPU renderer. */
300
+ export class WebGpuRendererImpl implements WebGpuRenderer {
301
+ readonly kind = "webgpu" as const;
302
+
303
+ #canvas: HTMLCanvasElement | null = null;
304
+ #device: GPUDevice | null = null;
305
+ #context: GPUCanvasContext | null = null;
306
+ #format: GPUTextureFormat | null = null;
307
+ #pipelines: (WebGpuPipelineSet | null)[] = [null, null, null];
308
+ #solidBindGroupLayout: GPUBindGroupLayout | null = null;
309
+ #texturedBindGroupLayout: GPUBindGroupLayout | null = null;
310
+ #clippedSolidBindGroupLayout: GPUBindGroupLayout | null = null;
311
+ #clippedTexturedBindGroupLayout: GPUBindGroupLayout | null = null;
312
+ #maskWriteBindGroupLayout: GPUBindGroupLayout | null = null;
313
+ #maskWritePipeline: GPURenderPipeline | null = null;
314
+ #sampler: GPUSampler | null = null;
315
+ #encoder: GPUCommandEncoder | null = null;
316
+ #pass: GPURenderPassEncoder | null = null;
317
+ #swapView: GPUTextureView | null = null;
318
+ #pendingClear = false;
319
+ #msaaTexture: GPUTexture | null = null;
320
+ #msaaW = 0;
321
+ #msaaH = 0;
322
+ #sampleCount = 4;
323
+ #maskTexture: GPUTexture | null = null;
324
+ #maskW = 0;
325
+ #maskH = 0;
326
+ #whiteTexture: GPUTexture | null = null;
327
+ #transient: GPUBuffer[] = [];
328
+ #gpuTextures: (GPUTexture | null)[] = [];
329
+ readonly #options: WebGpuRendererOptions;
330
+
331
+ constructor(options: WebGpuRendererOptions = {}) {
332
+ this.#options = options;
333
+ }
334
+
335
+ async initialize(canvas: HTMLCanvasElement): Promise<void> {
336
+ if (!navigator.gpu) {
337
+ throw new Error(
338
+ "@doki-land/live2d-renderer: WebGPU is not available",
339
+ );
340
+ }
341
+ const adapter = await navigator.gpu.requestAdapter({
342
+ powerPreference:
343
+ this.#options.powerPreference ?? "high-performance",
344
+ });
345
+ if (!adapter) {
346
+ throw new Error("@doki-land/live2d-renderer: no WebGPU adapter");
347
+ }
348
+ const device = await adapter.requestDevice();
349
+ const context = canvas.getContext("webgpu");
350
+ if (!context) {
351
+ throw new Error(
352
+ "@doki-land/live2d-renderer: failed to get webgpu context",
353
+ );
354
+ }
355
+ const format = navigator.gpu.getPreferredCanvasFormat();
356
+ context.configure({
357
+ device,
358
+ format,
359
+ alphaMode: "premultiplied",
360
+ });
361
+
362
+ const solidModule = device.createShaderModule({ code: SOLID_WGSL });
363
+ const texturedModule = device.createShaderModule({
364
+ code: TEXTURED_WGSL,
365
+ });
366
+ const solidClippedModule = device.createShaderModule({
367
+ code: SOLID_CLIPPED_WGSL,
368
+ });
369
+ const texturedClippedModule = device.createShaderModule({
370
+ code: TEXTURED_CLIPPED_WGSL,
371
+ });
372
+ const maskWriteModule = device.createShaderModule({
373
+ code: MASK_WRITE_WGSL,
374
+ });
375
+
376
+ const solidBindGroupLayout = device.createBindGroupLayout({
377
+ entries: [
378
+ {
379
+ binding: 0,
380
+ visibility: GPUShaderStage.FRAGMENT,
381
+ buffer: { type: "uniform" },
382
+ },
383
+ ],
384
+ });
385
+ const texturedBindGroupLayout = device.createBindGroupLayout({
386
+ entries: [
387
+ {
388
+ binding: 0,
389
+ visibility: GPUShaderStage.FRAGMENT,
390
+ buffer: { type: "uniform" },
391
+ },
392
+ {
393
+ binding: 1,
394
+ visibility: GPUShaderStage.FRAGMENT,
395
+ sampler: { type: "filtering" },
396
+ },
397
+ {
398
+ binding: 2,
399
+ visibility: GPUShaderStage.FRAGMENT,
400
+ texture: { sampleType: "float" },
401
+ },
402
+ ],
403
+ });
404
+ const clippedSolidBindGroupLayout = device.createBindGroupLayout({
405
+ entries: [
406
+ {
407
+ binding: 0,
408
+ visibility: GPUShaderStage.FRAGMENT,
409
+ buffer: { type: "uniform" },
410
+ },
411
+ {
412
+ binding: 1,
413
+ visibility: GPUShaderStage.FRAGMENT,
414
+ sampler: { type: "filtering" },
415
+ },
416
+ {
417
+ binding: 2,
418
+ visibility: GPUShaderStage.FRAGMENT,
419
+ texture: { sampleType: "float" },
420
+ },
421
+ ],
422
+ });
423
+ const clippedTexturedBindGroupLayout = device.createBindGroupLayout({
424
+ entries: [
425
+ {
426
+ binding: 0,
427
+ visibility: GPUShaderStage.FRAGMENT,
428
+ buffer: { type: "uniform" },
429
+ },
430
+ {
431
+ binding: 1,
432
+ visibility: GPUShaderStage.FRAGMENT,
433
+ sampler: { type: "filtering" },
434
+ },
435
+ {
436
+ binding: 2,
437
+ visibility: GPUShaderStage.FRAGMENT,
438
+ texture: { sampleType: "float" },
439
+ },
440
+ {
441
+ binding: 3,
442
+ visibility: GPUShaderStage.FRAGMENT,
443
+ texture: { sampleType: "float" },
444
+ },
445
+ ],
446
+ });
447
+ const maskWriteBindGroupLayout = device.createBindGroupLayout({
448
+ entries: [
449
+ {
450
+ binding: 0,
451
+ visibility: GPUShaderStage.FRAGMENT,
452
+ buffer: { type: "uniform" },
453
+ },
454
+ {
455
+ binding: 1,
456
+ visibility: GPUShaderStage.FRAGMENT,
457
+ sampler: { type: "filtering" },
458
+ },
459
+ {
460
+ binding: 2,
461
+ visibility: GPUShaderStage.FRAGMENT,
462
+ texture: { sampleType: "float" },
463
+ },
464
+ ],
465
+ });
466
+
467
+ const solidLayout = device.createPipelineLayout({
468
+ bindGroupLayouts: [solidBindGroupLayout],
469
+ });
470
+ const texturedLayout = device.createPipelineLayout({
471
+ bindGroupLayouts: [texturedBindGroupLayout],
472
+ });
473
+ const clippedSolidLayout = device.createPipelineLayout({
474
+ bindGroupLayouts: [clippedSolidBindGroupLayout],
475
+ });
476
+ const clippedTexturedLayout = device.createPipelineLayout({
477
+ bindGroupLayouts: [clippedTexturedBindGroupLayout],
478
+ });
479
+ const maskWriteLayout = device.createPipelineLayout({
480
+ bindGroupLayouts: [maskWriteBindGroupLayout],
481
+ });
482
+
483
+ const solidVertex: GPUVertexState = {
484
+ module: solidModule,
485
+ entryPoint: "vs_main",
486
+ buffers: [
487
+ {
488
+ arrayStride: 8,
489
+ attributes: [
490
+ {
491
+ shaderLocation: 0,
492
+ offset: 0,
493
+ format: "float32x2",
494
+ },
495
+ ],
496
+ },
497
+ ],
498
+ };
499
+ const texturedVertex: GPUVertexState = {
500
+ module: texturedModule,
501
+ entryPoint: "vs_main",
502
+ buffers: [
503
+ {
504
+ arrayStride: 16,
505
+ attributes: [
506
+ {
507
+ shaderLocation: 0,
508
+ offset: 0,
509
+ format: "float32x2",
510
+ },
511
+ {
512
+ shaderLocation: 1,
513
+ offset: 8,
514
+ format: "float32x2",
515
+ },
516
+ ],
517
+ },
518
+ ],
519
+ };
520
+ const solidClippedVertex: GPUVertexState = {
521
+ module: solidClippedModule,
522
+ entryPoint: "vs_main",
523
+ buffers: [
524
+ {
525
+ arrayStride: 8,
526
+ attributes: [
527
+ {
528
+ shaderLocation: 0,
529
+ offset: 0,
530
+ format: "float32x2",
531
+ },
532
+ ],
533
+ },
534
+ ],
535
+ };
536
+ const texturedClippedVertex: GPUVertexState = {
537
+ module: texturedClippedModule,
538
+ entryPoint: "vs_main",
539
+ buffers: [
540
+ {
541
+ arrayStride: 16,
542
+ attributes: [
543
+ {
544
+ shaderLocation: 0,
545
+ offset: 0,
546
+ format: "float32x2",
547
+ },
548
+ {
549
+ shaderLocation: 1,
550
+ offset: 8,
551
+ format: "float32x2",
552
+ },
553
+ ],
554
+ },
555
+ ],
556
+ };
557
+ const maskWriteVertex: GPUVertexState = {
558
+ module: maskWriteModule,
559
+ entryPoint: "vs_main",
560
+ buffers: [
561
+ {
562
+ arrayStride: 16,
563
+ attributes: [
564
+ {
565
+ shaderLocation: 0,
566
+ offset: 0,
567
+ format: "float32x2",
568
+ },
569
+ {
570
+ shaderLocation: 1,
571
+ offset: 8,
572
+ format: "float32x2",
573
+ },
574
+ ],
575
+ },
576
+ ],
577
+ };
578
+
579
+ const maskAccumulateBlend: GPUBlendState = {
580
+ color: {
581
+ srcFactor: "one",
582
+ dstFactor: "one",
583
+ operation: "add",
584
+ },
585
+ alpha: {
586
+ srcFactor: "one",
587
+ dstFactor: "one",
588
+ operation: "add",
589
+ },
590
+ };
591
+
592
+ const makeSet = (
593
+ sampleCount: number,
594
+ mode: BlendMode,
595
+ ): WebGpuPipelineSet => {
596
+ const blend = webGpuBlendState(mode);
597
+ const multisample: GPUMultisampleState = { count: sampleCount };
598
+ return {
599
+ fill: device.createRenderPipeline({
600
+ layout: solidLayout,
601
+ vertex: solidVertex,
602
+ fragment: {
603
+ module: solidModule,
604
+ entryPoint: "fs_main",
605
+ targets: [{ format, blend }],
606
+ },
607
+ primitive: { topology: "triangle-list" },
608
+ multisample,
609
+ }),
610
+ line: device.createRenderPipeline({
611
+ layout: solidLayout,
612
+ vertex: solidVertex,
613
+ fragment: {
614
+ module: solidModule,
615
+ entryPoint: "fs_main",
616
+ targets: [{ format, blend }],
617
+ },
618
+ primitive: { topology: "line-list" },
619
+ multisample,
620
+ }),
621
+ textured: device.createRenderPipeline({
622
+ layout: texturedLayout,
623
+ vertex: texturedVertex,
624
+ fragment: {
625
+ module: texturedModule,
626
+ entryPoint: "fs_main",
627
+ targets: [{ format, blend }],
628
+ },
629
+ primitive: { topology: "triangle-list" },
630
+ multisample,
631
+ }),
632
+ clippedFill: device.createRenderPipeline({
633
+ layout: clippedSolidLayout,
634
+ vertex: solidClippedVertex,
635
+ fragment: {
636
+ module: solidClippedModule,
637
+ entryPoint: "fs_main",
638
+ targets: [{ format, blend }],
639
+ },
640
+ primitive: { topology: "triangle-list" },
641
+ multisample,
642
+ }),
643
+ clippedTextured: device.createRenderPipeline({
644
+ layout: clippedTexturedLayout,
645
+ vertex: texturedClippedVertex,
646
+ fragment: {
647
+ module: texturedClippedModule,
648
+ entryPoint: "fs_main",
649
+ targets: [{ format, blend }],
650
+ },
651
+ primitive: { topology: "triangle-list" },
652
+ multisample,
653
+ }),
654
+ };
655
+ };
656
+
657
+ let sampleCount = 4;
658
+ let pipelines: WebGpuPipelineSet[];
659
+ let maskWritePipeline: GPURenderPipeline;
660
+ try {
661
+ pipelines = BLEND_MODES.map((mode) => makeSet(sampleCount, mode));
662
+ maskWritePipeline = device.createRenderPipeline({
663
+ layout: maskWriteLayout,
664
+ vertex: maskWriteVertex,
665
+ fragment: {
666
+ module: maskWriteModule,
667
+ entryPoint: "fs_main",
668
+ targets: [
669
+ { format: "rgba8unorm", blend: maskAccumulateBlend },
670
+ ],
671
+ },
672
+ primitive: { topology: "triangle-list" },
673
+ multisample: { count: 1 },
674
+ });
675
+ } catch {
676
+ sampleCount = 1;
677
+ pipelines = BLEND_MODES.map((mode) => makeSet(sampleCount, mode));
678
+ maskWritePipeline = device.createRenderPipeline({
679
+ layout: maskWriteLayout,
680
+ vertex: maskWriteVertex,
681
+ fragment: {
682
+ module: maskWriteModule,
683
+ entryPoint: "fs_main",
684
+ targets: [
685
+ { format: "rgba8unorm", blend: maskAccumulateBlend },
686
+ ],
687
+ },
688
+ primitive: { topology: "triangle-list" },
689
+ multisample: { count: 1 },
690
+ });
691
+ }
692
+
693
+ this.#sampleCount = sampleCount;
694
+ this.#canvas = canvas;
695
+ this.#device = device;
696
+ this.#context = context;
697
+ this.#format = format;
698
+ this.#pipelines = pipelines;
699
+ this.#solidBindGroupLayout = solidBindGroupLayout;
700
+ this.#texturedBindGroupLayout = texturedBindGroupLayout;
701
+ this.#clippedSolidBindGroupLayout = clippedSolidBindGroupLayout;
702
+ this.#clippedTexturedBindGroupLayout = clippedTexturedBindGroupLayout;
703
+ this.#maskWriteBindGroupLayout = maskWriteBindGroupLayout;
704
+ this.#maskWritePipeline = maskWritePipeline;
705
+ this.#sampler = device.createSampler({
706
+ magFilter: "linear",
707
+ minFilter: "linear",
708
+ addressModeU: "clamp-to-edge",
709
+ addressModeV: "clamp-to-edge",
710
+ });
711
+ this.#whiteTexture = createWhiteTexture(device);
712
+ this.#ensureMsaa();
713
+ }
714
+
715
+ #ensureMsaa(): void {
716
+ const device = this.#device;
717
+ const canvas = this.#canvas;
718
+ const format = this.#format;
719
+ if (!device || !canvas || !format) return;
720
+ const w = Math.max(1, canvas.width);
721
+ const h = Math.max(1, canvas.height);
722
+ if (this.#msaaTexture && this.#msaaW === w && this.#msaaH === h) {
723
+ return;
724
+ }
725
+ this.#msaaTexture?.destroy();
726
+ this.#msaaTexture = device.createTexture({
727
+ size: [w, h],
728
+ sampleCount: this.#sampleCount,
729
+ format,
730
+ usage: GPUTextureUsage.RENDER_ATTACHMENT,
731
+ });
732
+ this.#msaaW = w;
733
+ this.#msaaH = h;
734
+ }
735
+
736
+ #ensureMaskTexture(width: number, height: number): GPUTexture | null {
737
+ const device = this.#device;
738
+ if (!device) return null;
739
+ const w = Math.max(1, width);
740
+ const h = Math.max(1, height);
741
+ if (this.#maskTexture && this.#maskW === w && this.#maskH === h) {
742
+ return this.#maskTexture;
743
+ }
744
+ this.#maskTexture?.destroy();
745
+ this.#maskTexture = device.createTexture({
746
+ size: [w, h],
747
+ format: "rgba8unorm",
748
+ usage:
749
+ GPUTextureUsage.RENDER_ATTACHMENT |
750
+ GPUTextureUsage.TEXTURE_BINDING,
751
+ });
752
+ this.#maskW = w;
753
+ this.#maskH = h;
754
+ return this.#maskTexture;
755
+ }
756
+
757
+ #clearGpuTextures(): void {
758
+ for (const t of this.#gpuTextures) t?.destroy();
759
+ this.#gpuTextures = [];
760
+ }
761
+
762
+ setTextures(textures: TextureData[]): void {
763
+ const device = this.#device;
764
+ this.#clearGpuTextures();
765
+ if (!device) return;
766
+
767
+ let maxIndex = -1;
768
+ for (const t of textures) maxIndex = Math.max(maxIndex, t.index);
769
+ this.#gpuTextures = new Array(Math.max(0, maxIndex + 1)).fill(null);
770
+
771
+ for (const t of textures) {
772
+ const gpuTex = device.createTexture({
773
+ size: [t.width, t.height],
774
+ format: "rgba8unorm",
775
+ usage:
776
+ GPUTextureUsage.TEXTURE_BINDING |
777
+ GPUTextureUsage.COPY_DST |
778
+ GPUTextureUsage.RENDER_ATTACHMENT,
779
+ });
780
+ device.queue.copyExternalImageToTexture(
781
+ { source: t.image },
782
+ { texture: gpuTex },
783
+ [t.width, t.height],
784
+ );
785
+ this.#gpuTextures[t.index] = gpuTex;
786
+ }
787
+ }
788
+
789
+ createModelDrawPass(): ModelDrawPass {
790
+ if (!this.#device || !this.#pipelines[BlendMode.Normal]) {
791
+ throw new Error(
792
+ "@doki-land/live2d-renderer: WebGPU renderer not initialized",
793
+ );
794
+ }
795
+ return new WebGpuModelDrawPass(this);
796
+ }
797
+
798
+ beginFrame(): void {
799
+ const device = this.#device;
800
+ const context = this.#context;
801
+ if (!device || !context) return;
802
+
803
+ this.#ensureMsaa();
804
+ this.#encoder = device.createCommandEncoder();
805
+ this.#swapView = context.getCurrentTexture().createView();
806
+ this.#pass = null;
807
+ this.#pendingClear = true;
808
+ }
809
+
810
+ #endColorPass(): void {
811
+ if (this.#pass) {
812
+ this.#pass.end();
813
+ this.#pass = null;
814
+ }
815
+ }
816
+
817
+ #beginColorPass(): GPURenderPassEncoder | null {
818
+ const encoder = this.#encoder;
819
+ const msaa = this.#msaaTexture;
820
+ const swapView = this.#swapView;
821
+ if (!encoder || !swapView) return null;
822
+ if (this.#pass) return this.#pass;
823
+
824
+ const loadOp: GPULoadOp = this.#pendingClear ? "clear" : "load";
825
+ this.#pendingClear = false;
826
+ const useMsaa = this.#sampleCount > 1 && msaa !== null;
827
+
828
+ this.#pass = encoder.beginRenderPass({
829
+ colorAttachments: [
830
+ useMsaa
831
+ ? {
832
+ view: msaa?.createView(),
833
+ resolveTarget: swapView,
834
+ clearValue: { r: 0, g: 0, b: 0, a: 0 },
835
+ loadOp,
836
+ storeOp: "discard",
837
+ }
838
+ : {
839
+ view: swapView,
840
+ clearValue: { r: 0, g: 0, b: 0, a: 0 },
841
+ loadOp,
842
+ storeOp: "store",
843
+ },
844
+ ],
845
+ });
846
+ return this.#pass;
847
+ }
848
+
849
+ drawMeshes(drawables: DrawableMesh[]): void {
850
+ const device = this.#device;
851
+ const encoder = this.#encoder;
852
+ const canvas = this.#canvas;
853
+ const sampler = this.#sampler;
854
+ const solidLayout = this.#solidBindGroupLayout;
855
+ const texturedLayout = this.#texturedBindGroupLayout;
856
+ const clippedSolidLayout = this.#clippedSolidBindGroupLayout;
857
+ const clippedTexturedLayout = this.#clippedTexturedBindGroupLayout;
858
+ const maskWriteLayout = this.#maskWriteBindGroupLayout;
859
+ const maskWritePipeline = this.#maskWritePipeline;
860
+ const whiteTex = this.#whiteTexture;
861
+ if (
862
+ !device ||
863
+ !encoder ||
864
+ !canvas ||
865
+ !sampler ||
866
+ !solidLayout ||
867
+ !texturedLayout ||
868
+ !clippedSolidLayout ||
869
+ !clippedTexturedLayout ||
870
+ !maskWriteLayout ||
871
+ !maskWritePipeline ||
872
+ !whiteTex
873
+ ) {
874
+ return;
875
+ }
876
+
877
+ const byIndex = new Map<number, DrawableMesh>();
878
+ for (const d of drawables) byIndex.set(d.index, d);
879
+
880
+ const partitioned = partitionForClipping(drawables);
881
+ const contexts = fitClippingContexts(partitioned.contexts, byIndex);
882
+ const { maskOnly } = partitioned;
883
+
884
+ let maskTex: GPUTexture | null = null;
885
+ if (contexts.length > 0) {
886
+ maskTex = this.#ensureMaskTexture(canvas.width, canvas.height);
887
+ if (maskTex) {
888
+ const maskPass = encoder.beginRenderPass({
889
+ colorAttachments: [
890
+ {
891
+ view: maskTex.createView(),
892
+ clearValue: { r: 0, g: 0, b: 0, a: 0 },
893
+ loadOp: "clear",
894
+ storeOp: "store",
895
+ },
896
+ ],
897
+ });
898
+ maskPass.setPipeline(maskWritePipeline);
899
+ for (const ctx of contexts) {
900
+ for (const mi of ctx.maskIndices) {
901
+ const maskMesh = byIndex.get(mi);
902
+ if (maskMesh) {
903
+ this.#drawMaskMesh(
904
+ maskMesh,
905
+ maskPass,
906
+ maskWriteLayout,
907
+ sampler,
908
+ whiteTex,
909
+ ctx.layout,
910
+ ctx.modelBounds,
911
+ ctx.channelFlag,
912
+ );
913
+ }
914
+ }
915
+ }
916
+ maskPass.end();
917
+ }
918
+ }
919
+
920
+ const colorPass = this.#beginColorPass();
921
+ if (!colorPass) return;
922
+
923
+ const layoutByClippedIndex = new Map<
924
+ number,
925
+ Pick<
926
+ LaidOutClippingContext,
927
+ "layout" | "modelBounds" | "invertedMask" | "channelFlag"
928
+ >
929
+ >();
930
+ for (const ctx of contexts) {
931
+ for (const ci of ctx.clippedIndices) {
932
+ layoutByClippedIndex.set(ci, {
933
+ layout: ctx.layout,
934
+ modelBounds: ctx.modelBounds,
935
+ invertedMask: ctx.invertedMask,
936
+ channelFlag: ctx.channelFlag,
937
+ });
938
+ }
939
+ }
940
+
941
+ for (const d of drawables) {
942
+ if (maskOnly.has(d.index)) continue;
943
+ const clip = layoutByClippedIndex.get(d.index);
944
+ if (clip && maskTex) {
945
+ this.#drawColorMesh(
946
+ d,
947
+ colorPass,
948
+ true,
949
+ clip.invertedMask,
950
+ maskTex,
951
+ clip.layout,
952
+ clip.channelFlag,
953
+ clip.modelBounds,
954
+ );
955
+ } else {
956
+ this.#drawColorMesh(d, colorPass, false, false);
957
+ }
958
+ }
959
+ }
960
+
961
+ #drawMaskMesh(
962
+ d: DrawableMesh,
963
+ pass: GPURenderPassEncoder,
964
+ layout: GPUBindGroupLayout,
965
+ sampler: GPUSampler,
966
+ whiteTex: GPUTexture,
967
+ atlasLayout: MaskLayoutRect,
968
+ modelBounds: MaskLayoutRect,
969
+ channelFlag: LaidOutClippingContext["channelFlag"],
970
+ ): void {
971
+ const device = this.#device;
972
+ if (!device || d.opacity <= 0) return;
973
+
974
+ const interleaved = interleavePosUv(d.vertexPositions, d.uvs);
975
+ const vbo = device.createBuffer({
976
+ size: interleaved.byteLength,
977
+ usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
978
+ mappedAtCreation: true,
979
+ });
980
+ new Float32Array(vbo.getMappedRange()).set(interleaved);
981
+ vbo.unmap();
982
+
983
+ const gpuTex = this.#gpuTextures[d.textureIndex] ?? whiteTex;
984
+ const ubo = device.createBuffer({
985
+ size: 64,
986
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
987
+ mappedAtCreation: true,
988
+ });
989
+ const layoutVec = maskLayoutVec4(atlasLayout);
990
+ const ch = maskChannelVec4(channelFlag);
991
+ const boundsVec = maskLayoutVec4(modelBounds);
992
+ new Float32Array(ubo.getMappedRange()).set([
993
+ Math.max(d.opacity, 1),
994
+ this.#gpuTextures[d.textureIndex] ? 1 : 0,
995
+ 0,
996
+ 0,
997
+ layoutVec[0]!,
998
+ layoutVec[1]!,
999
+ layoutVec[2]!,
1000
+ layoutVec[3]!,
1001
+ ch[0]!,
1002
+ ch[1]!,
1003
+ ch[2]!,
1004
+ ch[3]!,
1005
+ boundsVec[0]!,
1006
+ boundsVec[1]!,
1007
+ boundsVec[2]!,
1008
+ boundsVec[3]!,
1009
+ ]);
1010
+ ubo.unmap();
1011
+
1012
+ const ibo = writeIndexBuffer(device, d.indices);
1013
+ pass.setBindGroup(
1014
+ 0,
1015
+ device.createBindGroup({
1016
+ layout,
1017
+ entries: [
1018
+ { binding: 0, resource: { buffer: ubo } },
1019
+ { binding: 1, resource: sampler },
1020
+ { binding: 2, resource: gpuTex.createView() },
1021
+ ],
1022
+ }),
1023
+ );
1024
+ pass.setVertexBuffer(0, vbo);
1025
+ pass.setIndexBuffer(ibo, "uint16");
1026
+ pass.drawIndexed(d.indices.length);
1027
+ this.#transient.push(vbo, ubo, ibo);
1028
+ }
1029
+
1030
+ #drawColorMesh(
1031
+ d: DrawableMesh,
1032
+ pass: GPURenderPassEncoder,
1033
+ useMask: boolean,
1034
+ invertMask: boolean,
1035
+ maskTex?: GPUTexture,
1036
+ atlasLayout?: MaskLayoutRect,
1037
+ channelFlag?: LaidOutClippingContext["channelFlag"],
1038
+ modelBounds?: MaskLayoutRect,
1039
+ ): void {
1040
+ const device = this.#device;
1041
+ const sampler = this.#sampler;
1042
+ const solidLayout = this.#solidBindGroupLayout;
1043
+ const texturedLayout = this.#texturedBindGroupLayout;
1044
+ const clippedSolidLayout = this.#clippedSolidBindGroupLayout;
1045
+ const clippedTexturedLayout = this.#clippedTexturedBindGroupLayout;
1046
+ if (
1047
+ !device ||
1048
+ !sampler ||
1049
+ !solidLayout ||
1050
+ !texturedLayout ||
1051
+ !clippedSolidLayout ||
1052
+ !clippedTexturedLayout
1053
+ ) {
1054
+ return;
1055
+ }
1056
+ if (!d.visible || d.opacity <= 0) return;
1057
+
1058
+ const set =
1059
+ this.#pipelines[d.blendMode] ?? this.#pipelines[BlendMode.Normal];
1060
+ if (!set) return;
1061
+
1062
+ const layoutVec = maskLayoutVec4(
1063
+ atlasLayout ?? { x: 0, y: 0, width: 1, height: 1 },
1064
+ );
1065
+ const ch = maskChannelVec4(channelFlag ?? [0, 0, 0, 1]);
1066
+ const boundsVec = maskLayoutVec4(
1067
+ modelBounds ?? { x: -1, y: -1, width: 2, height: 2 },
1068
+ );
1069
+
1070
+ const gpuTex = this.#gpuTextures[d.textureIndex] ?? null;
1071
+ if (gpuTex) {
1072
+ const interleaved = interleavePosUv(d.vertexPositions, d.uvs);
1073
+ const vbo = device.createBuffer({
1074
+ size: interleaved.byteLength,
1075
+ usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
1076
+ mappedAtCreation: true,
1077
+ });
1078
+ new Float32Array(vbo.getMappedRange()).set(interleaved);
1079
+ vbo.unmap();
1080
+
1081
+ const uboSize = useMask && maskTex ? 64 : 16;
1082
+ const ubo = device.createBuffer({
1083
+ size: uboSize,
1084
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
1085
+ mappedAtCreation: true,
1086
+ });
1087
+ if (useMask && maskTex) {
1088
+ new Float32Array(ubo.getMappedRange()).set([
1089
+ d.opacity,
1090
+ invertMask ? 1 : 0,
1091
+ 0,
1092
+ 0,
1093
+ layoutVec[0]!,
1094
+ layoutVec[1]!,
1095
+ layoutVec[2]!,
1096
+ layoutVec[3]!,
1097
+ ch[0]!,
1098
+ ch[1]!,
1099
+ ch[2]!,
1100
+ ch[3]!,
1101
+ boundsVec[0]!,
1102
+ boundsVec[1]!,
1103
+ boundsVec[2]!,
1104
+ boundsVec[3]!,
1105
+ ]);
1106
+ } else {
1107
+ new Float32Array(ubo.getMappedRange()).set([
1108
+ d.opacity,
1109
+ 0,
1110
+ 0,
1111
+ 0,
1112
+ ]);
1113
+ }
1114
+ ubo.unmap();
1115
+
1116
+ const ibo = writeIndexBuffer(device, d.indices);
1117
+ if (useMask && maskTex) {
1118
+ pass.setPipeline(set.clippedTextured);
1119
+ pass.setBindGroup(
1120
+ 0,
1121
+ device.createBindGroup({
1122
+ layout: clippedTexturedLayout,
1123
+ entries: [
1124
+ { binding: 0, resource: { buffer: ubo } },
1125
+ { binding: 1, resource: sampler },
1126
+ { binding: 2, resource: gpuTex.createView() },
1127
+ { binding: 3, resource: maskTex.createView() },
1128
+ ],
1129
+ }),
1130
+ );
1131
+ } else {
1132
+ pass.setPipeline(set.textured);
1133
+ pass.setBindGroup(
1134
+ 0,
1135
+ device.createBindGroup({
1136
+ layout: texturedLayout,
1137
+ entries: [
1138
+ { binding: 0, resource: { buffer: ubo } },
1139
+ { binding: 1, resource: sampler },
1140
+ { binding: 2, resource: gpuTex.createView() },
1141
+ ],
1142
+ }),
1143
+ );
1144
+ }
1145
+ pass.setVertexBuffer(0, vbo);
1146
+ pass.setIndexBuffer(ibo, "uint16");
1147
+ pass.drawIndexed(d.indices.length);
1148
+ this.#transient.push(vbo, ubo, ibo);
1149
+ return;
1150
+ }
1151
+
1152
+ const vbo = device.createBuffer({
1153
+ size: d.vertexPositions.byteLength,
1154
+ usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
1155
+ mappedAtCreation: true,
1156
+ });
1157
+ new Float32Array(vbo.getMappedRange()).set(d.vertexPositions);
1158
+ vbo.unmap();
1159
+
1160
+ if (useMask && maskTex) {
1161
+ const ubo = device.createBuffer({
1162
+ size: 80,
1163
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
1164
+ mappedAtCreation: true,
1165
+ });
1166
+ new Float32Array(ubo.getMappedRange()).set([
1167
+ PREVIEW_FILL.r,
1168
+ PREVIEW_FILL.g,
1169
+ PREVIEW_FILL.b,
1170
+ PREVIEW_FILL.a * d.opacity,
1171
+ invertMask ? 1 : 0,
1172
+ 0,
1173
+ 0,
1174
+ 0,
1175
+ layoutVec[0]!,
1176
+ layoutVec[1]!,
1177
+ layoutVec[2]!,
1178
+ layoutVec[3]!,
1179
+ ch[0]!,
1180
+ ch[1]!,
1181
+ ch[2]!,
1182
+ ch[3]!,
1183
+ boundsVec[0]!,
1184
+ boundsVec[1]!,
1185
+ boundsVec[2]!,
1186
+ boundsVec[3]!,
1187
+ ]);
1188
+ ubo.unmap();
1189
+ const ibo = writeIndexBuffer(device, d.indices);
1190
+ pass.setPipeline(set.clippedFill);
1191
+ pass.setBindGroup(
1192
+ 0,
1193
+ device.createBindGroup({
1194
+ layout: clippedSolidLayout,
1195
+ entries: [
1196
+ { binding: 0, resource: { buffer: ubo } },
1197
+ { binding: 1, resource: sampler },
1198
+ { binding: 2, resource: maskTex.createView() },
1199
+ ],
1200
+ }),
1201
+ );
1202
+ pass.setVertexBuffer(0, vbo);
1203
+ pass.setIndexBuffer(ibo, "uint16");
1204
+ pass.drawIndexed(d.indices.length);
1205
+ this.#transient.push(vbo, ubo, ibo);
1206
+ return;
1207
+ }
1208
+
1209
+ const fillUbo = this.#makeColorUbo(
1210
+ device,
1211
+ PREVIEW_FILL.r,
1212
+ PREVIEW_FILL.g,
1213
+ PREVIEW_FILL.b,
1214
+ PREVIEW_FILL.a * d.opacity,
1215
+ );
1216
+ const fillIbo = writeIndexBuffer(device, d.indices);
1217
+ pass.setPipeline(set.fill);
1218
+ pass.setBindGroup(
1219
+ 0,
1220
+ device.createBindGroup({
1221
+ layout: solidLayout,
1222
+ entries: [{ binding: 0, resource: { buffer: fillUbo } }],
1223
+ }),
1224
+ );
1225
+ pass.setVertexBuffer(0, vbo);
1226
+ pass.setIndexBuffer(fillIbo, "uint16");
1227
+ pass.drawIndexed(d.indices.length);
1228
+
1229
+ const lines = triangleEdgesToLineList(d.indices);
1230
+ const lineUbo = this.#makeColorUbo(
1231
+ device,
1232
+ PREVIEW_STROKE.r,
1233
+ PREVIEW_STROKE.g,
1234
+ PREVIEW_STROKE.b,
1235
+ PREVIEW_STROKE.a * d.opacity,
1236
+ );
1237
+ const lineIbo = writeIndexBuffer(device, lines);
1238
+ pass.setPipeline(set.line);
1239
+ pass.setBindGroup(
1240
+ 0,
1241
+ device.createBindGroup({
1242
+ layout: solidLayout,
1243
+ entries: [{ binding: 0, resource: { buffer: lineUbo } }],
1244
+ }),
1245
+ );
1246
+ pass.setVertexBuffer(0, vbo);
1247
+ pass.setIndexBuffer(lineIbo, "uint16");
1248
+ pass.drawIndexed(lines.length);
1249
+ this.#transient.push(vbo, fillUbo, fillIbo, lineUbo, lineIbo);
1250
+ }
1251
+
1252
+ #makeColorUbo(
1253
+ device: GPUDevice,
1254
+ r: number,
1255
+ g: number,
1256
+ b: number,
1257
+ a: number,
1258
+ ): GPUBuffer {
1259
+ const ubo = device.createBuffer({
1260
+ size: 16,
1261
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
1262
+ mappedAtCreation: true,
1263
+ });
1264
+ new Float32Array(ubo.getMappedRange()).set([r, g, b, a]);
1265
+ ubo.unmap();
1266
+ return ubo;
1267
+ }
1268
+
1269
+ endFrame(): void {
1270
+ const device = this.#device;
1271
+ const encoder = this.#encoder;
1272
+ if (!device || !encoder) return;
1273
+
1274
+ // Empty frame still clears once.
1275
+ if (this.#pendingClear && !this.#pass) {
1276
+ this.#beginColorPass();
1277
+ }
1278
+ this.#endColorPass();
1279
+
1280
+ const buffers = this.#transient.splice(0);
1281
+ device.queue.submit([encoder.finish()]);
1282
+ for (const b of buffers) b.destroy();
1283
+ this.#encoder = null;
1284
+ this.#swapView = null;
1285
+ this.#pendingClear = false;
1286
+ }
1287
+
1288
+ resize(width: number, height: number): void {
1289
+ if (!this.#canvas) return;
1290
+ this.#canvas.width = width;
1291
+ this.#canvas.height = height;
1292
+ this.#ensureMsaa();
1293
+ }
1294
+
1295
+ getDevice(): GPUDevice | null {
1296
+ return this.#device;
1297
+ }
1298
+
1299
+ destroy(): void {
1300
+ for (const b of this.#transient) b.destroy();
1301
+ this.#transient = [];
1302
+ this.#clearGpuTextures();
1303
+ this.#msaaTexture?.destroy();
1304
+ this.#msaaTexture = null;
1305
+ this.#maskTexture?.destroy();
1306
+ this.#maskTexture = null;
1307
+ this.#whiteTexture?.destroy();
1308
+ this.#whiteTexture = null;
1309
+ this.#device?.destroy();
1310
+ this.#device = null;
1311
+ this.#context = null;
1312
+ this.#format = null;
1313
+ this.#pipelines = [null, null, null];
1314
+ this.#solidBindGroupLayout = null;
1315
+ this.#texturedBindGroupLayout = null;
1316
+ this.#clippedSolidBindGroupLayout = null;
1317
+ this.#clippedTexturedBindGroupLayout = null;
1318
+ this.#maskWriteBindGroupLayout = null;
1319
+ this.#maskWritePipeline = null;
1320
+ this.#sampler = null;
1321
+ this.#encoder = null;
1322
+ this.#pass = null;
1323
+ this.#swapView = null;
1324
+ this.#canvas = null;
1325
+ }
1326
+ }
1327
+
1328
+ export function createWebGpuRenderer(
1329
+ options?: WebGpuRendererOptions,
1330
+ ): WebGpuRenderer {
1331
+ return new WebGpuRendererImpl(options);
1332
+ }
1333
+
1334
+ export function isWebGpuAvailable(): boolean {
1335
+ return typeof navigator !== "undefined" && !!navigator.gpu;
1336
+ }