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