@codexo/exojs 0.8.2 → 0.8.3
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/CHANGELOG.md +72 -0
- package/dist/esm/audio/AudioAnalyser.d.ts +36 -0
- package/dist/esm/audio/AudioAnalyser.js +148 -0
- package/dist/esm/audio/AudioAnalyser.js.map +1 -1
- package/dist/esm/audio/BeatDetector.d.ts +62 -0
- package/dist/esm/audio/BeatDetector.js +77 -0
- package/dist/esm/audio/BeatDetector.js.map +1 -1
- package/dist/esm/audio/dsp/mel.js +70 -0
- package/dist/esm/audio/dsp/mel.js.map +1 -0
- package/dist/esm/debug/RenderPassInspectorLayer.d.ts +71 -0
- package/dist/esm/debug/RenderPassInspectorLayer.js +201 -0
- package/dist/esm/debug/RenderPassInspectorLayer.js.map +1 -0
- package/dist/esm/debug/index.d.ts +1 -0
- package/dist/esm/debug/index.js +1 -0
- package/dist/esm/debug/index.js.map +1 -1
- package/dist/esm/index.js +2 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/rendering/filters/WebGpuShaderFilter.js +5 -1
- package/dist/esm/rendering/filters/WebGpuShaderFilter.js.map +1 -1
- package/dist/esm/rendering/index.d.ts +2 -0
- package/dist/esm/rendering/mesh/Mesh.d.ts +4 -47
- package/dist/esm/rendering/mesh/Mesh.js.map +1 -1
- package/dist/esm/rendering/mesh/MeshShader.d.ts +183 -0
- package/dist/esm/rendering/mesh/MeshShader.js +231 -0
- package/dist/esm/rendering/mesh/MeshShader.js.map +1 -0
- package/dist/esm/rendering/texture/DataTexture.d.ts +115 -0
- package/dist/esm/rendering/texture/DataTexture.js +173 -0
- package/dist/esm/rendering/texture/DataTexture.js.map +1 -0
- package/dist/esm/rendering/webgl2/WebGl2Backend.js +42 -1
- package/dist/esm/rendering/webgl2/WebGl2Backend.js.map +1 -1
- package/dist/esm/rendering/webgl2/WebGl2MeshRenderer.js +12 -1
- package/dist/esm/rendering/webgl2/WebGl2MeshRenderer.js.map +1 -1
- package/dist/esm/rendering/webgpu/WebGpuBackend.d.ts +1 -0
- package/dist/esm/rendering/webgpu/WebGpuBackend.js +60 -7
- package/dist/esm/rendering/webgpu/WebGpuBackend.js.map +1 -1
- package/dist/esm/rendering/webgpu/WebGpuMaskCompositor.js +2 -1
- package/dist/esm/rendering/webgpu/WebGpuMaskCompositor.js.map +1 -1
- package/dist/esm/rendering/webgpu/WebGpuMeshRenderer.d.ts +13 -0
- package/dist/esm/rendering/webgpu/WebGpuMeshRenderer.js +636 -83
- package/dist/esm/rendering/webgpu/WebGpuMeshRenderer.js.map +1 -1
- package/dist/exo.esm.js +1452 -102
- package/dist/exo.esm.js.map +1 -1
- package/package.json +1 -1
|
@@ -48,16 +48,27 @@ fn fragmentMain(input: VertexOutput) -> @location(0) vec4<f32> {
|
|
|
48
48
|
}
|
|
49
49
|
`;
|
|
50
50
|
// Per-vertex layout (20 bytes): pos f32x2 + uv f32x2 + color u8x4-norm.
|
|
51
|
-
//
|
|
52
|
-
// shader stays branchless and uniform-free except for the per-mesh tint.
|
|
51
|
+
// Default-shader path bakes the (view * globalTransform) into position so the
|
|
52
|
+
// vertex shader stays branchless and uniform-free except for the per-mesh tint.
|
|
53
|
+
// Custom-shader path keeps positions in LOCAL space — the user's vertex
|
|
54
|
+
// shader receives mesh transforms via the auto-bound u_mesh uniform block.
|
|
53
55
|
const vertexStrideBytes = 20;
|
|
54
56
|
const wordsPerVertex = vertexStrideBytes / 4;
|
|
55
57
|
const tintByteLength = 32; // vec4 tint + vec4 flags (only flags.x used)
|
|
58
|
+
// Custom-shader uniform layout:
|
|
59
|
+
// mat3x3<f32> projection — 48 bytes (3 vec3 columns padded to vec4 in WGSL)
|
|
60
|
+
// mat3x3<f32> translation — 48 bytes
|
|
61
|
+
// vec4<f32> tint — 16 bytes
|
|
62
|
+
// Total: 112 bytes; aligned up to 256 for dynamic offset.
|
|
63
|
+
const customMeshUniformBytes = 112;
|
|
64
|
+
const meshUniformAlignment = 256;
|
|
65
|
+
const maxCustomTextureSlots = 7; // user texture uniforms; group 2 binding 1..N
|
|
56
66
|
class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
|
|
57
67
|
_combinedTransform = new Matrix();
|
|
58
68
|
_drawCalls = [];
|
|
59
69
|
_pipelines = new Map();
|
|
60
70
|
_textureBindGroups = new Map();
|
|
71
|
+
_customShaders = new Map();
|
|
61
72
|
_device = null;
|
|
62
73
|
_shaderModule = null;
|
|
63
74
|
_uniformBindGroupLayout = null;
|
|
@@ -81,8 +92,9 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
|
|
|
81
92
|
if (backend === null) {
|
|
82
93
|
throw new Error('WebGpuMeshRenderer is not connected to a backend.');
|
|
83
94
|
}
|
|
84
|
-
|
|
85
|
-
|
|
95
|
+
const customShader = mesh.shader;
|
|
96
|
+
if (customShader !== null && customShader.wgsl === null) {
|
|
97
|
+
throw new Error('MeshShader has no `wgsl` source; cannot render through the WebGPU backend.');
|
|
86
98
|
}
|
|
87
99
|
const vertexCount = mesh.vertexCount;
|
|
88
100
|
if (vertexCount === 0) {
|
|
@@ -91,15 +103,26 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
|
|
|
91
103
|
const blendMode = mesh.blendMode;
|
|
92
104
|
backend.setBlendMode(blendMode);
|
|
93
105
|
const meshTexture = mesh.texture ?? Texture.white;
|
|
94
|
-
//
|
|
95
|
-
//
|
|
106
|
+
// backend.shouldPremultiplyTextureSample expects RenderTexture-or-Texture.
|
|
107
|
+
// Both branches are valid here. Premultiply flag is ignored by custom
|
|
108
|
+
// shaders (they handle premultiplication themselves), but we still record
|
|
109
|
+
// it so the default path uses the right value.
|
|
96
110
|
const premultiplySample = backend.shouldPremultiplyTextureSample(meshTexture);
|
|
97
111
|
const indexCount = mesh.indexCount;
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
112
|
+
let customDrawIndex = -1;
|
|
113
|
+
if (customShader !== null) {
|
|
114
|
+
const resources = this._getOrCreateCustomShaderResources(customShader);
|
|
115
|
+
customDrawIndex = resources.drawCount;
|
|
116
|
+
resources.drawCount++;
|
|
117
|
+
resources.totalVertices += vertexCount;
|
|
118
|
+
resources.totalIndices += indexCount;
|
|
119
|
+
}
|
|
120
|
+
// Plan offsets within the shared (default) or per-shader (custom) buffers;
|
|
121
|
+
// actual data packing happens in flush() after all drawcalls are known so
|
|
122
|
+
// a single writeBuffer per resource covers the whole frame.
|
|
101
123
|
const drawCall = {
|
|
102
124
|
mesh,
|
|
125
|
+
customShader,
|
|
103
126
|
blendMode,
|
|
104
127
|
texture: meshTexture,
|
|
105
128
|
premultiplySample,
|
|
@@ -107,6 +130,7 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
|
|
|
107
130
|
vertexCount,
|
|
108
131
|
indexByteOffset: 0,
|
|
109
132
|
indexCount,
|
|
133
|
+
customDrawIndex,
|
|
110
134
|
};
|
|
111
135
|
// Use mutable record (interface readonly is for type safety against
|
|
112
136
|
// callers; the renderer fills these slots in flush()).
|
|
@@ -135,88 +159,194 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
|
|
|
135
159
|
pass.end();
|
|
136
160
|
backend.submit(encoder.finish());
|
|
137
161
|
}
|
|
138
|
-
this.
|
|
162
|
+
this._resetFrame();
|
|
139
163
|
return;
|
|
140
164
|
}
|
|
141
|
-
// Phase 1: compute layout offsets
|
|
142
|
-
|
|
143
|
-
let
|
|
165
|
+
// Phase 1: compute layout offsets (default vs. custom paths use separate
|
|
166
|
+
// buffers, so default offsets are independent of custom offsets).
|
|
167
|
+
let defaultVertices = 0;
|
|
168
|
+
let defaultIndices = 0;
|
|
169
|
+
const customVertexCursors = new Map(); // running vertex count per shader
|
|
170
|
+
const customIndexCursors = new Map();
|
|
144
171
|
for (let i = 0; i < this._drawCallCount; i++) {
|
|
145
172
|
const dc = this._drawCalls[i];
|
|
146
|
-
dc.
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
173
|
+
if (dc.customShader === null) {
|
|
174
|
+
dc.vertexByteOffset = defaultVertices * vertexStrideBytes;
|
|
175
|
+
dc.indexByteOffset = defaultIndices * Uint16Array.BYTES_PER_ELEMENT;
|
|
176
|
+
defaultVertices += dc.vertexCount;
|
|
177
|
+
defaultIndices += dc.indexCount;
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
const vCursor = customVertexCursors.get(dc.customShader) ?? 0;
|
|
181
|
+
const iCursor = customIndexCursors.get(dc.customShader) ?? 0;
|
|
182
|
+
dc.vertexByteOffset = vCursor * vertexStrideBytes;
|
|
183
|
+
dc.indexByteOffset = iCursor * Uint16Array.BYTES_PER_ELEMENT;
|
|
184
|
+
customVertexCursors.set(dc.customShader, vCursor + dc.vertexCount);
|
|
185
|
+
customIndexCursors.set(dc.customShader, iCursor + dc.indexCount);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
// Phase 2: ensure capacities for the totals (default path).
|
|
189
|
+
this._ensureVertexCapacity(defaultVertices);
|
|
190
|
+
this._ensureIndexCapacity(defaultIndices);
|
|
191
|
+
// Default-path uniform buffer holds (tint vec4 + flags vec4) per draw call;
|
|
192
|
+
// each custom-shader resource manages its own.
|
|
193
|
+
const defaultDrawCalls = this._drawCallCount - this._totalCustomDraws();
|
|
194
|
+
this._ensureUniformCapacity(defaultDrawCalls);
|
|
195
|
+
// Phase 3: pack default-path vertex/index/uniform data.
|
|
196
|
+
const defaultUniformBytes = defaultDrawCalls * this._uniformAlignment;
|
|
197
|
+
const defaultUniformData = defaultUniformBytes > 0 ? new ArrayBuffer(defaultUniformBytes) : null;
|
|
198
|
+
const defaultUniformF32 = defaultUniformData !== null ? new Float32Array(defaultUniformData) : null;
|
|
199
|
+
let defaultUniformIndex = 0;
|
|
159
200
|
for (let i = 0; i < this._drawCallCount; i++) {
|
|
160
201
|
const dc = this._drawCalls[i];
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
this.
|
|
202
|
+
if (dc.customShader === null) {
|
|
203
|
+
// Default path: CPU-bake transform into vertex positions.
|
|
204
|
+
this._writeMeshVertices(backend, dc.mesh, dc.vertexByteOffset / vertexStrideBytes, /* bake */ true);
|
|
205
|
+
if (dc.mesh.indices !== null) {
|
|
206
|
+
this._packedIndexData.set(dc.mesh.indices, dc.indexByteOffset / Uint16Array.BYTES_PER_ELEMENT);
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
const start = dc.indexByteOffset / Uint16Array.BYTES_PER_ELEMENT;
|
|
210
|
+
for (let j = 0; j < dc.indexCount; j++) {
|
|
211
|
+
this._packedIndexData[start + j] = j;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
// Pack tint+flags for default path.
|
|
215
|
+
if (defaultUniformF32 !== null) {
|
|
216
|
+
const offsetWords = (defaultUniformIndex * this._uniformAlignment) / Float32Array.BYTES_PER_ELEMENT;
|
|
217
|
+
const tint = dc.mesh.tint;
|
|
218
|
+
defaultUniformF32[offsetWords + 0] = tint.red;
|
|
219
|
+
defaultUniformF32[offsetWords + 1] = tint.green;
|
|
220
|
+
defaultUniformF32[offsetWords + 2] = tint.blue;
|
|
221
|
+
defaultUniformF32[offsetWords + 3] = tint.alpha;
|
|
222
|
+
defaultUniformF32[offsetWords + 4] = dc.premultiplySample ? 1 : 0;
|
|
223
|
+
defaultUniformF32[offsetWords + 5] = 0;
|
|
224
|
+
defaultUniformF32[offsetWords + 6] = 0;
|
|
225
|
+
defaultUniformF32[offsetWords + 7] = 0;
|
|
226
|
+
}
|
|
227
|
+
defaultUniformIndex++;
|
|
164
228
|
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
229
|
+
}
|
|
230
|
+
// Phase 3b: pack custom-path vertex/index/uniform data per shader.
|
|
231
|
+
for (const [shader, resources] of this._customShaders) {
|
|
232
|
+
if (resources.drawCount === 0) {
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
this._ensureCustomCapacities(resources);
|
|
236
|
+
// Pack vertices/indices in local space (no CPU bake).
|
|
237
|
+
let vWritten = 0;
|
|
238
|
+
let iWritten = 0;
|
|
239
|
+
let drawCursor = 0;
|
|
240
|
+
for (let i = 0; i < this._drawCallCount; i++) {
|
|
241
|
+
const dc = this._drawCalls[i];
|
|
242
|
+
if (dc.customShader !== shader)
|
|
243
|
+
continue;
|
|
244
|
+
this._writeMeshVerticesIntoBuffer(dc.mesh, vWritten, resources.vertexFloatView, resources.vertexUintView);
|
|
245
|
+
if (dc.mesh.indices !== null) {
|
|
246
|
+
resources.indexData.set(dc.mesh.indices, iWritten);
|
|
247
|
+
}
|
|
248
|
+
else {
|
|
249
|
+
for (let j = 0; j < dc.indexCount; j++) {
|
|
250
|
+
resources.indexData[iWritten + j] = j;
|
|
251
|
+
}
|
|
169
252
|
}
|
|
253
|
+
// Write mesh-uniform slot (proj/trans/tint) with dynamic offset.
|
|
254
|
+
this._writeCustomMeshUniform(shader, resources, drawCursor, dc.mesh, backend);
|
|
255
|
+
vWritten += dc.vertexCount;
|
|
256
|
+
iWritten += dc.indexCount;
|
|
257
|
+
drawCursor++;
|
|
170
258
|
}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
//
|
|
187
|
-
const encoder = device.createCommandEncoder();
|
|
259
|
+
device.queue.writeBuffer(resources.vertexBuffer, 0, resources.vertexData, 0, resources.totalVertices * vertexStrideBytes);
|
|
260
|
+
device.queue.writeBuffer(resources.indexBuffer, 0, resources.indexData.buffer, resources.indexData.byteOffset, resources.totalIndices * Uint16Array.BYTES_PER_ELEMENT);
|
|
261
|
+
// Build/refresh user uniform UBO from shader.uniforms (re-built every
|
|
262
|
+
// frame so mutations to shader.uniforms.X are picked up).
|
|
263
|
+
this._uploadUserUniforms(shader, resources);
|
|
264
|
+
}
|
|
265
|
+
// Phase 4: single writeBuffer per resource for the default path.
|
|
266
|
+
if (defaultVertices > 0) {
|
|
267
|
+
device.queue.writeBuffer(this._vertexBuffer, 0, this._vertexData, 0, defaultVertices * vertexStrideBytes);
|
|
268
|
+
device.queue.writeBuffer(this._indexBuffer, 0, this._packedIndexData.buffer, this._packedIndexData.byteOffset, defaultIndices * Uint16Array.BYTES_PER_ELEMENT);
|
|
269
|
+
}
|
|
270
|
+
if (defaultUniformData !== null) {
|
|
271
|
+
device.queue.writeBuffer(this._uniformBuffer, 0, defaultUniformData, 0, defaultUniformBytes);
|
|
272
|
+
}
|
|
273
|
+
// Phase 5: single render pass with one drawIndexed per mesh, switching
|
|
274
|
+
// pipeline+bind groups between default and custom paths as needed.
|
|
275
|
+
const encoder = device.createCommandEncoder({ label: 'WebGpuMeshRenderer' });
|
|
188
276
|
const pass = encoder.beginRenderPass({
|
|
189
277
|
colorAttachments: [backend.createColorAttachment()],
|
|
278
|
+
label: 'WebGpuMeshRenderer pass',
|
|
190
279
|
});
|
|
191
280
|
backend.stats.renderPasses++;
|
|
192
281
|
if (scissor !== null) {
|
|
193
282
|
pass.setScissorRect(scissor.x, scissor.y, scissor.width, scissor.height);
|
|
194
283
|
}
|
|
284
|
+
const renderTargetFormat = backend.renderTargetFormat;
|
|
285
|
+
let lastShader = null;
|
|
195
286
|
let lastBlendMode = null;
|
|
196
287
|
let lastFormat = null;
|
|
197
288
|
let lastTexture = null;
|
|
198
|
-
|
|
289
|
+
let defaultDrawCursor = 0;
|
|
290
|
+
const customDrawCursors = new Map();
|
|
199
291
|
for (let i = 0; i < this._drawCallCount; i++) {
|
|
200
292
|
const dc = this._drawCalls[i];
|
|
201
|
-
if (dc.
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
293
|
+
if (dc.customShader === null) {
|
|
294
|
+
// ----- Default path -----
|
|
295
|
+
const needsPipeline = lastShader !== 'default' || dc.blendMode !== lastBlendMode || renderTargetFormat !== lastFormat;
|
|
296
|
+
if (needsPipeline) {
|
|
297
|
+
pass.setPipeline(this._getPipeline({ blendMode: dc.blendMode, format: renderTargetFormat }));
|
|
298
|
+
lastShader = 'default';
|
|
299
|
+
lastBlendMode = dc.blendMode;
|
|
300
|
+
lastFormat = renderTargetFormat;
|
|
301
|
+
// Pipeline switch invalidates bind group state assumptions.
|
|
302
|
+
lastTexture = null;
|
|
303
|
+
}
|
|
304
|
+
pass.setBindGroup(0, this._uniformBindGroup, [defaultDrawCursor * this._uniformAlignment]);
|
|
305
|
+
if (dc.texture !== lastTexture) {
|
|
306
|
+
lastTexture = dc.texture;
|
|
307
|
+
pass.setBindGroup(1, this._getTextureBindGroup(backend, dc.texture));
|
|
308
|
+
}
|
|
309
|
+
pass.setVertexBuffer(0, this._vertexBuffer, dc.vertexByteOffset);
|
|
310
|
+
pass.setIndexBuffer(this._indexBuffer, 'uint16', dc.indexByteOffset);
|
|
311
|
+
pass.drawIndexed(dc.indexCount);
|
|
312
|
+
defaultDrawCursor++;
|
|
205
313
|
}
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
314
|
+
else {
|
|
315
|
+
// ----- Custom path -----
|
|
316
|
+
const resources = this._customShaders.get(dc.customShader);
|
|
317
|
+
const needsPipeline = lastShader !== dc.customShader || dc.blendMode !== lastBlendMode || renderTargetFormat !== lastFormat;
|
|
318
|
+
// Wrap each custom-shader draw in a debug group so capture tools
|
|
319
|
+
// (Spector.js, Chrome DevTools' WebGPU panel) show meaningful
|
|
320
|
+
// labels for the otherwise-anonymous mesh draws inside the
|
|
321
|
+
// batched render pass.
|
|
322
|
+
pass.pushDebugGroup('MeshShader (custom)');
|
|
323
|
+
if (needsPipeline) {
|
|
324
|
+
pass.setPipeline(this._getOrCreateCustomPipeline(resources, dc.blendMode, renderTargetFormat));
|
|
325
|
+
lastShader = dc.customShader;
|
|
326
|
+
lastBlendMode = dc.blendMode;
|
|
327
|
+
lastFormat = renderTargetFormat;
|
|
328
|
+
lastTexture = null;
|
|
329
|
+
// User bind group is shader-scoped; rebind once per shader switch.
|
|
330
|
+
pass.setBindGroup(2, this._buildUserBindGroup(backend, dc.customShader, resources));
|
|
331
|
+
}
|
|
332
|
+
const cursor = customDrawCursors.get(dc.customShader) ?? 0;
|
|
333
|
+
pass.setBindGroup(0, resources.meshUniformBindGroup, [cursor * meshUniformAlignment]);
|
|
334
|
+
if (dc.texture !== lastTexture) {
|
|
335
|
+
lastTexture = dc.texture;
|
|
336
|
+
pass.setBindGroup(1, this._getOrCreateCustomMeshTextureBindGroup(resources, backend, dc.texture));
|
|
337
|
+
}
|
|
338
|
+
pass.setVertexBuffer(0, resources.vertexBuffer, dc.vertexByteOffset);
|
|
339
|
+
pass.setIndexBuffer(resources.indexBuffer, 'uint16', dc.indexByteOffset);
|
|
340
|
+
pass.drawIndexed(dc.indexCount);
|
|
341
|
+
pass.popDebugGroup();
|
|
342
|
+
customDrawCursors.set(dc.customShader, cursor + 1);
|
|
210
343
|
}
|
|
211
|
-
pass.setVertexBuffer(0, this._vertexBuffer, dc.vertexByteOffset);
|
|
212
|
-
pass.setIndexBuffer(this._indexBuffer, 'uint16', dc.indexByteOffset);
|
|
213
|
-
pass.drawIndexed(dc.indexCount);
|
|
214
344
|
backend.stats.batches++;
|
|
215
345
|
backend.stats.drawCalls++;
|
|
216
346
|
}
|
|
217
347
|
pass.end();
|
|
218
348
|
backend.submit(encoder.finish());
|
|
219
|
-
this.
|
|
349
|
+
this._resetFrame();
|
|
220
350
|
}
|
|
221
351
|
destroy() {
|
|
222
352
|
this.disconnect();
|
|
@@ -292,6 +422,15 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
|
|
|
292
422
|
this._textureBindGroupLayout = null;
|
|
293
423
|
this._uniformBindGroupLayout = null;
|
|
294
424
|
this._shaderModule = null;
|
|
425
|
+
// Custom shaders are owned by user code (one MeshShader can be shared
|
|
426
|
+
// across multiple Mesh instances). Their resources are released when the
|
|
427
|
+
// user calls shader.destroy(), which fires our _onDispose callback. On
|
|
428
|
+
// backend disconnect we eagerly release everything to avoid GPU leaks
|
|
429
|
+
// even if the user keeps the shader reference around.
|
|
430
|
+
for (const resources of this._customShaders.values()) {
|
|
431
|
+
this._releaseCustomShaderResources(resources);
|
|
432
|
+
}
|
|
433
|
+
this._customShaders.clear();
|
|
295
434
|
this._device = null;
|
|
296
435
|
this._backend = null;
|
|
297
436
|
this._drawCallCount = 0;
|
|
@@ -299,30 +438,47 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
|
|
|
299
438
|
this._indexBufferCapacity = 0;
|
|
300
439
|
this._uniformBufferCapacity = 0;
|
|
301
440
|
}
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
const a = matrix.a;
|
|
307
|
-
const b = matrix.b;
|
|
308
|
-
const c = matrix.c;
|
|
309
|
-
const d = matrix.d;
|
|
310
|
-
const tx = matrix.x;
|
|
311
|
-
const ty = matrix.y;
|
|
441
|
+
// ---------------------------------------------------------------------------
|
|
442
|
+
// Default-path helpers
|
|
443
|
+
// ---------------------------------------------------------------------------
|
|
444
|
+
_writeMeshVertices(backend, mesh, vertexStart, bake) {
|
|
312
445
|
const vertices = mesh.vertices;
|
|
313
446
|
const uvs = mesh.uvs;
|
|
314
447
|
const colors = mesh.colors;
|
|
315
448
|
const vertexCount = mesh.vertexCount;
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
const
|
|
320
|
-
const
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
449
|
+
if (bake) {
|
|
450
|
+
// Bake (view * globalTransform) into vertex positions on the CPU,
|
|
451
|
+
// matching the primitive renderer's no-uniforms approach.
|
|
452
|
+
const matrix = this._combinedTransform.copy(mesh.getGlobalTransform()).combine(backend.view.getTransform());
|
|
453
|
+
const a = matrix.a;
|
|
454
|
+
const b = matrix.b;
|
|
455
|
+
const c = matrix.c;
|
|
456
|
+
const d = matrix.d;
|
|
457
|
+
const tx = matrix.x;
|
|
458
|
+
const ty = matrix.y;
|
|
459
|
+
for (let i = 0; i < vertexCount; i++) {
|
|
460
|
+
const sourceIndex = i * 2;
|
|
461
|
+
const targetIndex = (vertexStart + i) * wordsPerVertex;
|
|
462
|
+
const px = vertices[sourceIndex];
|
|
463
|
+
const py = vertices[sourceIndex + 1];
|
|
464
|
+
this._float32View[targetIndex + 0] = a * px + b * py + tx;
|
|
465
|
+
this._float32View[targetIndex + 1] = c * px + d * py + ty;
|
|
466
|
+
this._float32View[targetIndex + 2] = uvs !== null ? uvs[sourceIndex] : 0;
|
|
467
|
+
this._float32View[targetIndex + 3] = uvs !== null ? uvs[sourceIndex + 1] : 0;
|
|
468
|
+
this._uint32View[targetIndex + 4] = colors !== null ? colors[i] : 0xffffffff;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
else {
|
|
472
|
+
// Should not happen — default path always bakes. Defensive no-op.
|
|
473
|
+
for (let i = 0; i < vertexCount; i++) {
|
|
474
|
+
const sourceIndex = i * 2;
|
|
475
|
+
const targetIndex = (vertexStart + i) * wordsPerVertex;
|
|
476
|
+
this._float32View[targetIndex + 0] = vertices[sourceIndex];
|
|
477
|
+
this._float32View[targetIndex + 1] = vertices[sourceIndex + 1];
|
|
478
|
+
this._float32View[targetIndex + 2] = uvs !== null ? uvs[sourceIndex] : 0;
|
|
479
|
+
this._float32View[targetIndex + 3] = uvs !== null ? uvs[sourceIndex + 1] : 0;
|
|
480
|
+
this._uint32View[targetIndex + 4] = colors !== null ? colors[i] : 0xffffffff;
|
|
481
|
+
}
|
|
326
482
|
}
|
|
327
483
|
}
|
|
328
484
|
_getPipeline(key) {
|
|
@@ -416,6 +572,9 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
|
|
|
416
572
|
}
|
|
417
573
|
}
|
|
418
574
|
_ensureUniformCapacity(drawCallCount) {
|
|
575
|
+
if (drawCallCount === 0) {
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
419
578
|
const requiredBytes = drawCallCount * this._uniformAlignment;
|
|
420
579
|
if (requiredBytes > this._uniformBufferCapacity) {
|
|
421
580
|
this._uniformBuffer?.destroy();
|
|
@@ -435,6 +594,400 @@ class WebGpuMeshRenderer extends AbstractWebGpuRenderer {
|
|
|
435
594
|
});
|
|
436
595
|
}
|
|
437
596
|
}
|
|
597
|
+
// ---------------------------------------------------------------------------
|
|
598
|
+
// Custom-path helpers
|
|
599
|
+
// ---------------------------------------------------------------------------
|
|
600
|
+
_totalCustomDraws() {
|
|
601
|
+
let total = 0;
|
|
602
|
+
for (const resources of this._customShaders.values()) {
|
|
603
|
+
total += resources.drawCount;
|
|
604
|
+
}
|
|
605
|
+
return total;
|
|
606
|
+
}
|
|
607
|
+
_resetFrame() {
|
|
608
|
+
this._drawCallCount = 0;
|
|
609
|
+
for (const resources of this._customShaders.values()) {
|
|
610
|
+
resources.drawCount = 0;
|
|
611
|
+
resources.totalVertices = 0;
|
|
612
|
+
resources.totalIndices = 0;
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
_getOrCreateCustomShaderResources(shader) {
|
|
616
|
+
let resources = this._customShaders.get(shader);
|
|
617
|
+
if (resources !== undefined) {
|
|
618
|
+
return resources;
|
|
619
|
+
}
|
|
620
|
+
if (this._device === null) {
|
|
621
|
+
throw new Error('WebGpuMeshRenderer is not connected to a backend.');
|
|
622
|
+
}
|
|
623
|
+
if (shader.wgsl === null) {
|
|
624
|
+
throw new Error('MeshShader has no `wgsl` source; cannot render through the WebGPU backend.');
|
|
625
|
+
}
|
|
626
|
+
const device = this._device;
|
|
627
|
+
const shaderModule = device.createShaderModule({ code: shader.wgsl });
|
|
628
|
+
const meshUniformLayout = device.createBindGroupLayout({
|
|
629
|
+
entries: [
|
|
630
|
+
{
|
|
631
|
+
binding: 0,
|
|
632
|
+
visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
|
|
633
|
+
buffer: { type: 'uniform', hasDynamicOffset: true },
|
|
634
|
+
},
|
|
635
|
+
],
|
|
636
|
+
});
|
|
637
|
+
const meshTextureLayout = device.createBindGroupLayout({
|
|
638
|
+
entries: [
|
|
639
|
+
{ binding: 0, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: 'float' } },
|
|
640
|
+
{ binding: 1, visibility: GPUShaderStage.FRAGMENT, sampler: { type: 'filtering' } },
|
|
641
|
+
],
|
|
642
|
+
});
|
|
643
|
+
const userLayout = this._buildUserBindGroupLayout(device, shader);
|
|
644
|
+
const pipelineLayout = device.createPipelineLayout({
|
|
645
|
+
bindGroupLayouts: [meshUniformLayout, meshTextureLayout, userLayout],
|
|
646
|
+
});
|
|
647
|
+
const sampler = device.createSampler({
|
|
648
|
+
magFilter: 'linear',
|
|
649
|
+
minFilter: 'linear',
|
|
650
|
+
addressModeU: 'clamp-to-edge',
|
|
651
|
+
addressModeV: 'clamp-to-edge',
|
|
652
|
+
});
|
|
653
|
+
const initialVertexCount = 64;
|
|
654
|
+
const initialIndexCount = 192;
|
|
655
|
+
const vertexData = new ArrayBuffer(initialVertexCount * vertexStrideBytes);
|
|
656
|
+
resources = {
|
|
657
|
+
shaderModule,
|
|
658
|
+
meshUniformLayout,
|
|
659
|
+
meshTextureLayout,
|
|
660
|
+
userLayout,
|
|
661
|
+
pipelineLayout,
|
|
662
|
+
pipelines: new Map(),
|
|
663
|
+
vertexBuffer: null,
|
|
664
|
+
indexBuffer: null,
|
|
665
|
+
vertexBufferCapacity: 0,
|
|
666
|
+
indexBufferCapacity: 0,
|
|
667
|
+
vertexData,
|
|
668
|
+
vertexFloatView: new Float32Array(vertexData),
|
|
669
|
+
vertexUintView: new Uint32Array(vertexData),
|
|
670
|
+
indexData: new Uint16Array(initialIndexCount),
|
|
671
|
+
meshUniformBuffer: null,
|
|
672
|
+
meshUniformBufferCapacity: 0,
|
|
673
|
+
meshUniformBindGroup: null,
|
|
674
|
+
userUniformBuffer: null,
|
|
675
|
+
userUniformBufferCapacity: 0,
|
|
676
|
+
meshTextureBindGroups: new Map(),
|
|
677
|
+
sampler,
|
|
678
|
+
drawCount: 0,
|
|
679
|
+
totalVertices: 0,
|
|
680
|
+
totalIndices: 0,
|
|
681
|
+
};
|
|
682
|
+
this._customShaders.set(shader, resources);
|
|
683
|
+
// When the user calls shader.destroy(), evict and release.
|
|
684
|
+
shader._onDispose(() => {
|
|
685
|
+
const r = this._customShaders.get(shader);
|
|
686
|
+
if (r !== undefined) {
|
|
687
|
+
this._releaseCustomShaderResources(r);
|
|
688
|
+
this._customShaders.delete(shader);
|
|
689
|
+
}
|
|
690
|
+
});
|
|
691
|
+
return resources;
|
|
692
|
+
}
|
|
693
|
+
_ensureCustomCapacities(resources) {
|
|
694
|
+
const device = this._device;
|
|
695
|
+
// Vertex buffer
|
|
696
|
+
const vertexBytes = resources.totalVertices * vertexStrideBytes;
|
|
697
|
+
if (vertexBytes > resources.vertexData.byteLength) {
|
|
698
|
+
const newSize = Math.max(vertexBytes, resources.vertexData.byteLength * 2);
|
|
699
|
+
resources.vertexData = new ArrayBuffer(newSize);
|
|
700
|
+
resources.vertexFloatView = new Float32Array(resources.vertexData);
|
|
701
|
+
resources.vertexUintView = new Uint32Array(resources.vertexData);
|
|
702
|
+
}
|
|
703
|
+
if (vertexBytes > resources.vertexBufferCapacity) {
|
|
704
|
+
resources.vertexBuffer?.destroy();
|
|
705
|
+
resources.vertexBufferCapacity = Math.max(vertexBytes, resources.vertexBufferCapacity * 2 || vertexStrideBytes);
|
|
706
|
+
resources.vertexBuffer = device.createBuffer({
|
|
707
|
+
size: resources.vertexBufferCapacity,
|
|
708
|
+
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
|
|
709
|
+
});
|
|
710
|
+
}
|
|
711
|
+
// Index buffer
|
|
712
|
+
const indexBytes = resources.totalIndices * Uint16Array.BYTES_PER_ELEMENT;
|
|
713
|
+
if (resources.indexData.length < resources.totalIndices) {
|
|
714
|
+
resources.indexData = new Uint16Array(Math.max(resources.totalIndices, resources.indexData.length * 2));
|
|
715
|
+
}
|
|
716
|
+
if (indexBytes > resources.indexBufferCapacity) {
|
|
717
|
+
resources.indexBuffer?.destroy();
|
|
718
|
+
resources.indexBufferCapacity = Math.max(indexBytes, resources.indexBufferCapacity * 2 || Uint16Array.BYTES_PER_ELEMENT);
|
|
719
|
+
resources.indexBuffer = device.createBuffer({
|
|
720
|
+
size: resources.indexBufferCapacity,
|
|
721
|
+
usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST,
|
|
722
|
+
});
|
|
723
|
+
}
|
|
724
|
+
// Mesh-uniform UBO (proj/trans/tint per draw, 256-byte aligned).
|
|
725
|
+
const meshUniformBytes = resources.drawCount * meshUniformAlignment;
|
|
726
|
+
if (meshUniformBytes > resources.meshUniformBufferCapacity) {
|
|
727
|
+
resources.meshUniformBuffer?.destroy();
|
|
728
|
+
resources.meshUniformBufferCapacity = Math.max(meshUniformBytes, resources.meshUniformBufferCapacity * 2 || meshUniformAlignment);
|
|
729
|
+
resources.meshUniformBuffer = device.createBuffer({
|
|
730
|
+
size: resources.meshUniformBufferCapacity,
|
|
731
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
732
|
+
});
|
|
733
|
+
resources.meshUniformBindGroup = device.createBindGroup({
|
|
734
|
+
layout: resources.meshUniformLayout,
|
|
735
|
+
entries: [
|
|
736
|
+
{
|
|
737
|
+
binding: 0,
|
|
738
|
+
resource: { buffer: resources.meshUniformBuffer, size: customMeshUniformBytes },
|
|
739
|
+
},
|
|
740
|
+
],
|
|
741
|
+
});
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
_writeMeshVerticesIntoBuffer(mesh, vertexStart, floatView, uintView) {
|
|
745
|
+
const vertices = mesh.vertices;
|
|
746
|
+
const uvs = mesh.uvs;
|
|
747
|
+
const colors = mesh.colors;
|
|
748
|
+
const vertexCount = mesh.vertexCount;
|
|
749
|
+
for (let i = 0; i < vertexCount; i++) {
|
|
750
|
+
const sourceIndex = i * 2;
|
|
751
|
+
const targetIndex = (vertexStart + i) * wordsPerVertex;
|
|
752
|
+
floatView[targetIndex + 0] = vertices[sourceIndex];
|
|
753
|
+
floatView[targetIndex + 1] = vertices[sourceIndex + 1];
|
|
754
|
+
floatView[targetIndex + 2] = uvs !== null ? uvs[sourceIndex] : 0;
|
|
755
|
+
floatView[targetIndex + 3] = uvs !== null ? uvs[sourceIndex + 1] : 0;
|
|
756
|
+
uintView[targetIndex + 4] = colors !== null ? colors[i] : 0xffffffff;
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
_writeCustomMeshUniform(_shader, resources, drawCursor, mesh, backend) {
|
|
760
|
+
// Layout: mat3x3 projection (48B) + mat3x3 translation (48B) + vec4 tint (16B) = 112B.
|
|
761
|
+
// WGSL mat3x3 stores 3 vec3 columns padded to vec4 alignment.
|
|
762
|
+
const slotBytes = meshUniformAlignment;
|
|
763
|
+
const slotFloats = slotBytes / Float32Array.BYTES_PER_ELEMENT;
|
|
764
|
+
const data = new Float32Array(slotFloats);
|
|
765
|
+
const proj = backend.view.getTransform();
|
|
766
|
+
const trans = mesh.getGlobalTransform();
|
|
767
|
+
// mat3 (column-major): [a, c, tx | b, d, ty | 0, 0, 1] in 2D.
|
|
768
|
+
// WGSL mat3x3 has each column padded to vec4. Store as:
|
|
769
|
+
// col0 = [a, b, 0, 0] / [c, d, 0, 0] / ...
|
|
770
|
+
// ExoJS Matrix stores: a, b, c, d, x, y. Standard 2D affine is:
|
|
771
|
+
// [a c tx]
|
|
772
|
+
// [b d ty]
|
|
773
|
+
// [0 0 1 ]
|
|
774
|
+
// Column-major mat3: col0 = (a, b, 0), col1 = (c, d, 0), col2 = (tx, ty, 1).
|
|
775
|
+
let off = 0;
|
|
776
|
+
// projection
|
|
777
|
+
data[off + 0] = proj.a;
|
|
778
|
+
data[off + 1] = proj.b;
|
|
779
|
+
data[off + 2] = 0;
|
|
780
|
+
data[off + 3] = 0; // pad
|
|
781
|
+
data[off + 4] = proj.c;
|
|
782
|
+
data[off + 5] = proj.d;
|
|
783
|
+
data[off + 6] = 0;
|
|
784
|
+
data[off + 7] = 0; // pad
|
|
785
|
+
data[off + 8] = proj.x;
|
|
786
|
+
data[off + 9] = proj.y;
|
|
787
|
+
data[off + 10] = 1;
|
|
788
|
+
data[off + 11] = 0; // pad
|
|
789
|
+
off += 12;
|
|
790
|
+
// translation
|
|
791
|
+
data[off + 0] = trans.a;
|
|
792
|
+
data[off + 1] = trans.b;
|
|
793
|
+
data[off + 2] = 0;
|
|
794
|
+
data[off + 3] = 0;
|
|
795
|
+
data[off + 4] = trans.c;
|
|
796
|
+
data[off + 5] = trans.d;
|
|
797
|
+
data[off + 6] = 0;
|
|
798
|
+
data[off + 7] = 0;
|
|
799
|
+
data[off + 8] = trans.x;
|
|
800
|
+
data[off + 9] = trans.y;
|
|
801
|
+
data[off + 10] = 1;
|
|
802
|
+
data[off + 11] = 0;
|
|
803
|
+
off += 12;
|
|
804
|
+
// tint (vec4)
|
|
805
|
+
const tint = mesh.tint;
|
|
806
|
+
data[off + 0] = tint.red;
|
|
807
|
+
data[off + 1] = tint.green;
|
|
808
|
+
data[off + 2] = tint.blue;
|
|
809
|
+
data[off + 3] = tint.alpha;
|
|
810
|
+
this._device.queue.writeBuffer(resources.meshUniformBuffer, drawCursor * slotBytes, data);
|
|
811
|
+
}
|
|
812
|
+
_getOrCreateCustomPipeline(resources, blendMode, format) {
|
|
813
|
+
const cacheKey = `${blendMode}:${format}`;
|
|
814
|
+
let pipeline = resources.pipelines.get(cacheKey);
|
|
815
|
+
if (pipeline === undefined) {
|
|
816
|
+
pipeline = this._device.createRenderPipeline({
|
|
817
|
+
layout: resources.pipelineLayout,
|
|
818
|
+
vertex: {
|
|
819
|
+
module: resources.shaderModule,
|
|
820
|
+
entryPoint: 'vertexMain',
|
|
821
|
+
buffers: [
|
|
822
|
+
{
|
|
823
|
+
arrayStride: vertexStrideBytes,
|
|
824
|
+
stepMode: 'vertex',
|
|
825
|
+
attributes: [
|
|
826
|
+
{ shaderLocation: 0, offset: 0, format: 'float32x2' },
|
|
827
|
+
{ shaderLocation: 1, offset: 8, format: 'float32x2' },
|
|
828
|
+
{ shaderLocation: 2, offset: 16, format: 'unorm8x4' },
|
|
829
|
+
],
|
|
830
|
+
},
|
|
831
|
+
],
|
|
832
|
+
},
|
|
833
|
+
fragment: {
|
|
834
|
+
module: resources.shaderModule,
|
|
835
|
+
entryPoint: 'fragmentMain',
|
|
836
|
+
targets: [
|
|
837
|
+
{
|
|
838
|
+
format,
|
|
839
|
+
blend: getWebGpuBlendState(blendMode),
|
|
840
|
+
writeMask: GPUColorWrite.ALL,
|
|
841
|
+
},
|
|
842
|
+
],
|
|
843
|
+
},
|
|
844
|
+
primitive: {
|
|
845
|
+
topology: 'triangle-list',
|
|
846
|
+
cullMode: 'none',
|
|
847
|
+
},
|
|
848
|
+
});
|
|
849
|
+
resources.pipelines.set(cacheKey, pipeline);
|
|
850
|
+
}
|
|
851
|
+
return pipeline;
|
|
852
|
+
}
|
|
853
|
+
_getOrCreateCustomMeshTextureBindGroup(resources, backend, texture) {
|
|
854
|
+
let group = resources.meshTextureBindGroups.get(texture);
|
|
855
|
+
if (group === undefined) {
|
|
856
|
+
const binding = backend.getTextureBinding(texture);
|
|
857
|
+
group = this._device.createBindGroup({
|
|
858
|
+
layout: resources.meshTextureLayout,
|
|
859
|
+
entries: [
|
|
860
|
+
{ binding: 0, resource: binding.view },
|
|
861
|
+
{ binding: 1, resource: binding.sampler },
|
|
862
|
+
],
|
|
863
|
+
});
|
|
864
|
+
resources.meshTextureBindGroups.set(texture, group);
|
|
865
|
+
}
|
|
866
|
+
return group;
|
|
867
|
+
}
|
|
868
|
+
_buildUserBindGroupLayout(device, shader) {
|
|
869
|
+
const entries = [];
|
|
870
|
+
const userUniforms = shader.uniforms;
|
|
871
|
+
Object.values(userUniforms).some(v => !isTextureUniform(v));
|
|
872
|
+
// Binding 0 always reserved for the user UBO (even if empty), so the
|
|
873
|
+
// bind-group layout is stable across user-uniform mutations.
|
|
874
|
+
entries.push({
|
|
875
|
+
binding: 0,
|
|
876
|
+
visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
|
|
877
|
+
buffer: { type: 'uniform' },
|
|
878
|
+
});
|
|
879
|
+
let bindingIndex = 1;
|
|
880
|
+
let textureCount = 0;
|
|
881
|
+
for (const value of Object.values(userUniforms)) {
|
|
882
|
+
if (!isTextureUniform(value)) {
|
|
883
|
+
continue;
|
|
884
|
+
}
|
|
885
|
+
if (textureCount >= maxCustomTextureSlots) {
|
|
886
|
+
throw new Error(`MeshShader requested more than ${maxCustomTextureSlots} user texture uniforms.`);
|
|
887
|
+
}
|
|
888
|
+
entries.push({
|
|
889
|
+
binding: bindingIndex,
|
|
890
|
+
visibility: GPUShaderStage.FRAGMENT,
|
|
891
|
+
texture: { sampleType: 'float' },
|
|
892
|
+
});
|
|
893
|
+
bindingIndex++;
|
|
894
|
+
entries.push({
|
|
895
|
+
binding: bindingIndex,
|
|
896
|
+
visibility: GPUShaderStage.FRAGMENT,
|
|
897
|
+
sampler: { type: 'filtering' },
|
|
898
|
+
});
|
|
899
|
+
bindingIndex++;
|
|
900
|
+
textureCount++;
|
|
901
|
+
}
|
|
902
|
+
return device.createBindGroupLayout({ entries });
|
|
903
|
+
}
|
|
904
|
+
_uploadUserUniforms(_shader, resources) {
|
|
905
|
+
const device = this._device;
|
|
906
|
+
const uniforms = _shader.uniforms;
|
|
907
|
+
const scalarValues = Object.values(uniforms).filter(v => !isTextureUniform(v));
|
|
908
|
+
// Always create a UBO (even if empty) since binding 0 of the user layout
|
|
909
|
+
// is fixed. Min size 16 bytes to satisfy WebGPU's minimum buffer size.
|
|
910
|
+
const slotCount = Math.max(scalarValues.length, 1);
|
|
911
|
+
const bufferBytes = slotCount * 16;
|
|
912
|
+
if (resources.userUniformBuffer === null || resources.userUniformBufferCapacity < bufferBytes) {
|
|
913
|
+
resources.userUniformBuffer?.destroy();
|
|
914
|
+
resources.userUniformBufferCapacity = bufferBytes;
|
|
915
|
+
resources.userUniformBuffer = device.createBuffer({
|
|
916
|
+
size: bufferBytes,
|
|
917
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
918
|
+
});
|
|
919
|
+
}
|
|
920
|
+
const data = new Float32Array(bufferBytes / 4);
|
|
921
|
+
let slot = 0;
|
|
922
|
+
for (const value of scalarValues) {
|
|
923
|
+
const baseFloatIndex = slot * 4;
|
|
924
|
+
if (typeof value === 'number') {
|
|
925
|
+
data[baseFloatIndex] = value;
|
|
926
|
+
}
|
|
927
|
+
else if (value instanceof Float32Array) {
|
|
928
|
+
data.set(value, baseFloatIndex);
|
|
929
|
+
}
|
|
930
|
+
else if (value instanceof Int32Array) {
|
|
931
|
+
for (let i = 0; i < value.length; i++) {
|
|
932
|
+
data[baseFloatIndex + i] = value[i];
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
else {
|
|
936
|
+
const arr = value;
|
|
937
|
+
for (let i = 0; i < arr.length; i++) {
|
|
938
|
+
data[baseFloatIndex + i] = arr[i];
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
slot++;
|
|
942
|
+
}
|
|
943
|
+
device.queue.writeBuffer(resources.userUniformBuffer, 0, data);
|
|
944
|
+
}
|
|
945
|
+
_buildUserBindGroup(backend, shader, resources) {
|
|
946
|
+
const device = this._device;
|
|
947
|
+
const entries = [];
|
|
948
|
+
entries.push({ binding: 0, resource: { buffer: resources.userUniformBuffer } });
|
|
949
|
+
let bindingIndex = 1;
|
|
950
|
+
for (const value of Object.values(shader.uniforms)) {
|
|
951
|
+
if (!isTextureUniform(value)) {
|
|
952
|
+
continue;
|
|
953
|
+
}
|
|
954
|
+
const binding = backend.getTextureBinding(value);
|
|
955
|
+
entries.push({ binding: bindingIndex, resource: binding.view });
|
|
956
|
+
bindingIndex++;
|
|
957
|
+
entries.push({ binding: bindingIndex, resource: binding.sampler });
|
|
958
|
+
bindingIndex++;
|
|
959
|
+
}
|
|
960
|
+
return device.createBindGroup({
|
|
961
|
+
layout: resources.userLayout,
|
|
962
|
+
entries,
|
|
963
|
+
});
|
|
964
|
+
}
|
|
965
|
+
_releaseCustomShaderResources(resources) {
|
|
966
|
+
resources.vertexBuffer?.destroy();
|
|
967
|
+
resources.indexBuffer?.destroy();
|
|
968
|
+
resources.meshUniformBuffer?.destroy();
|
|
969
|
+
resources.userUniformBuffer?.destroy();
|
|
970
|
+
resources.pipelines.clear();
|
|
971
|
+
resources.meshTextureBindGroups.clear();
|
|
972
|
+
resources.vertexBuffer = null;
|
|
973
|
+
resources.indexBuffer = null;
|
|
974
|
+
resources.meshUniformBuffer = null;
|
|
975
|
+
resources.userUniformBuffer = null;
|
|
976
|
+
resources.meshUniformBindGroup = null;
|
|
977
|
+
resources.vertexBufferCapacity = 0;
|
|
978
|
+
resources.indexBufferCapacity = 0;
|
|
979
|
+
resources.meshUniformBufferCapacity = 0;
|
|
980
|
+
resources.userUniformBufferCapacity = 0;
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
function isTextureUniform(value) {
|
|
984
|
+
return (typeof value === 'object' &&
|
|
985
|
+
value !== null &&
|
|
986
|
+
'width' in value &&
|
|
987
|
+
'height' in value &&
|
|
988
|
+
!(value instanceof Float32Array) &&
|
|
989
|
+
!(value instanceof Int32Array) &&
|
|
990
|
+
!Array.isArray(value));
|
|
438
991
|
}
|
|
439
992
|
|
|
440
993
|
export { WebGpuMeshRenderer };
|