wgsl-renderer 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,438 @@
1
+
2
+ //#region src/RenderPass.ts
3
+ var RenderPass = class {
4
+ name;
5
+ pipeline;
6
+ bindGroup;
7
+ vertexBuffer;
8
+ clearColor;
9
+ blendMode;
10
+ hasOutputTexture = false;
11
+ constructor(descriptor, device, format, layout) {
12
+ this.name = descriptor.name;
13
+ this.clearColor = descriptor.clearColor || {
14
+ r: 0,
15
+ g: 0,
16
+ b: 0,
17
+ a: 1
18
+ };
19
+ this.blendMode = descriptor.blendMode || "alpha";
20
+ const module$1 = device.createShaderModule({ code: descriptor.shaderCode });
21
+ this.vertexBuffer = device.createBuffer({
22
+ size: 36,
23
+ usage: GPUBufferUsage.VERTEX,
24
+ mappedAtCreation: true
25
+ });
26
+ new Float32Array(this.vertexBuffer.getMappedRange()).set([
27
+ -1,
28
+ -1,
29
+ 0,
30
+ 3,
31
+ -1,
32
+ 0,
33
+ -1,
34
+ 3,
35
+ 0
36
+ ]);
37
+ this.vertexBuffer.unmap();
38
+ this.pipeline = device.createRenderPipeline({
39
+ layout,
40
+ vertex: {
41
+ module: module$1,
42
+ entryPoint: "vs_main",
43
+ buffers: [{
44
+ arrayStride: 12,
45
+ attributes: [{
46
+ shaderLocation: 0,
47
+ offset: 0,
48
+ format: "float32x3"
49
+ }]
50
+ }]
51
+ },
52
+ fragment: {
53
+ module: module$1,
54
+ entryPoint: "fs_main",
55
+ targets: [{
56
+ format,
57
+ blend: this.getBlendState()
58
+ }]
59
+ },
60
+ primitive: { topology: "triangle-list" }
61
+ });
62
+ const bindGroupLayout = this.pipeline.getBindGroupLayout(0);
63
+ this.bindGroup = device.createBindGroup({
64
+ layout: bindGroupLayout,
65
+ entries: descriptor.bindGroupEntries || []
66
+ });
67
+ }
68
+ getBlendState() {
69
+ switch (this.blendMode) {
70
+ case "none": return;
71
+ case "alpha": return {
72
+ color: {
73
+ srcFactor: "src-alpha",
74
+ dstFactor: "one-minus-src-alpha",
75
+ operation: "add"
76
+ },
77
+ alpha: {
78
+ srcFactor: "one",
79
+ dstFactor: "one-minus-src-alpha",
80
+ operation: "add"
81
+ }
82
+ };
83
+ case "additive": return {
84
+ color: {
85
+ srcFactor: "src-alpha",
86
+ dstFactor: "one",
87
+ operation: "add"
88
+ },
89
+ alpha: {
90
+ srcFactor: "one",
91
+ dstFactor: "one",
92
+ operation: "add"
93
+ }
94
+ };
95
+ case "multiply": return {
96
+ color: {
97
+ srcFactor: "src",
98
+ dstFactor: "dst",
99
+ operation: "add"
100
+ },
101
+ alpha: {
102
+ srcFactor: "one",
103
+ dstFactor: "one-minus-src-alpha",
104
+ operation: "add"
105
+ }
106
+ };
107
+ default: return;
108
+ }
109
+ }
110
+ };
111
+
112
+ //#endregion
113
+ //#region src/TextureManager.ts
114
+ var TextureManager = class {
115
+ textures = /* @__PURE__ */ new Map();
116
+ device;
117
+ width;
118
+ height;
119
+ constructor(device, width, height) {
120
+ this.device = device;
121
+ this.width = width;
122
+ this.height = height;
123
+ }
124
+ createTexture(name, format) {
125
+ if (this.textures.has(name)) this.textures.get(name).destroy();
126
+ const texture = this.device.createTexture({
127
+ size: [this.width, this.height],
128
+ format: format || "bgra8unorm",
129
+ usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING
130
+ });
131
+ this.textures.set(name, texture);
132
+ return texture;
133
+ }
134
+ getTexture(name) {
135
+ return this.textures.get(name);
136
+ }
137
+ resize(width, height) {
138
+ if (width === this.width && height === this.height) return;
139
+ this.textures.forEach((texture) => texture.destroy());
140
+ this.textures.clear();
141
+ this.width = width;
142
+ this.height = height;
143
+ }
144
+ destroy() {
145
+ this.textures.forEach((texture) => texture.destroy());
146
+ this.textures.clear();
147
+ }
148
+ getPixelSize() {
149
+ return {
150
+ width: this.width,
151
+ height: this.height
152
+ };
153
+ }
154
+ };
155
+
156
+ //#endregion
157
+ //#region src/index.ts
158
+ var WGSLRenderer = class {
159
+ ctx;
160
+ device;
161
+ format;
162
+ passes = [];
163
+ 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
+ animationFrameId = null;
173
+ constructor(canvas, options) {
174
+ this.canvas = canvas;
175
+ if (!navigator.gpu) throw new Error("WebGPU is not supported in this browser.");
176
+ 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
+ }
201
+ async init() {
202
+ this.device = await (await navigator.gpu.requestAdapter()).requestDevice();
203
+ this.format = navigator.gpu.getPreferredCanvasFormat();
204
+ this.ctx.configure({
205
+ device: this.device,
206
+ format: this.format,
207
+ alphaMode: "opaque"
208
+ });
209
+ const canvasWidth = this.canvas.width || this.canvas.clientWidth;
210
+ const canvasHeight = this.canvas.height || this.canvas.clientHeight;
211
+ this.textureManager = new TextureManager(this.device, canvasWidth, canvasHeight);
212
+ this.ensureBackgroundPass();
213
+ }
214
+ /**
215
+ * Ensure background pass is added
216
+ */
217
+ ensureBackgroundPass() {
218
+ if (!this.backgroundPassAdded) {
219
+ const backgroundPass = new RenderPass({
220
+ name: "builtin_background",
221
+ shaderCode: `
222
+ @vertex
223
+ fn vs_main(@location(0) p: vec3<f32>) -> @builtin(position) vec4<f32> {
224
+ return vec4<f32>(p, 1.0);
225
+ }
226
+
227
+ @fragment
228
+ fn fs_main() -> @location(0) vec4<f32> {
229
+ return vec4<f32>(${this.backgroundColor.r}, ${this.backgroundColor.g}, ${this.backgroundColor.b}, ${this.backgroundColor.a});
230
+ }
231
+ `,
232
+ blendMode: "none",
233
+ clearColor: this.backgroundColor,
234
+ bindGroupEntries: []
235
+ }, this.device, this.format, "auto");
236
+ this.passes.unshift(backgroundPass);
237
+ this.backgroundPassAdded = true;
238
+ this.textureManager.createTexture("pass_0_output", this.format);
239
+ }
240
+ }
241
+ /**
242
+ * Add a render pass to the multi-pass pipeline
243
+ */
244
+ addPass(descriptor) {
245
+ const finalBindGroupEntries = [];
246
+ if (this.passes.length > 0) {
247
+ const previousOutput = this.getPassOutput(this.passes.length - 1);
248
+ if (previousOutput) finalBindGroupEntries.push({
249
+ binding: 0,
250
+ resource: previousOutput.createView()
251
+ });
252
+ }
253
+ descriptor.resources.forEach((resource, index) => {
254
+ finalBindGroupEntries.push({
255
+ binding: index + 1,
256
+ resource
257
+ });
258
+ });
259
+ const pass = new RenderPass({
260
+ name: descriptor.name,
261
+ shaderCode: descriptor.shaderCode,
262
+ clearColor: descriptor.clearColor,
263
+ blendMode: descriptor.blendMode,
264
+ bindGroupEntries: finalBindGroupEntries
265
+ }, this.device, this.format, "auto");
266
+ this.passes.push(pass);
267
+ const currentPassIndex = this.passes.length - 1;
268
+ const textureName = `pass_${currentPassIndex}_output`;
269
+ this.textureManager.createTexture(textureName, this.format);
270
+ this.passes[currentPassIndex].hasOutputTexture = true;
271
+ }
272
+ /**
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
+ * Get the output texture of a specific pass
295
+ */
296
+ getPassOutput(passIndex) {
297
+ if (passIndex < 0 || passIndex >= this.passes.length) return;
298
+ if (passIndex !== 0 && passIndex === this.passes.length - 1) {
299
+ if (!this.passes[passIndex]?.hasOutputTexture) return;
300
+ }
301
+ const textureName = `pass_${passIndex}_output`;
302
+ return this.textureManager.getTexture(textureName);
303
+ }
304
+ /**
305
+ * Create a uniforms
306
+ * @param length The length of the uniform buffer in number of floats
307
+ * @return The uniform object containing the buffer and data array
308
+ */
309
+ createUniforms(length) {
310
+ const values = new Float32Array(Math.ceil(length));
311
+ const buffer = this.device.createBuffer({
312
+ size: values.byteLength,
313
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
314
+ });
315
+ const uniformID = Symbol();
316
+ const uniforms = {
317
+ id: uniformID,
318
+ values,
319
+ apply: () => {
320
+ this.device.queue.writeBuffer(buffer, 0, values.buffer, values.byteOffset, values.byteLength);
321
+ },
322
+ getBuffer: () => buffer
323
+ };
324
+ this.uniforms.set(uniformID, uniforms);
325
+ return uniforms;
326
+ }
327
+ getUniformsByID(id) {
328
+ return this.uniforms.get(id);
329
+ }
330
+ /**
331
+ * Create a sampler
332
+ */
333
+ createSampler(options) {
334
+ return this.device.createSampler(Object.assign({
335
+ magFilter: "linear",
336
+ minFilter: "linear",
337
+ addressModeU: "clamp-to-edge",
338
+ addressModeV: "clamp-to-edge"
339
+ }, options));
340
+ }
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) {
360
+ const resp = fetch(url);
361
+ resp.catch((err) => {
362
+ console.error("Failed to load texture:", err);
363
+ });
364
+ const res = await resp;
365
+ const imgBitmap = await createImageBitmap(await res.blob());
366
+ const texture = this.device.createTexture({
367
+ size: [
368
+ imgBitmap.width,
369
+ imgBitmap.height,
370
+ 1
371
+ ],
372
+ format: "rgba8unorm",
373
+ usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT
374
+ });
375
+ this.device.queue.copyExternalImageToTexture({ source: imgBitmap }, { texture }, [imgBitmap.width, imgBitmap.height]);
376
+ return {
377
+ texture,
378
+ width: imgBitmap.width,
379
+ height: imgBitmap.height
380
+ };
381
+ }
382
+ renderFrame() {
383
+ if (this.passes.length === 0) return;
384
+ 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
+ for (let i = 0; i < this.passes.length; i++) {
389
+ const pass = this.passes[i];
390
+ let renderTarget;
391
+ let loadOp = "clear";
392
+ if (i === this.passes.length - 1) renderTarget = this.ctx.getCurrentTexture().createView();
393
+ else {
394
+ const textureName = `pass_${i}_output`;
395
+ const texture = this.textureManager.getTexture(textureName);
396
+ if (!texture) continue;
397
+ renderTarget = texture.createView();
398
+ loadOp = "load";
399
+ }
400
+ const renderPass = commandEncoder.beginRenderPass({ colorAttachments: [{
401
+ view: renderTarget,
402
+ loadOp: i === 0 ? "clear" : loadOp,
403
+ storeOp: "store",
404
+ clearValue: pass.clearColor
405
+ }] });
406
+ renderPass.setPipeline(pass.pipeline);
407
+ if (pass.bindGroup) renderPass.setBindGroup(0, pass.bindGroup);
408
+ renderPass.setVertexBuffer(0, pass.vertexBuffer);
409
+ renderPass.draw(3, 1, 0, 0);
410
+ renderPass.end();
411
+ }
412
+ this.device.queue.submit([commandEncoder.finish()]);
413
+ }
414
+ loopRender(cb) {
415
+ cb?.();
416
+ this.renderFrame();
417
+ this.animationFrameId = requestAnimationFrame(() => this.loopRender(cb));
418
+ }
419
+ stopLoop() {
420
+ if (this.animationFrameId !== null) {
421
+ cancelAnimationFrame(this.animationFrameId);
422
+ this.animationFrameId = null;
423
+ }
424
+ }
425
+ resize(width, height) {
426
+ this.canvas.width = width;
427
+ this.canvas.height = height;
428
+ this.textureManager.resize(width, height);
429
+ }
430
+ };
431
+ async function createWGSLRenderer(cvs, options) {
432
+ const renderer = new WGSLRenderer(cvs, options);
433
+ await renderer.init();
434
+ return renderer;
435
+ }
436
+
437
+ //#endregion
438
+ exports.createWGSLRenderer = createWGSLRenderer;