wgsl-renderer 0.0.2 → 0.0.4

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.
@@ -1,434 +0,0 @@
1
- //#region src/RenderPass.ts
2
- var RenderPass = class {
3
- name;
4
- pipeline;
5
- bindGroup;
6
- vertexBuffer;
7
- clearColor;
8
- blendMode;
9
- hasOutputTexture = false;
10
- passResources = [];
11
- device;
12
- originalBindGroupEntries;
13
- constructor(descriptor, device, format, layout) {
14
- this.device = device;
15
- this.originalBindGroupEntries = [...descriptor.bindGroupEntries || []];
16
- this.name = descriptor.name;
17
- this.clearColor = descriptor.clearColor || {
18
- r: 0,
19
- g: 0,
20
- b: 0,
21
- a: 1
22
- };
23
- this.blendMode = descriptor.blendMode || "alpha";
24
- const module = this.device.createShaderModule({ code: descriptor.shaderCode });
25
- this.vertexBuffer = this.device.createBuffer({
26
- size: 36,
27
- usage: GPUBufferUsage.VERTEX,
28
- mappedAtCreation: true
29
- });
30
- new Float32Array(this.vertexBuffer.getMappedRange()).set([
31
- -1,
32
- -1,
33
- 0,
34
- 3,
35
- -1,
36
- 0,
37
- -1,
38
- 3,
39
- 0
40
- ]);
41
- this.vertexBuffer.unmap();
42
- this.pipeline = this.device.createRenderPipeline({
43
- layout,
44
- vertex: {
45
- module,
46
- entryPoint: "vs_main",
47
- buffers: [{
48
- arrayStride: 12,
49
- attributes: [{
50
- shaderLocation: 0,
51
- offset: 0,
52
- format: "float32x3"
53
- }]
54
- }]
55
- },
56
- fragment: {
57
- module,
58
- entryPoint: "fs_main",
59
- targets: [{
60
- format,
61
- blend: this.getBlendState()
62
- }]
63
- },
64
- primitive: { topology: "triangle-list" }
65
- });
66
- const bindGroupLayout = this.pipeline.getBindGroupLayout(0);
67
- this.bindGroup = this.device.createBindGroup({
68
- layout: bindGroupLayout,
69
- entries: this.originalBindGroupEntries
70
- });
71
- }
72
- /**
73
- * Update bind group with new entries (e.g., after texture resize)
74
- */
75
- updateBindGroup(newEntries) {
76
- const bindGroupLayout = this.pipeline.getBindGroupLayout(0);
77
- this.originalBindGroupEntries = [...newEntries];
78
- this.bindGroup = this.device.createBindGroup({
79
- layout: bindGroupLayout,
80
- entries: newEntries
81
- });
82
- }
83
- getBlendState() {
84
- switch (this.blendMode) {
85
- case "none": return;
86
- case "alpha": return {
87
- color: {
88
- srcFactor: "src-alpha",
89
- dstFactor: "one-minus-src-alpha",
90
- operation: "add"
91
- },
92
- alpha: {
93
- srcFactor: "one",
94
- dstFactor: "one-minus-src-alpha",
95
- operation: "add"
96
- }
97
- };
98
- case "additive": return {
99
- color: {
100
- srcFactor: "src-alpha",
101
- dstFactor: "one",
102
- operation: "add"
103
- },
104
- alpha: {
105
- srcFactor: "one",
106
- dstFactor: "one",
107
- operation: "add"
108
- }
109
- };
110
- case "multiply": return {
111
- color: {
112
- srcFactor: "src",
113
- dstFactor: "dst",
114
- operation: "add"
115
- },
116
- alpha: {
117
- srcFactor: "one",
118
- dstFactor: "one-minus-src-alpha",
119
- operation: "add"
120
- }
121
- };
122
- default: return;
123
- }
124
- }
125
- };
126
-
127
- //#endregion
128
- //#region src/TextureManager.ts
129
- var TextureManager = class {
130
- textures = /* @__PURE__ */ new Map();
131
- device;
132
- width;
133
- height;
134
- oldTextures = [];
135
- constructor(device, width, height) {
136
- this.device = device;
137
- this.width = width;
138
- this.height = height;
139
- }
140
- createTexture(name, format) {
141
- if (this.textures.has(name)) {
142
- const oldTexture = this.textures.get(name);
143
- this.oldTextures.push(oldTexture);
144
- }
145
- const texture = this.device.createTexture({
146
- size: [this.width, this.height],
147
- format: format || "bgra8unorm",
148
- usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING
149
- });
150
- this.textures.set(name, texture);
151
- return texture;
152
- }
153
- getTexture(name) {
154
- return this.textures.get(name);
155
- }
156
- resize(width, height) {
157
- if (width === this.width && height === this.height) return;
158
- this.width = width;
159
- this.height = height;
160
- this.textures.forEach((texture) => {
161
- this.oldTextures.push(texture);
162
- });
163
- this.textures.clear();
164
- }
165
- /**
166
- * Recreate a specific texture with current dimensions
167
- */
168
- recreateTexture(name, format) {
169
- const texture = this.device.createTexture({
170
- size: [this.width, this.height],
171
- format: format || "bgra8unorm",
172
- usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING
173
- });
174
- this.textures.set(name, texture);
175
- return texture;
176
- }
177
- /**
178
- * Clean up old textures that are no longer needed
179
- * Call this after ensuring GPU work is complete
180
- */
181
- cleanupOldTextures() {
182
- this.oldTextures.forEach((texture) => {
183
- texture.destroy();
184
- });
185
- this.oldTextures.length = 0;
186
- }
187
- destroy() {
188
- this.textures.forEach((texture) => texture.destroy());
189
- this.textures.clear();
190
- this.oldTextures.forEach((texture) => texture.destroy());
191
- this.oldTextures.length = 0;
192
- }
193
- getPixelSize() {
194
- return {
195
- width: this.width,
196
- height: this.height
197
- };
198
- }
199
- };
200
-
201
- //#endregion
202
- //#region src/index.ts
203
- var WGSLRenderer = class {
204
- ctx;
205
- device;
206
- format;
207
- passes = [];
208
- textureManager;
209
- animationFrameId = null;
210
- isResizing = false;
211
- constructor(canvas, options) {
212
- this.canvas = canvas;
213
- this.options = options;
214
- if (!navigator.gpu) throw new Error("WebGPU is not supported in this browser.");
215
- this.ctx = canvas.getContext("webgpu");
216
- }
217
- async init() {
218
- this.device = await (await navigator.gpu.requestAdapter()).requestDevice();
219
- this.format = navigator.gpu.getPreferredCanvasFormat();
220
- const config = Object.assign({
221
- device: this.device,
222
- format: this.format,
223
- alphaMode: "opaque"
224
- }, this.options?.config);
225
- this.ctx.configure(config);
226
- const canvasWidth = this.canvas.width || this.canvas.clientWidth;
227
- const canvasHeight = this.canvas.height || this.canvas.clientHeight;
228
- this.textureManager = new TextureManager(this.device, canvasWidth, canvasHeight);
229
- }
230
- async resize(width, height) {
231
- if (this.isResizing) return;
232
- const currentSize = this.textureManager.getPixelSize();
233
- if (currentSize.width === width && currentSize.height === height) return;
234
- this.isResizing = true;
235
- this.canvas.width = width;
236
- this.canvas.height = height;
237
- const future = this.device.queue.onSubmittedWorkDone();
238
- future.catch(() => {
239
- this.isResizing = false;
240
- });
241
- await future;
242
- this.textureManager.cleanupOldTextures();
243
- this.textureManager.resize(width, height);
244
- this.updateAllBindGroups();
245
- this.isResizing = false;
246
- }
247
- getContext() {
248
- return this.ctx;
249
- }
250
- getDevice() {
251
- return this.device;
252
- }
253
- /**
254
- * Update bind groups for all passes after texture resize
255
- */
256
- updateAllBindGroups() {
257
- this.recreatePassTextures();
258
- this.passes.forEach((pass, index) => {
259
- const finalBindGroupEntries = [];
260
- if (index >= 1) {
261
- const previousOutputTextureName = `pass_${index - 1}_output`;
262
- let previousOutput = this.textureManager.getTexture(previousOutputTextureName);
263
- if (!previousOutput) previousOutput = this.textureManager.recreateTexture(previousOutputTextureName, this.format);
264
- finalBindGroupEntries.push({
265
- binding: 0,
266
- resource: previousOutput.createView()
267
- });
268
- }
269
- if (pass.passResources) pass.passResources.forEach((resource, resourceIndex) => {
270
- finalBindGroupEntries.push({
271
- binding: resourceIndex + 1,
272
- resource
273
- });
274
- });
275
- pass.updateBindGroup(finalBindGroupEntries);
276
- });
277
- }
278
- /**
279
- * Recreate all pass output textures after resize
280
- */
281
- recreatePassTextures() {
282
- this.passes.forEach((pass, index) => {
283
- if (index < this.passes.length - 1 || pass.hasOutputTexture) {
284
- const textureName = `pass_${index}_output`;
285
- this.textureManager.recreateTexture(textureName, this.format);
286
- }
287
- });
288
- }
289
- /**
290
- * Add a render pass to the multi-pass pipeline
291
- */
292
- addPass(descriptor) {
293
- const finalBindGroupEntries = [];
294
- const firstPass = this.passes.length === 0;
295
- if (!firstPass) {
296
- const previousOutput = this.getPassOutput(this.passes.length - 1);
297
- if (previousOutput) finalBindGroupEntries.push({
298
- binding: 0,
299
- resource: previousOutput.createView()
300
- });
301
- }
302
- descriptor.resources.forEach((resource, index) => {
303
- finalBindGroupEntries.push({
304
- binding: index + (firstPass ? 0 : 1),
305
- resource
306
- });
307
- });
308
- const pass = new RenderPass({
309
- name: descriptor.name,
310
- shaderCode: descriptor.shaderCode,
311
- clearColor: descriptor.clearColor,
312
- blendMode: descriptor.blendMode,
313
- bindGroupEntries: finalBindGroupEntries
314
- }, this.device, this.format, "auto");
315
- pass.passResources = [...descriptor.resources];
316
- this.passes.push(pass);
317
- const currentPassIndex = this.passes.length - 1;
318
- const textureName = `pass_${currentPassIndex}_output`;
319
- this.textureManager.createTexture(textureName, this.format);
320
- this.passes[currentPassIndex].hasOutputTexture = true;
321
- }
322
- /**
323
- * Get the output texture of a specific pass
324
- */
325
- getPassOutput(passIndex) {
326
- if (passIndex < 0 || passIndex >= this.passes.length) return;
327
- if (passIndex === this.passes.length - 1) {
328
- if (!this.passes[passIndex]?.hasOutputTexture) return;
329
- }
330
- const textureName = `pass_${passIndex}_output`;
331
- return this.textureManager.getTexture(textureName);
332
- }
333
- /**
334
- * Create a uniforms
335
- * @param length The length of the uniform buffer in number of floats
336
- * @return The uniform object containing the buffer and data array
337
- */
338
- createUniforms(length) {
339
- const values = new Float32Array(Math.ceil(length));
340
- const buffer = this.device.createBuffer({
341
- size: values.byteLength,
342
- usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
343
- });
344
- return {
345
- values,
346
- apply: () => {
347
- this.device.queue.writeBuffer(buffer, 0, values.buffer, values.byteOffset, values.byteLength);
348
- },
349
- getBuffer: () => buffer
350
- };
351
- }
352
- /**
353
- * Create a sampler
354
- */
355
- createSampler(options) {
356
- return this.device.createSampler(Object.assign({
357
- magFilter: "linear",
358
- minFilter: "linear",
359
- addressModeU: "clamp-to-edge",
360
- addressModeV: "clamp-to-edge"
361
- }, options));
362
- }
363
- async loadImageTexture(url) {
364
- const resp = fetch(url);
365
- resp.catch((err) => {
366
- console.error("Failed to load texture:", err);
367
- });
368
- const res = await resp;
369
- const imgBitmap = await createImageBitmap(await res.blob());
370
- const texture = this.device.createTexture({
371
- size: [
372
- imgBitmap.width,
373
- imgBitmap.height,
374
- 1
375
- ],
376
- format: "rgba8unorm",
377
- usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT
378
- });
379
- this.device.queue.copyExternalImageToTexture({ source: imgBitmap }, { texture }, [imgBitmap.width, imgBitmap.height]);
380
- return {
381
- texture,
382
- width: imgBitmap.width,
383
- height: imgBitmap.height
384
- };
385
- }
386
- renderFrame() {
387
- if (this.passes.length === 0) return;
388
- const commandEncoder = this.device.createCommandEncoder();
389
- for (let i = 0; i < this.passes.length; i++) {
390
- const pass = this.passes[i];
391
- let renderTarget;
392
- let loadOp = "clear";
393
- if (i === this.passes.length - 1) renderTarget = this.ctx.getCurrentTexture().createView();
394
- else {
395
- const textureName = `pass_${i}_output`;
396
- const texture = this.textureManager.getTexture(textureName);
397
- if (!texture) continue;
398
- renderTarget = texture.createView();
399
- loadOp = "load";
400
- }
401
- const renderPass = commandEncoder.beginRenderPass({ colorAttachments: [{
402
- view: renderTarget,
403
- loadOp: i === 0 ? "clear" : loadOp,
404
- storeOp: "store",
405
- clearValue: pass.clearColor
406
- }] });
407
- renderPass.setPipeline(pass.pipeline);
408
- if (pass.bindGroup) renderPass.setBindGroup(0, pass.bindGroup);
409
- renderPass.setVertexBuffer(0, pass.vertexBuffer);
410
- renderPass.draw(3, 1, 0, 0);
411
- renderPass.end();
412
- }
413
- this.device.queue.submit([commandEncoder.finish()]);
414
- }
415
- loopRender(cb) {
416
- cb?.();
417
- this.renderFrame();
418
- this.animationFrameId = requestAnimationFrame(() => this.loopRender(cb));
419
- }
420
- stopLoop() {
421
- if (this.animationFrameId !== null) {
422
- cancelAnimationFrame(this.animationFrameId);
423
- this.animationFrameId = null;
424
- }
425
- }
426
- };
427
- async function createWGSLRenderer(cvs, options) {
428
- const renderer = new WGSLRenderer(cvs, options);
429
- await renderer.init();
430
- return renderer;
431
- }
432
-
433
- //#endregion
434
- export { createWGSLRenderer };