@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/src/backends/webgpu.ts
CHANGED
|
@@ -8,13 +8,12 @@
|
|
|
8
8
|
|
|
9
9
|
import { webGpuBlendState } from "../render/blend.js";
|
|
10
10
|
import {
|
|
11
|
-
fitClippingContexts,
|
|
12
11
|
type LaidOutClippingContext,
|
|
13
12
|
type MaskLayoutRect,
|
|
14
13
|
maskChannelVec4,
|
|
15
14
|
maskLayoutVec4,
|
|
16
|
-
partitionForClipping,
|
|
17
15
|
} from "../render/clipping.js";
|
|
16
|
+
import { ResidentClippingPlan } from "../render/clipping-plan.js";
|
|
18
17
|
import {
|
|
19
18
|
PREVIEW_FILL,
|
|
20
19
|
PREVIEW_STROKE,
|
|
@@ -27,6 +26,7 @@ import type {
|
|
|
27
26
|
WebGpuRenderer,
|
|
28
27
|
} from "../types.js";
|
|
29
28
|
import { BlendMode } from "../types.js";
|
|
29
|
+
import { WebGpuMeshCache } from "./webgpu-mesh-cache.js";
|
|
30
30
|
|
|
31
31
|
const SOLID_WGSL = /* wgsl */ `
|
|
32
32
|
struct Uniforms {
|
|
@@ -218,40 +218,6 @@ const BLEND_MODES: BlendMode[] = [
|
|
|
218
218
|
BlendMode.Multiplicative,
|
|
219
219
|
];
|
|
220
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
221
|
/** 1x1 white texture used when a mask mesh has no model texture. */
|
|
256
222
|
function createWhiteTexture(device: GPUDevice): GPUTexture {
|
|
257
223
|
const tex = device.createTexture({
|
|
@@ -324,8 +290,17 @@ export class WebGpuRendererImpl implements WebGpuRenderer {
|
|
|
324
290
|
#maskW = 0;
|
|
325
291
|
#maskH = 0;
|
|
326
292
|
#whiteTexture: GPUTexture | null = null;
|
|
327
|
-
#
|
|
293
|
+
#whiteView: GPUTextureView | null = null;
|
|
294
|
+
#maskView: GPUTextureView | null = null;
|
|
328
295
|
#gpuTextures: (GPUTexture | null)[] = [];
|
|
296
|
+
#gpuTextureViews: (GPUTextureView | null)[] = [];
|
|
297
|
+
readonly #meshCache = new WebGpuMeshCache();
|
|
298
|
+
readonly #clipPlan = new ResidentClippingPlan();
|
|
299
|
+
readonly #layoutScratch = new Float32Array(4);
|
|
300
|
+
readonly #channelScratch = new Float32Array(4);
|
|
301
|
+
readonly #boundsScratch = new Float32Array(4);
|
|
302
|
+
/** Preview-only line index buffers destroyed at endFrame. */
|
|
303
|
+
#transient: GPUBuffer[] = [];
|
|
329
304
|
readonly #options: WebGpuRendererOptions;
|
|
330
305
|
|
|
331
306
|
constructor(options: WebGpuRendererOptions = {}) {
|
|
@@ -709,6 +684,7 @@ export class WebGpuRendererImpl implements WebGpuRenderer {
|
|
|
709
684
|
addressModeV: "clamp-to-edge",
|
|
710
685
|
});
|
|
711
686
|
this.#whiteTexture = createWhiteTexture(device);
|
|
687
|
+
this.#whiteView = this.#whiteTexture.createView();
|
|
712
688
|
this.#ensureMsaa();
|
|
713
689
|
}
|
|
714
690
|
|
|
@@ -751,12 +727,16 @@ export class WebGpuRendererImpl implements WebGpuRenderer {
|
|
|
751
727
|
});
|
|
752
728
|
this.#maskW = w;
|
|
753
729
|
this.#maskH = h;
|
|
730
|
+
this.#maskView = this.#maskTexture.createView();
|
|
731
|
+
this.#meshCache.bumpMaskGeneration();
|
|
754
732
|
return this.#maskTexture;
|
|
755
733
|
}
|
|
756
734
|
|
|
757
735
|
#clearGpuTextures(): void {
|
|
758
736
|
for (const t of this.#gpuTextures) t?.destroy();
|
|
759
737
|
this.#gpuTextures = [];
|
|
738
|
+
this.#gpuTextureViews = [];
|
|
739
|
+
this.#meshCache.invalidateTextureBindGroups();
|
|
760
740
|
}
|
|
761
741
|
|
|
762
742
|
setTextures(textures: TextureData[]): void {
|
|
@@ -767,6 +747,7 @@ export class WebGpuRendererImpl implements WebGpuRenderer {
|
|
|
767
747
|
let maxIndex = -1;
|
|
768
748
|
for (const t of textures) maxIndex = Math.max(maxIndex, t.index);
|
|
769
749
|
this.#gpuTextures = new Array(Math.max(0, maxIndex + 1)).fill(null);
|
|
750
|
+
this.#gpuTextureViews = new Array(Math.max(0, maxIndex + 1)).fill(null);
|
|
770
751
|
|
|
771
752
|
for (const t of textures) {
|
|
772
753
|
const gpuTex = device.createTexture({
|
|
@@ -783,6 +764,7 @@ export class WebGpuRendererImpl implements WebGpuRenderer {
|
|
|
783
764
|
[t.width, t.height],
|
|
784
765
|
);
|
|
785
766
|
this.#gpuTextures[t.index] = gpuTex;
|
|
767
|
+
this.#gpuTextureViews[t.index] = gpuTex.createView();
|
|
786
768
|
}
|
|
787
769
|
}
|
|
788
770
|
|
|
@@ -877,8 +859,8 @@ export class WebGpuRendererImpl implements WebGpuRenderer {
|
|
|
877
859
|
const byIndex = new Map<number, DrawableMesh>();
|
|
878
860
|
for (const d of drawables) byIndex.set(d.index, d);
|
|
879
861
|
|
|
880
|
-
const partitioned =
|
|
881
|
-
const contexts =
|
|
862
|
+
const partitioned = this.#clipPlan.resolveMeshes(drawables);
|
|
863
|
+
const contexts = partitioned.contexts;
|
|
882
864
|
const { maskOnly } = partitioned;
|
|
883
865
|
|
|
884
866
|
let maskTex: GPUTexture | null = null;
|
|
@@ -969,29 +951,25 @@ export class WebGpuRendererImpl implements WebGpuRenderer {
|
|
|
969
951
|
channelFlag: LaidOutClippingContext["channelFlag"],
|
|
970
952
|
): void {
|
|
971
953
|
const device = this.#device;
|
|
972
|
-
|
|
954
|
+
const whiteView = this.#whiteView;
|
|
955
|
+
if (!device || !whiteView || d.opacity <= 0) return;
|
|
973
956
|
|
|
974
|
-
const
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
957
|
+
const slot = this.#meshCache.ensureSlot(device, d.index);
|
|
958
|
+
this.#meshCache.uploadMesh(
|
|
959
|
+
device,
|
|
960
|
+
slot,
|
|
961
|
+
d.vertexPositions,
|
|
962
|
+
d.uvs,
|
|
963
|
+
d.indices,
|
|
964
|
+
);
|
|
982
965
|
|
|
983
|
-
const
|
|
984
|
-
const
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
});
|
|
989
|
-
const layoutVec = maskLayoutVec4(atlasLayout);
|
|
990
|
-
const ch = maskChannelVec4(channelFlag);
|
|
991
|
-
const boundsVec = maskLayoutVec4(modelBounds);
|
|
992
|
-
new Float32Array(ubo.getMappedRange()).set([
|
|
966
|
+
const layoutVec = maskLayoutVec4(atlasLayout, this.#layoutScratch);
|
|
967
|
+
const ch = maskChannelVec4(channelFlag, this.#channelScratch);
|
|
968
|
+
const boundsVec = maskLayoutVec4(modelBounds, this.#boundsScratch);
|
|
969
|
+
const useWhite = !this.#gpuTextures[d.textureIndex];
|
|
970
|
+
this.#meshCache.writeUbo(device, slot, [
|
|
993
971
|
Math.max(d.opacity, 1),
|
|
994
|
-
|
|
972
|
+
useWhite ? 0 : 1,
|
|
995
973
|
0,
|
|
996
974
|
0,
|
|
997
975
|
layoutVec[0]!,
|
|
@@ -1007,24 +985,30 @@ export class WebGpuRendererImpl implements WebGpuRenderer {
|
|
|
1007
985
|
boundsVec[2]!,
|
|
1008
986
|
boundsVec[3]!,
|
|
1009
987
|
]);
|
|
1010
|
-
ubo.unmap();
|
|
1011
988
|
|
|
1012
|
-
const
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
989
|
+
const texView = this.#gpuTextureViews[d.textureIndex] ?? whiteView;
|
|
990
|
+
if (
|
|
991
|
+
!slot.bgMask ||
|
|
992
|
+
slot.texIndex !== d.textureIndex ||
|
|
993
|
+
slot.useWhite !== useWhite
|
|
994
|
+
) {
|
|
995
|
+
slot.bgMask = device.createBindGroup({
|
|
1016
996
|
layout,
|
|
1017
997
|
entries: [
|
|
1018
|
-
{ binding: 0, resource: { buffer: ubo } },
|
|
998
|
+
{ binding: 0, resource: { buffer: slot.ubo } },
|
|
1019
999
|
{ binding: 1, resource: sampler },
|
|
1020
|
-
{ binding: 2, resource:
|
|
1000
|
+
{ binding: 2, resource: texView },
|
|
1021
1001
|
],
|
|
1022
|
-
})
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1002
|
+
});
|
|
1003
|
+
slot.texIndex = d.textureIndex;
|
|
1004
|
+
slot.useWhite = useWhite;
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
pass.setBindGroup(0, slot.bgMask);
|
|
1008
|
+
pass.setVertexBuffer(0, slot.vbo);
|
|
1009
|
+
pass.setIndexBuffer(slot.ibo, "uint16");
|
|
1026
1010
|
pass.drawIndexed(d.indices.length);
|
|
1027
|
-
|
|
1011
|
+
void whiteTex;
|
|
1028
1012
|
}
|
|
1029
1013
|
|
|
1030
1014
|
#drawColorMesh(
|
|
@@ -1043,6 +1027,7 @@ export class WebGpuRendererImpl implements WebGpuRenderer {
|
|
|
1043
1027
|
const texturedLayout = this.#texturedBindGroupLayout;
|
|
1044
1028
|
const clippedSolidLayout = this.#clippedSolidBindGroupLayout;
|
|
1045
1029
|
const clippedTexturedLayout = this.#clippedTexturedBindGroupLayout;
|
|
1030
|
+
const maskView = this.#maskView;
|
|
1046
1031
|
if (
|
|
1047
1032
|
!device ||
|
|
1048
1033
|
!sampler ||
|
|
@@ -1061,31 +1046,31 @@ export class WebGpuRendererImpl implements WebGpuRenderer {
|
|
|
1061
1046
|
|
|
1062
1047
|
const layoutVec = maskLayoutVec4(
|
|
1063
1048
|
atlasLayout ?? { x: 0, y: 0, width: 1, height: 1 },
|
|
1049
|
+
this.#layoutScratch,
|
|
1050
|
+
);
|
|
1051
|
+
const ch = maskChannelVec4(
|
|
1052
|
+
channelFlag ?? [0, 0, 0, 1],
|
|
1053
|
+
this.#channelScratch,
|
|
1064
1054
|
);
|
|
1065
|
-
const ch = maskChannelVec4(channelFlag ?? [0, 0, 0, 1]);
|
|
1066
1055
|
const boundsVec = maskLayoutVec4(
|
|
1067
1056
|
modelBounds ?? { x: -1, y: -1, width: 2, height: 2 },
|
|
1057
|
+
this.#boundsScratch,
|
|
1068
1058
|
);
|
|
1069
1059
|
|
|
1060
|
+
const slot = this.#meshCache.ensureSlot(device, d.index);
|
|
1070
1061
|
const gpuTex = this.#gpuTextures[d.textureIndex] ?? null;
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
size: uboSize,
|
|
1084
|
-
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
1085
|
-
mappedAtCreation: true,
|
|
1086
|
-
});
|
|
1087
|
-
if (useMask && maskTex) {
|
|
1088
|
-
new Float32Array(ubo.getMappedRange()).set([
|
|
1062
|
+
const texView = this.#gpuTextureViews[d.textureIndex] ?? null;
|
|
1063
|
+
|
|
1064
|
+
if (gpuTex && texView) {
|
|
1065
|
+
this.#meshCache.uploadMesh(
|
|
1066
|
+
device,
|
|
1067
|
+
slot,
|
|
1068
|
+
d.vertexPositions,
|
|
1069
|
+
d.uvs,
|
|
1070
|
+
d.indices,
|
|
1071
|
+
);
|
|
1072
|
+
if (useMask && maskTex && maskView) {
|
|
1073
|
+
this.#meshCache.writeUbo(device, slot, [
|
|
1089
1074
|
d.opacity,
|
|
1090
1075
|
invertMask ? 1 : 0,
|
|
1091
1076
|
0,
|
|
@@ -1103,67 +1088,57 @@ export class WebGpuRendererImpl implements WebGpuRenderer {
|
|
|
1103
1088
|
boundsVec[2]!,
|
|
1104
1089
|
boundsVec[3]!,
|
|
1105
1090
|
]);
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
d.
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
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({
|
|
1091
|
+
if (
|
|
1092
|
+
!slot.bgClippedTextured ||
|
|
1093
|
+
slot.texIndex !== d.textureIndex ||
|
|
1094
|
+
slot.maskGeneration !== this.#meshCache.maskGeneration
|
|
1095
|
+
) {
|
|
1096
|
+
slot.bgClippedTextured = device.createBindGroup({
|
|
1122
1097
|
layout: clippedTexturedLayout,
|
|
1123
1098
|
entries: [
|
|
1124
|
-
{ binding: 0, resource: { buffer: ubo } },
|
|
1099
|
+
{ binding: 0, resource: { buffer: slot.ubo } },
|
|
1125
1100
|
{ binding: 1, resource: sampler },
|
|
1126
|
-
{ binding: 2, resource:
|
|
1127
|
-
{ binding: 3, resource:
|
|
1101
|
+
{ binding: 2, resource: texView },
|
|
1102
|
+
{ binding: 3, resource: maskView },
|
|
1128
1103
|
],
|
|
1129
|
-
})
|
|
1130
|
-
|
|
1104
|
+
});
|
|
1105
|
+
slot.texIndex = d.textureIndex;
|
|
1106
|
+
slot.maskGeneration = this.#meshCache.maskGeneration;
|
|
1107
|
+
}
|
|
1108
|
+
pass.setPipeline(set.clippedTextured);
|
|
1109
|
+
pass.setBindGroup(0, slot.bgClippedTextured);
|
|
1131
1110
|
} else {
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
device.createBindGroup({
|
|
1111
|
+
this.#meshCache.writeUbo(device, slot, [d.opacity, 0, 0, 0]);
|
|
1112
|
+
if (!slot.bgTextured || slot.texIndex !== d.textureIndex) {
|
|
1113
|
+
slot.bgTextured = device.createBindGroup({
|
|
1136
1114
|
layout: texturedLayout,
|
|
1137
1115
|
entries: [
|
|
1138
|
-
{ binding: 0, resource: { buffer: ubo } },
|
|
1116
|
+
{ binding: 0, resource: { buffer: slot.ubo } },
|
|
1139
1117
|
{ binding: 1, resource: sampler },
|
|
1140
|
-
{ binding: 2, resource:
|
|
1118
|
+
{ binding: 2, resource: texView },
|
|
1141
1119
|
],
|
|
1142
|
-
})
|
|
1143
|
-
|
|
1120
|
+
});
|
|
1121
|
+
slot.texIndex = d.textureIndex;
|
|
1122
|
+
}
|
|
1123
|
+
pass.setPipeline(set.textured);
|
|
1124
|
+
pass.setBindGroup(0, slot.bgTextured);
|
|
1144
1125
|
}
|
|
1145
|
-
pass.setVertexBuffer(0, vbo);
|
|
1146
|
-
pass.setIndexBuffer(ibo, "uint16");
|
|
1126
|
+
pass.setVertexBuffer(0, slot.vbo);
|
|
1127
|
+
pass.setIndexBuffer(slot.ibo, "uint16");
|
|
1147
1128
|
pass.drawIndexed(d.indices.length);
|
|
1148
|
-
this.#transient.push(vbo, ubo, ibo);
|
|
1149
1129
|
return;
|
|
1150
1130
|
}
|
|
1151
1131
|
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
if (useMask && maskTex) {
|
|
1161
|
-
|
|
1162
|
-
size: 80,
|
|
1163
|
-
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
1164
|
-
mappedAtCreation: true,
|
|
1165
|
-
});
|
|
1166
|
-
new Float32Array(ubo.getMappedRange()).set([
|
|
1132
|
+
// Preview path: solid fill (+ optional stroke) without model textures.
|
|
1133
|
+
this.#meshCache.uploadMesh(
|
|
1134
|
+
device,
|
|
1135
|
+
slot,
|
|
1136
|
+
d.vertexPositions,
|
|
1137
|
+
d.uvs,
|
|
1138
|
+
d.indices,
|
|
1139
|
+
);
|
|
1140
|
+
if (useMask && maskTex && maskView) {
|
|
1141
|
+
this.#meshCache.writeUbo(device, slot, [
|
|
1167
1142
|
PREVIEW_FILL.r,
|
|
1168
1143
|
PREVIEW_FILL.g,
|
|
1169
1144
|
PREVIEW_FILL.b,
|
|
@@ -1185,56 +1160,66 @@ export class WebGpuRendererImpl implements WebGpuRenderer {
|
|
|
1185
1160
|
boundsVec[2]!,
|
|
1186
1161
|
boundsVec[3]!,
|
|
1187
1162
|
]);
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
device.createBindGroup({
|
|
1163
|
+
if (
|
|
1164
|
+
!slot.bgClippedSolid ||
|
|
1165
|
+
slot.maskGeneration !== this.#meshCache.maskGeneration
|
|
1166
|
+
) {
|
|
1167
|
+
slot.bgClippedSolid = device.createBindGroup({
|
|
1194
1168
|
layout: clippedSolidLayout,
|
|
1195
1169
|
entries: [
|
|
1196
|
-
{ binding: 0, resource: { buffer: ubo } },
|
|
1170
|
+
{ binding: 0, resource: { buffer: slot.ubo } },
|
|
1197
1171
|
{ binding: 1, resource: sampler },
|
|
1198
|
-
{ binding: 2, resource:
|
|
1172
|
+
{ binding: 2, resource: maskView },
|
|
1199
1173
|
],
|
|
1200
|
-
})
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
pass.
|
|
1174
|
+
});
|
|
1175
|
+
slot.maskGeneration = this.#meshCache.maskGeneration;
|
|
1176
|
+
}
|
|
1177
|
+
pass.setPipeline(set.clippedFill);
|
|
1178
|
+
pass.setBindGroup(0, slot.bgClippedSolid);
|
|
1179
|
+
pass.setVertexBuffer(0, slot.vbo);
|
|
1180
|
+
pass.setIndexBuffer(slot.ibo, "uint16");
|
|
1204
1181
|
pass.drawIndexed(d.indices.length);
|
|
1205
|
-
this.#transient.push(vbo, ubo, ibo);
|
|
1206
1182
|
return;
|
|
1207
1183
|
}
|
|
1208
1184
|
|
|
1209
|
-
|
|
1210
|
-
device,
|
|
1185
|
+
this.#meshCache.writeUbo(device, slot, [
|
|
1211
1186
|
PREVIEW_FILL.r,
|
|
1212
1187
|
PREVIEW_FILL.g,
|
|
1213
1188
|
PREVIEW_FILL.b,
|
|
1214
1189
|
PREVIEW_FILL.a * d.opacity,
|
|
1215
|
-
);
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
pass.setBindGroup(
|
|
1219
|
-
0,
|
|
1220
|
-
device.createBindGroup({
|
|
1190
|
+
]);
|
|
1191
|
+
if (!slot.bgSolid) {
|
|
1192
|
+
slot.bgSolid = device.createBindGroup({
|
|
1221
1193
|
layout: solidLayout,
|
|
1222
|
-
entries: [{ binding: 0, resource: { buffer:
|
|
1223
|
-
})
|
|
1224
|
-
|
|
1225
|
-
pass.
|
|
1226
|
-
pass.
|
|
1194
|
+
entries: [{ binding: 0, resource: { buffer: slot.ubo } }],
|
|
1195
|
+
});
|
|
1196
|
+
}
|
|
1197
|
+
pass.setPipeline(set.fill);
|
|
1198
|
+
pass.setBindGroup(0, slot.bgSolid);
|
|
1199
|
+
pass.setVertexBuffer(0, slot.vbo);
|
|
1200
|
+
pass.setIndexBuffer(slot.ibo, "uint16");
|
|
1227
1201
|
pass.drawIndexed(d.indices.length);
|
|
1228
1202
|
|
|
1229
1203
|
const lines = triangleEdgesToLineList(d.indices);
|
|
1230
|
-
const lineUbo =
|
|
1231
|
-
|
|
1204
|
+
const lineUbo = device.createBuffer({
|
|
1205
|
+
size: 16,
|
|
1206
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
1207
|
+
mappedAtCreation: true,
|
|
1208
|
+
});
|
|
1209
|
+
new Float32Array(lineUbo.getMappedRange()).set([
|
|
1232
1210
|
PREVIEW_STROKE.r,
|
|
1233
1211
|
PREVIEW_STROKE.g,
|
|
1234
1212
|
PREVIEW_STROKE.b,
|
|
1235
1213
|
PREVIEW_STROKE.a * d.opacity,
|
|
1236
|
-
);
|
|
1237
|
-
|
|
1214
|
+
]);
|
|
1215
|
+
lineUbo.unmap();
|
|
1216
|
+
const lineIbo = device.createBuffer({
|
|
1217
|
+
size: Math.max(4, (lines.byteLength + 3) & ~3),
|
|
1218
|
+
usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST,
|
|
1219
|
+
mappedAtCreation: true,
|
|
1220
|
+
});
|
|
1221
|
+
new Uint16Array(lineIbo.getMappedRange()).set(lines);
|
|
1222
|
+
lineIbo.unmap();
|
|
1238
1223
|
pass.setPipeline(set.line);
|
|
1239
1224
|
pass.setBindGroup(
|
|
1240
1225
|
0,
|
|
@@ -1243,27 +1228,10 @@ export class WebGpuRendererImpl implements WebGpuRenderer {
|
|
|
1243
1228
|
entries: [{ binding: 0, resource: { buffer: lineUbo } }],
|
|
1244
1229
|
}),
|
|
1245
1230
|
);
|
|
1246
|
-
pass.setVertexBuffer(0, vbo);
|
|
1231
|
+
pass.setVertexBuffer(0, slot.vbo);
|
|
1247
1232
|
pass.setIndexBuffer(lineIbo, "uint16");
|
|
1248
1233
|
pass.drawIndexed(lines.length);
|
|
1249
|
-
this.#transient.push(
|
|
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;
|
|
1234
|
+
this.#transient.push(lineUbo, lineIbo);
|
|
1267
1235
|
}
|
|
1268
1236
|
|
|
1269
1237
|
endFrame(): void {
|
|
@@ -1299,13 +1267,17 @@ export class WebGpuRendererImpl implements WebGpuRenderer {
|
|
|
1299
1267
|
destroy(): void {
|
|
1300
1268
|
for (const b of this.#transient) b.destroy();
|
|
1301
1269
|
this.#transient = [];
|
|
1270
|
+
this.#meshCache.clear();
|
|
1271
|
+
this.#clipPlan.clear();
|
|
1302
1272
|
this.#clearGpuTextures();
|
|
1303
1273
|
this.#msaaTexture?.destroy();
|
|
1304
1274
|
this.#msaaTexture = null;
|
|
1305
1275
|
this.#maskTexture?.destroy();
|
|
1306
1276
|
this.#maskTexture = null;
|
|
1277
|
+
this.#maskView = null;
|
|
1307
1278
|
this.#whiteTexture?.destroy();
|
|
1308
1279
|
this.#whiteTexture = null;
|
|
1280
|
+
this.#whiteView = null;
|
|
1309
1281
|
this.#device?.destroy();
|
|
1310
1282
|
this.#device = null;
|
|
1311
1283
|
this.#context = null;
|
package/src/index.ts
CHANGED
|
@@ -95,6 +95,15 @@ export {
|
|
|
95
95
|
maskLayoutVec4,
|
|
96
96
|
partitionForClipping,
|
|
97
97
|
} from "./render/clipping.js";
|
|
98
|
+
export {
|
|
99
|
+
clippingTopologyKey,
|
|
100
|
+
fitClippingContextsInPlace,
|
|
101
|
+
ResidentClippingPlan,
|
|
102
|
+
} from "./render/clipping-plan.js";
|
|
103
|
+
export {
|
|
104
|
+
interleaveByteLength,
|
|
105
|
+
interleavePosUv,
|
|
106
|
+
} from "./render/mesh-interleave.js";
|
|
98
107
|
export {
|
|
99
108
|
PREVIEW_FILL,
|
|
100
109
|
PREVIEW_STROKE,
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resident clipping / mask atlas plan.
|
|
3
|
+
*
|
|
4
|
+
* Topology (maskIndices / invert / packing options) is compiled once; per frame
|
|
5
|
+
* only `modelBounds` is refreshed from current vertex positions.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { DrawableMesh } from "../types.js";
|
|
9
|
+
import {
|
|
10
|
+
type ClippingDrawableRef,
|
|
11
|
+
type ClippingPartition,
|
|
12
|
+
calcClippedDrawableBounds,
|
|
13
|
+
type LaidOutClippingContext,
|
|
14
|
+
type MaskAtlasOptions,
|
|
15
|
+
type MaskBoundsMeshRef,
|
|
16
|
+
type MaskLayoutRect,
|
|
17
|
+
partitionForClipping,
|
|
18
|
+
} from "./clipping.js";
|
|
19
|
+
|
|
20
|
+
/** Stable key for mask topology (ignores vertex positions / opacity). */
|
|
21
|
+
export function clippingTopologyKey(
|
|
22
|
+
drawables: readonly ClippingDrawableRef[],
|
|
23
|
+
): string {
|
|
24
|
+
let key = `n:${drawables.length}`;
|
|
25
|
+
for (let i = 0; i < drawables.length; i++) {
|
|
26
|
+
const d = drawables[i]!;
|
|
27
|
+
key += `|${d.index}:${d.invertedMask ? 1 : 0}:`;
|
|
28
|
+
const masks = d.maskIndices;
|
|
29
|
+
for (let j = 0; j < masks.length; j++) {
|
|
30
|
+
key += `${masks[j]!},`;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return key;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function atlasOptionsKey(options: MaskAtlasOptions): string {
|
|
37
|
+
return `${options.mode ?? "rgba"}:${options.inset ?? ""}:${options.renderTextureCount ?? ""}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
type ResidentContext = {
|
|
41
|
+
-readonly [K in keyof LaidOutClippingContext]: LaidOutClippingContext[K];
|
|
42
|
+
} & { modelBounds: MaskLayoutRect };
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Update `modelBounds` on resident contexts without reallocating the plan.
|
|
46
|
+
*/
|
|
47
|
+
export function fitClippingContextsInPlace(
|
|
48
|
+
contexts: readonly ResidentContext[],
|
|
49
|
+
byIndex: ReadonlyMap<number, MaskBoundsMeshRef>,
|
|
50
|
+
margin = 0.05,
|
|
51
|
+
): void {
|
|
52
|
+
for (let i = 0; i < contexts.length; i++) {
|
|
53
|
+
const ctx = contexts[i]!;
|
|
54
|
+
ctx.modelBounds = calcClippedDrawableBounds(
|
|
55
|
+
ctx.clippedIndices,
|
|
56
|
+
byIndex,
|
|
57
|
+
margin,
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Caches atlas layout + mask/clipped sets while drawable mask topology is
|
|
64
|
+
* unchanged. Call {@link resolve} each frame; call {@link clear} on destroy.
|
|
65
|
+
*/
|
|
66
|
+
export class ResidentClippingPlan {
|
|
67
|
+
#cacheKey = "";
|
|
68
|
+
#contexts: ResidentContext[] = [];
|
|
69
|
+
#clipped: Set<number> = new Set();
|
|
70
|
+
#maskOnly: Set<number> = new Set();
|
|
71
|
+
readonly #byIndex = new Map<number, MaskBoundsMeshRef>();
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Returns a partition whose `contexts` array identity is stable across
|
|
75
|
+
* frames with the same topology. `modelBounds` are refreshed in place.
|
|
76
|
+
*/
|
|
77
|
+
resolve(
|
|
78
|
+
drawables: readonly ClippingDrawableRef[],
|
|
79
|
+
options: MaskAtlasOptions = {},
|
|
80
|
+
): ClippingPartition {
|
|
81
|
+
const nextKey = `${clippingTopologyKey(drawables)}|${atlasOptionsKey(options)}`;
|
|
82
|
+
if (nextKey !== this.#cacheKey) {
|
|
83
|
+
const partitioned = partitionForClipping(drawables, options);
|
|
84
|
+
this.#contexts = partitioned.contexts.map((ctx) => ({
|
|
85
|
+
key: ctx.key,
|
|
86
|
+
maskIndices: ctx.maskIndices,
|
|
87
|
+
clippedIndices: ctx.clippedIndices,
|
|
88
|
+
invertedMask: ctx.invertedMask,
|
|
89
|
+
layout: ctx.layout,
|
|
90
|
+
channelIndex: ctx.channelIndex,
|
|
91
|
+
channelFlag: ctx.channelFlag,
|
|
92
|
+
bufferIndex: ctx.bufferIndex,
|
|
93
|
+
modelBounds: { ...ctx.modelBounds },
|
|
94
|
+
}));
|
|
95
|
+
this.#clipped = new Set(partitioned.clipped);
|
|
96
|
+
this.#maskOnly = new Set(partitioned.maskOnly);
|
|
97
|
+
this.#cacheKey = nextKey;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
this.#byIndex.clear();
|
|
101
|
+
for (let i = 0; i < drawables.length; i++) {
|
|
102
|
+
const d = drawables[i]!;
|
|
103
|
+
if ("vertexPositions" in d) {
|
|
104
|
+
this.#byIndex.set(d.index, d as MaskBoundsMeshRef);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
fitClippingContextsInPlace(this.#contexts, this.#byIndex);
|
|
108
|
+
return {
|
|
109
|
+
contexts: this.#contexts,
|
|
110
|
+
clipped: this.#clipped,
|
|
111
|
+
maskOnly: this.#maskOnly,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Convenience for full drawable meshes (GPU backends). */
|
|
116
|
+
resolveMeshes(
|
|
117
|
+
drawables: readonly DrawableMesh[],
|
|
118
|
+
options: MaskAtlasOptions = {},
|
|
119
|
+
): ClippingPartition {
|
|
120
|
+
return this.resolve(drawables, options);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** True when the last {@link resolve} reused the cached atlas layout. */
|
|
124
|
+
get hasPlan(): boolean {
|
|
125
|
+
return this.#cacheKey.length > 0;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Exposed for tests — context array identity while topology holds. */
|
|
129
|
+
get residentContexts(): readonly LaidOutClippingContext[] {
|
|
130
|
+
return this.#contexts;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
clear(): void {
|
|
134
|
+
this.#cacheKey = "";
|
|
135
|
+
this.#contexts = [];
|
|
136
|
+
this.#clipped = new Set();
|
|
137
|
+
this.#maskOnly = new Set();
|
|
138
|
+
this.#byIndex.clear();
|
|
139
|
+
}
|
|
140
|
+
}
|