wgsl-renderer 0.0.1 → 0.0.2
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/LICENSE.md +8 -8
- package/README.md +236 -150
- package/dist/cjs/index.js +122 -125
- package/dist/esm/index.js +122 -125
- package/dist/types/index.d.ts +19 -74
- package/dist/types/index.js +122 -125
- package/package.json +1 -1
package/dist/cjs/index.js
CHANGED
|
@@ -8,7 +8,12 @@ var RenderPass = class {
|
|
|
8
8
|
clearColor;
|
|
9
9
|
blendMode;
|
|
10
10
|
hasOutputTexture = false;
|
|
11
|
+
passResources = [];
|
|
12
|
+
device;
|
|
13
|
+
originalBindGroupEntries;
|
|
11
14
|
constructor(descriptor, device, format, layout) {
|
|
15
|
+
this.device = device;
|
|
16
|
+
this.originalBindGroupEntries = [...descriptor.bindGroupEntries || []];
|
|
12
17
|
this.name = descriptor.name;
|
|
13
18
|
this.clearColor = descriptor.clearColor || {
|
|
14
19
|
r: 0,
|
|
@@ -17,8 +22,8 @@ var RenderPass = class {
|
|
|
17
22
|
a: 1
|
|
18
23
|
};
|
|
19
24
|
this.blendMode = descriptor.blendMode || "alpha";
|
|
20
|
-
const module$1 = device.createShaderModule({ code: descriptor.shaderCode });
|
|
21
|
-
this.vertexBuffer = device.createBuffer({
|
|
25
|
+
const module$1 = this.device.createShaderModule({ code: descriptor.shaderCode });
|
|
26
|
+
this.vertexBuffer = this.device.createBuffer({
|
|
22
27
|
size: 36,
|
|
23
28
|
usage: GPUBufferUsage.VERTEX,
|
|
24
29
|
mappedAtCreation: true
|
|
@@ -35,7 +40,7 @@ var RenderPass = class {
|
|
|
35
40
|
0
|
|
36
41
|
]);
|
|
37
42
|
this.vertexBuffer.unmap();
|
|
38
|
-
this.pipeline = device.createRenderPipeline({
|
|
43
|
+
this.pipeline = this.device.createRenderPipeline({
|
|
39
44
|
layout,
|
|
40
45
|
vertex: {
|
|
41
46
|
module: module$1,
|
|
@@ -60,9 +65,20 @@ var RenderPass = class {
|
|
|
60
65
|
primitive: { topology: "triangle-list" }
|
|
61
66
|
});
|
|
62
67
|
const bindGroupLayout = this.pipeline.getBindGroupLayout(0);
|
|
63
|
-
this.bindGroup = device.createBindGroup({
|
|
68
|
+
this.bindGroup = this.device.createBindGroup({
|
|
69
|
+
layout: bindGroupLayout,
|
|
70
|
+
entries: this.originalBindGroupEntries
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Update bind group with new entries (e.g., after texture resize)
|
|
75
|
+
*/
|
|
76
|
+
updateBindGroup(newEntries) {
|
|
77
|
+
const bindGroupLayout = this.pipeline.getBindGroupLayout(0);
|
|
78
|
+
this.originalBindGroupEntries = [...newEntries];
|
|
79
|
+
this.bindGroup = this.device.createBindGroup({
|
|
64
80
|
layout: bindGroupLayout,
|
|
65
|
-
entries:
|
|
81
|
+
entries: newEntries
|
|
66
82
|
});
|
|
67
83
|
}
|
|
68
84
|
getBlendState() {
|
|
@@ -116,13 +132,17 @@ var TextureManager = class {
|
|
|
116
132
|
device;
|
|
117
133
|
width;
|
|
118
134
|
height;
|
|
135
|
+
oldTextures = [];
|
|
119
136
|
constructor(device, width, height) {
|
|
120
137
|
this.device = device;
|
|
121
138
|
this.width = width;
|
|
122
139
|
this.height = height;
|
|
123
140
|
}
|
|
124
141
|
createTexture(name, format) {
|
|
125
|
-
if (this.textures.has(name))
|
|
142
|
+
if (this.textures.has(name)) {
|
|
143
|
+
const oldTexture = this.textures.get(name);
|
|
144
|
+
this.oldTextures.push(oldTexture);
|
|
145
|
+
}
|
|
126
146
|
const texture = this.device.createTexture({
|
|
127
147
|
size: [this.width, this.height],
|
|
128
148
|
format: format || "bgra8unorm",
|
|
@@ -136,14 +156,40 @@ var TextureManager = class {
|
|
|
136
156
|
}
|
|
137
157
|
resize(width, height) {
|
|
138
158
|
if (width === this.width && height === this.height) return;
|
|
139
|
-
this.textures.forEach((texture) => texture.destroy());
|
|
140
|
-
this.textures.clear();
|
|
141
159
|
this.width = width;
|
|
142
160
|
this.height = height;
|
|
161
|
+
this.textures.forEach((texture) => {
|
|
162
|
+
this.oldTextures.push(texture);
|
|
163
|
+
});
|
|
164
|
+
this.textures.clear();
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Recreate a specific texture with current dimensions
|
|
168
|
+
*/
|
|
169
|
+
recreateTexture(name, format) {
|
|
170
|
+
const texture = this.device.createTexture({
|
|
171
|
+
size: [this.width, this.height],
|
|
172
|
+
format: format || "bgra8unorm",
|
|
173
|
+
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING
|
|
174
|
+
});
|
|
175
|
+
this.textures.set(name, texture);
|
|
176
|
+
return texture;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Clean up old textures that are no longer needed
|
|
180
|
+
* Call this after ensuring GPU work is complete
|
|
181
|
+
*/
|
|
182
|
+
cleanupOldTextures() {
|
|
183
|
+
this.oldTextures.forEach((texture) => {
|
|
184
|
+
texture.destroy();
|
|
185
|
+
});
|
|
186
|
+
this.oldTextures.length = 0;
|
|
143
187
|
}
|
|
144
188
|
destroy() {
|
|
145
189
|
this.textures.forEach((texture) => texture.destroy());
|
|
146
190
|
this.textures.clear();
|
|
191
|
+
this.oldTextures.forEach((texture) => texture.destroy());
|
|
192
|
+
this.oldTextures.length = 0;
|
|
147
193
|
}
|
|
148
194
|
getPixelSize() {
|
|
149
195
|
return {
|
|
@@ -161,89 +207,93 @@ var WGSLRenderer = class {
|
|
|
161
207
|
format;
|
|
162
208
|
passes = [];
|
|
163
209
|
textureManager;
|
|
164
|
-
backgroundPassAdded = false;
|
|
165
|
-
backgroundColor = {
|
|
166
|
-
r: .1,
|
|
167
|
-
g: .1,
|
|
168
|
-
b: .1,
|
|
169
|
-
a: 1
|
|
170
|
-
};
|
|
171
|
-
uniforms = /* @__PURE__ */ new Map();
|
|
172
210
|
animationFrameId = null;
|
|
211
|
+
isResizing = false;
|
|
173
212
|
constructor(canvas, options) {
|
|
174
213
|
this.canvas = canvas;
|
|
214
|
+
this.options = options;
|
|
175
215
|
if (!navigator.gpu) throw new Error("WebGPU is not supported in this browser.");
|
|
176
216
|
this.ctx = canvas.getContext("webgpu");
|
|
177
|
-
switch (typeof options?.backgroundColor) {
|
|
178
|
-
case "number":
|
|
179
|
-
const hex = options.backgroundColor;
|
|
180
|
-
this.backgroundColor = {
|
|
181
|
-
r: (hex >> 16 & 255) / 255,
|
|
182
|
-
g: (hex >> 8 & 255) / 255,
|
|
183
|
-
b: (hex & 255) / 255,
|
|
184
|
-
a: 1
|
|
185
|
-
};
|
|
186
|
-
break;
|
|
187
|
-
case "string":
|
|
188
|
-
const m = options.backgroundColor.match(/^#?([\da-f]{2})([\da-f]{2})([\da-f]{2})$/i);
|
|
189
|
-
if (m) this.backgroundColor = {
|
|
190
|
-
r: Number.parseInt(m[1], 16) / 255,
|
|
191
|
-
g: Number.parseInt(m[2], 16) / 255,
|
|
192
|
-
b: Number.parseInt(m[3], 16) / 255,
|
|
193
|
-
a: 1
|
|
194
|
-
};
|
|
195
|
-
break;
|
|
196
|
-
case "object":
|
|
197
|
-
Object.assign(this.backgroundColor, options.backgroundColor);
|
|
198
|
-
break;
|
|
199
|
-
}
|
|
200
217
|
}
|
|
201
218
|
async init() {
|
|
202
219
|
this.device = await (await navigator.gpu.requestAdapter()).requestDevice();
|
|
203
220
|
this.format = navigator.gpu.getPreferredCanvasFormat();
|
|
204
|
-
|
|
221
|
+
const config = Object.assign({
|
|
205
222
|
device: this.device,
|
|
206
223
|
format: this.format,
|
|
207
224
|
alphaMode: "opaque"
|
|
208
|
-
});
|
|
225
|
+
}, this.options?.config);
|
|
226
|
+
this.ctx.configure(config);
|
|
209
227
|
const canvasWidth = this.canvas.width || this.canvas.clientWidth;
|
|
210
228
|
const canvasHeight = this.canvas.height || this.canvas.clientHeight;
|
|
211
229
|
this.textureManager = new TextureManager(this.device, canvasWidth, canvasHeight);
|
|
212
|
-
|
|
230
|
+
}
|
|
231
|
+
async resize(width, height) {
|
|
232
|
+
if (this.isResizing) return;
|
|
233
|
+
const currentSize = this.textureManager.getPixelSize();
|
|
234
|
+
if (currentSize.width === width && currentSize.height === height) return;
|
|
235
|
+
this.isResizing = true;
|
|
236
|
+
this.canvas.width = width;
|
|
237
|
+
this.canvas.height = height;
|
|
238
|
+
const future = this.device.queue.onSubmittedWorkDone();
|
|
239
|
+
future.catch(() => {
|
|
240
|
+
this.isResizing = false;
|
|
241
|
+
});
|
|
242
|
+
await future;
|
|
243
|
+
this.textureManager.cleanupOldTextures();
|
|
244
|
+
this.textureManager.resize(width, height);
|
|
245
|
+
this.updateAllBindGroups();
|
|
246
|
+
this.isResizing = false;
|
|
247
|
+
}
|
|
248
|
+
getContext() {
|
|
249
|
+
return this.ctx;
|
|
250
|
+
}
|
|
251
|
+
getDevice() {
|
|
252
|
+
return this.device;
|
|
213
253
|
}
|
|
214
254
|
/**
|
|
215
|
-
*
|
|
255
|
+
* Update bind groups for all passes after texture resize
|
|
216
256
|
*/
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
257
|
+
updateAllBindGroups() {
|
|
258
|
+
this.recreatePassTextures();
|
|
259
|
+
this.passes.forEach((pass, index) => {
|
|
260
|
+
const finalBindGroupEntries = [];
|
|
261
|
+
if (index >= 1) {
|
|
262
|
+
const previousOutputTextureName = `pass_${index - 1}_output`;
|
|
263
|
+
let previousOutput = this.textureManager.getTexture(previousOutputTextureName);
|
|
264
|
+
if (!previousOutput) previousOutput = this.textureManager.recreateTexture(previousOutputTextureName, this.format);
|
|
265
|
+
finalBindGroupEntries.push({
|
|
266
|
+
binding: 0,
|
|
267
|
+
resource: previousOutput.createView()
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
if (pass.passResources) pass.passResources.forEach((resource, resourceIndex) => {
|
|
271
|
+
finalBindGroupEntries.push({
|
|
272
|
+
binding: resourceIndex + 1,
|
|
273
|
+
resource
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
pass.updateBindGroup(finalBindGroupEntries);
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Recreate all pass output textures after resize
|
|
281
|
+
*/
|
|
282
|
+
recreatePassTextures() {
|
|
283
|
+
this.passes.forEach((pass, index) => {
|
|
284
|
+
if (index < this.passes.length - 1 || pass.hasOutputTexture) {
|
|
285
|
+
const textureName = `pass_${index}_output`;
|
|
286
|
+
this.textureManager.recreateTexture(textureName, this.format);
|
|
287
|
+
}
|
|
288
|
+
});
|
|
240
289
|
}
|
|
241
290
|
/**
|
|
242
291
|
* Add a render pass to the multi-pass pipeline
|
|
243
292
|
*/
|
|
244
293
|
addPass(descriptor) {
|
|
245
294
|
const finalBindGroupEntries = [];
|
|
246
|
-
|
|
295
|
+
const firstPass = this.passes.length === 0;
|
|
296
|
+
if (!firstPass) {
|
|
247
297
|
const previousOutput = this.getPassOutput(this.passes.length - 1);
|
|
248
298
|
if (previousOutput) finalBindGroupEntries.push({
|
|
249
299
|
binding: 0,
|
|
@@ -252,7 +302,7 @@ var WGSLRenderer = class {
|
|
|
252
302
|
}
|
|
253
303
|
descriptor.resources.forEach((resource, index) => {
|
|
254
304
|
finalBindGroupEntries.push({
|
|
255
|
-
binding: index + 1,
|
|
305
|
+
binding: index + (firstPass ? 0 : 1),
|
|
256
306
|
resource
|
|
257
307
|
});
|
|
258
308
|
});
|
|
@@ -263,6 +313,7 @@ var WGSLRenderer = class {
|
|
|
263
313
|
blendMode: descriptor.blendMode,
|
|
264
314
|
bindGroupEntries: finalBindGroupEntries
|
|
265
315
|
}, this.device, this.format, "auto");
|
|
316
|
+
pass.passResources = [...descriptor.resources];
|
|
266
317
|
this.passes.push(pass);
|
|
267
318
|
const currentPassIndex = this.passes.length - 1;
|
|
268
319
|
const textureName = `pass_${currentPassIndex}_output`;
|
|
@@ -270,32 +321,11 @@ var WGSLRenderer = class {
|
|
|
270
321
|
this.passes[currentPassIndex].hasOutputTexture = true;
|
|
271
322
|
}
|
|
272
323
|
/**
|
|
273
|
-
* Set background color
|
|
274
|
-
*/
|
|
275
|
-
setBackgroundColor(r, g, b, a = 1) {
|
|
276
|
-
this.backgroundColor = {
|
|
277
|
-
r,
|
|
278
|
-
g,
|
|
279
|
-
b,
|
|
280
|
-
a
|
|
281
|
-
};
|
|
282
|
-
}
|
|
283
|
-
/**
|
|
284
|
-
* Force create output texture for a specific pass
|
|
285
|
-
* This is useful when you need the output texture immediately after adding a pass
|
|
286
|
-
*/
|
|
287
|
-
createPassOutput(passIndex) {
|
|
288
|
-
if (passIndex < 0 || passIndex >= this.passes.length) return;
|
|
289
|
-
if (passIndex === this.passes.length - 1) return;
|
|
290
|
-
const textureName = `pass_${passIndex}_output`;
|
|
291
|
-
return this.textureManager.createTexture(textureName, this.format);
|
|
292
|
-
}
|
|
293
|
-
/**
|
|
294
324
|
* Get the output texture of a specific pass
|
|
295
325
|
*/
|
|
296
326
|
getPassOutput(passIndex) {
|
|
297
327
|
if (passIndex < 0 || passIndex >= this.passes.length) return;
|
|
298
|
-
if (passIndex
|
|
328
|
+
if (passIndex === this.passes.length - 1) {
|
|
299
329
|
if (!this.passes[passIndex]?.hasOutputTexture) return;
|
|
300
330
|
}
|
|
301
331
|
const textureName = `pass_${passIndex}_output`;
|
|
@@ -312,20 +342,13 @@ var WGSLRenderer = class {
|
|
|
312
342
|
size: values.byteLength,
|
|
313
343
|
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
|
|
314
344
|
});
|
|
315
|
-
|
|
316
|
-
const uniforms = {
|
|
317
|
-
id: uniformID,
|
|
345
|
+
return {
|
|
318
346
|
values,
|
|
319
347
|
apply: () => {
|
|
320
348
|
this.device.queue.writeBuffer(buffer, 0, values.buffer, values.byteOffset, values.byteLength);
|
|
321
349
|
},
|
|
322
350
|
getBuffer: () => buffer
|
|
323
351
|
};
|
|
324
|
-
this.uniforms.set(uniformID, uniforms);
|
|
325
|
-
return uniforms;
|
|
326
|
-
}
|
|
327
|
-
getUniformsByID(id) {
|
|
328
|
-
return this.uniforms.get(id);
|
|
329
352
|
}
|
|
330
353
|
/**
|
|
331
354
|
* Create a sampler
|
|
@@ -338,25 +361,7 @@ var WGSLRenderer = class {
|
|
|
338
361
|
addressModeV: "clamp-to-edge"
|
|
339
362
|
}, options));
|
|
340
363
|
}
|
|
341
|
-
|
|
342
|
-
* Create a bind group entry for texture
|
|
343
|
-
*/
|
|
344
|
-
createTextureBinding(texture) {
|
|
345
|
-
return texture.createView();
|
|
346
|
-
}
|
|
347
|
-
/**
|
|
348
|
-
* Configure multi-pass rendering
|
|
349
|
-
*/
|
|
350
|
-
setupMultiPass(descriptor) {
|
|
351
|
-
this.passes = [];
|
|
352
|
-
this.textureManager.destroy();
|
|
353
|
-
const canvasWidth = this.canvas.width || this.canvas.clientWidth;
|
|
354
|
-
const canvasHeight = this.canvas.height || this.canvas.clientHeight;
|
|
355
|
-
this.textureManager = new TextureManager(this.device, canvasWidth, canvasHeight);
|
|
356
|
-
descriptor.passes.forEach((passDesc) => this.addPass(passDesc));
|
|
357
|
-
if (descriptor.output?.texture && !descriptor.output.writeToCanvas) this.textureManager.createTexture("final_output");
|
|
358
|
-
}
|
|
359
|
-
async loadTexture(url) {
|
|
364
|
+
async loadImageTexture(url) {
|
|
360
365
|
const resp = fetch(url);
|
|
361
366
|
resp.catch((err) => {
|
|
362
367
|
console.error("Failed to load texture:", err);
|
|
@@ -382,9 +387,6 @@ var WGSLRenderer = class {
|
|
|
382
387
|
renderFrame() {
|
|
383
388
|
if (this.passes.length === 0) return;
|
|
384
389
|
const commandEncoder = this.device.createCommandEncoder();
|
|
385
|
-
const canvasWidth = this.canvas.width || this.canvas.clientWidth;
|
|
386
|
-
const canvasHeight = this.canvas.height || this.canvas.clientHeight;
|
|
387
|
-
this.textureManager.resize(canvasWidth, canvasHeight);
|
|
388
390
|
for (let i = 0; i < this.passes.length; i++) {
|
|
389
391
|
const pass = this.passes[i];
|
|
390
392
|
let renderTarget;
|
|
@@ -422,11 +424,6 @@ var WGSLRenderer = class {
|
|
|
422
424
|
this.animationFrameId = null;
|
|
423
425
|
}
|
|
424
426
|
}
|
|
425
|
-
resize(width, height) {
|
|
426
|
-
this.canvas.width = width;
|
|
427
|
-
this.canvas.height = height;
|
|
428
|
-
this.textureManager.resize(width, height);
|
|
429
|
-
}
|
|
430
427
|
};
|
|
431
428
|
async function createWGSLRenderer(cvs, options) {
|
|
432
429
|
const renderer = new WGSLRenderer(cvs, options);
|