wgsl-renderer 0.4.2 → 0.5.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.
package/README.md CHANGED
@@ -1,590 +1,591 @@
1
- # WGSL Multi-Pass Renderer
2
-
3
- English | [中文](./README.zh-CN.md)
4
-
5
- A multi-pass renderer based on WebGPU and WGSL.
6
-
7
- ## ✨ Features
8
-
9
- - 🖼️ **Multi-Pass Rendering** - Support for texture rendering, post-processing effects, and other multi-pass rendering
10
- - ⚡ **High-Performance Rendering Loop** - Support for single-frame rendering and loop rendering modes
11
- - 🛠️ **TypeScript Support** - Complete type definitions and clear API separation
12
- - 🎮 **Uniform System** - Built-in uniform buffer management with dynamic parameter support
13
-
14
- ## 🚀 Quick Start
15
-
16
- ### Installation
17
-
18
- ```bash
19
- npm i wgls-renderer
20
- ```
21
-
22
- ### Add Pass
23
-
24
- ```typescript
25
- import { createWGSLRenderer } from 'wgls-renderer'
26
-
27
- const canvas = document.querySelector('canvas');
28
- const renderer = await createWGSLRenderer(canvas)
29
-
30
- renderer.addPass({
31
- name: 'my-pass',
32
- shaderCode: `
33
- struct VSOut {
34
- @builtin(position) pos: vec4<f32>,
35
- @location(0) uv: vec2<f32>,
36
- };
37
-
38
- @vertex
39
- fn vs_main(@location(0) p: vec3<f32>) -> VSOut {
40
- var o: VSOut;
41
- o.pos = vec4<f32>(p, 1.0);
42
- o.uv = p.xy * 0.5 + vec2<f32>(0.5, 0.5);
43
- o.uv.y = 1.0 - o.uv.y;
44
- return o;
45
- }
46
-
47
- @fragment
48
- fn fs_main(@location(0) uv: vec2<f32>) -> @location(0) vec4<f32> {
49
- return vec4(1.0, 1.0, 0.0, 1.0);
50
- }`,
51
- })
52
-
53
- renderer.renderFrame()
54
- ```
55
-
56
-
57
-
58
- ### Basic Multi-Pass Usage
59
-
60
- ```typescript
61
- import { createWGSLRenderer } from 'wgls-renderer'
62
-
63
- const canvas = document.getElementById('canvas')
64
- const renderer = await createWGSLRenderer(canvas)
65
-
66
- // Create sampler
67
- const sampler = renderer.createSampler()
68
-
69
- // Load image texture
70
- const { texture, width, height } = await renderer.loadImageTexture('image.jpg')
71
-
72
- // Add Pass 1: Render texture
73
- renderer.addPass({
74
- name: 'texture_pass',
75
- shaderCode: textureShader,
76
- resources: [texture, sampler], // binding 0, 1
77
- })
78
-
79
- // Add Pass 2: Post-processing effect
80
- const uniforms = renderer.createUniforms(8) // Create uniform variable binding
81
-
82
- // Get Pass 1 output texture and bind to Pass 2
83
- const texturePassOutput = renderer.getPassTexture('texture_pass')
84
- renderer.addPass({
85
- name: 'post_process',
86
- shaderCode: postProcessShader,
87
- resources: [
88
- texturePassOutput, // @group(0) @binding(0)
89
- sampler, // @group(0) @binding(1)
90
- uniforms.getBuffer(), // @group(0) @binding(2)
91
- ],
92
- })
93
-
94
- // Start loop rendering, can update uniforms in callback
95
- renderer.loopRender((t) => {
96
-
97
- // Update uniforms (Note WebGPU memory alignment rules)
98
- uniforms.values[0] = canvas.width // resolution.x
99
- uniforms.values[1] = canvas.height // resolution.y
100
- uniforms.values[2] = t / 1000 // time
101
- uniforms.values[3] = 0 // padding (leave empty)
102
- uniforms.values[4] = width // textureResolution.x
103
- uniforms.values[5] = height // textureResolution.y
104
- uniforms.apply() // Apply to GPU
105
- })
106
-
107
- // Or manually execute single frame render
108
- renderer.renderFrame()
109
- ```
110
-
111
- ## 🎨 Shader Examples
112
-
113
- ### Pass 1: Texture Rendering
114
-
115
- ```wgsl
116
- // textureShader
117
- struct VSOut {
118
- @builtin(position) pos: vec4<f32>,
119
- @location(0) uv: vec2<f32>,
120
- };
121
-
122
- @vertex
123
- fn vs_main(@location(0) p: vec3<f32>) -> VSOut {
124
- var o: VSOut;
125
- o.pos = vec4<f32>(p, 1.0);
126
- o.uv = p.xy * 0.5 + vec2<f32>(0.5, 0.5);
127
- o.uv.y = 1.0 - o.uv.y;
128
- return o;
129
- }
130
-
131
- @group(0) @binding(0) var myTexture: texture_2d<f32>;
132
- @group(0) @binding(1) var mySampler: sampler;
133
-
134
- @fragment
135
- fn fs_main(@location(0) uv: vec2<f32>) -> @location(0) vec4<f32> {
136
- return textureSample(myTexture, mySampler, uv);
137
- }
138
- ```
139
-
140
- ### Pass 2: Brightness & Contrast Adjustment
141
-
142
- ```wgsl
143
- // postProcessShader
144
- struct Uniforms {
145
- brightness: f32, // offset 0
146
- contrast: f32, // offset 4
147
- saturation: f32, // offset 8
148
- // 4 bytes padding for vec3 alignment
149
- }
150
-
151
- @group(0) @binding(0) var prevTexture: texture_2d<f32>; // Pass 1 output texture
152
- @group(0) @binding(1) var mySampler: sampler;
153
- @group(0) @binding(2) var<uniform> uniforms: Uniforms;
154
-
155
- @fragment
156
- fn fs_main(@location(0) uv: vec2<f32>) -> @location(0) vec4<f32> {
157
- var color = textureSample(prevTexture, mySampler, uv);
158
-
159
- // Apply brightness
160
- color.rgb += uniforms.brightness;
161
-
162
- // Apply contrast
163
- color.rgb = (color.rgb - 0.5) * uniforms.contrast + 0.5;
164
-
165
- // Apply saturation
166
- let gray = dot(color.rgb, vec3<f32>(0.299, 0.587, 0.114));
167
- color.rgb = mix(vec3<f32>(gray), color.rgb, uniforms.saturation);
168
-
169
- return clamp(color, vec4<f32>(0.0), vec4<f32>(1.0));
170
- }
171
- ```
172
-
173
-
174
- ## 📋 API
175
-
176
- ### createWGSLRenderer(canvas, options?)
177
-
178
- Create WGSL renderer instance.
179
-
180
- ```typescript
181
- import { createWGSLRenderer } from 'wgsl-renderer'
182
- const renderer = await createWGSLRenderer(canvas)
183
- ```
184
-
185
- options:
186
-
187
- ```ts
188
- interface WGSLRendererOptions {
189
- config?: GPUCanvasConfiguration;
190
- }
191
- ```
192
-
193
- ### renderer.addPass(passOptions)
194
-
195
- Add a render pass.
196
-
197
- ```ts
198
- interface RenderPassOptions {
199
- name: string;
200
- shaderCode: string;
201
- entryPoints?: {
202
- vertex?: string; // Default is 'vs_main' function
203
- fragment?: string; // Default is 'fs_main' function
204
- };
205
- clearColor?: { r: number; g: number; b: number; a: number };
206
- blendMode?: 'additive' | 'alpha' | 'multiply' | 'none';
207
- resources?: GPUBindingResource[];
208
- bindGroupSets?: { [setName: string]: GPUBindingResource[] }; // Multiple bind group sets
209
- renderToCanvas?: boolean; // Optional render current pass to canvas, default is false and the lastest pass always true.
210
- view?: GPUTextureView; // Optional custom view for this pass, invalid when rederToCanvas is ture.
211
- format?: GPUTextureFormat; // Optional format for the view (required when using custom view with different format)
212
- }
213
- ```
214
-
215
- ### renderer.getPassTexture(passName)
216
-
217
- Get the output texture of the specified pass. The return value is not a real texture but a placeholder that automatically binds the output texture to the shader during actual rendering.
218
-
219
- ```typescript
220
- // Get output texture of my_pass
221
- const passOutputTexture = renderer.getPassTexture('my_pass')
222
- const sampler = renderer.createSampler()
223
- renderer.addPass({
224
- name: 'my_pass2',
225
- shaderCode: wgslShaderCode,
226
- resources: [
227
- passOutputTexture,
228
- sampler,
229
- ],
230
- })
231
- ```
232
-
233
- **Corresponding WGSL binding:**
234
-
235
- ```wgsl
236
- @group(0) @binding(0) var myTexture: texture_2d<f32>; // resources[0]
237
- @group(0) @binding(1) var mySampler: sampler; // resources[1]
238
- ```
239
-
240
-
241
-
242
- ### renderer.createUniforms(length)
243
-
244
- Create uniform variables using Float32Array, length unit is the number of floats.
245
-
246
- ```typescript
247
- const myUniforms = renderer.createUniforms(8) // 8 floats
248
-
249
- // Bind to shader
250
- renderer.addPass({
251
- name: 'my_pass',
252
- shaderCode: wgslShaderCode,
253
- resources: [
254
- myUniforms.getBuffer(), // group(0) binding(0) var<uniform>
255
- ],
256
- })
257
-
258
- myUniforms.values[0] = 1.0 // Set value
259
- myUniforms.apply() // Apply to GPU
260
- ```
261
-
262
- ### renderer.getContext()
263
-
264
- Get WebGPU canvas context.
265
-
266
- ```typescript
267
- const context = renderer.getContext()
268
- ```
269
-
270
- ### renderer.getDevice()
271
-
272
- Get WebGPU device object.
273
-
274
- ```typescript
275
- const device = renderer.getDevice()
276
- ```
277
-
278
- ### Render Control
279
-
280
- #### renderer.renderFrame()
281
- Single frame rendering.
282
-
283
- #### renderer.loopRender(callback?)
284
- Built-in loop rendering with per-frame callback for real-time uniform updates.
285
-
286
- ```typescript
287
- renderer.loopRender(time => {
288
-
289
- // Update uniforms every frame
290
- myUniforms.values[2] = time * 0.001
291
- myUniforms.apply()
292
- })
293
- ```
294
-
295
- #### renderer.stopLoop()
296
- Stop loop rendering.
297
-
298
- ### Bind Group Switching
299
-
300
- The renderer supports switching between different bind group sets at runtime. This is useful for:
301
- - Switching between different textures
302
- - Changing shader parameters dynamically
303
- - Implementing multi-material rendering
304
-
305
- #### renderer.switchBindGroupSet(passName, setName)
306
-
307
- Switch to a different bind group set for a specific pass.
308
-
309
- ```typescript
310
- // Add pass with multiple bind group sets
311
- renderer.addPass({
312
- name: 'main',
313
- shaderCode: myShader,
314
- resources: [uniforms, sampler, texture1], // Default resources
315
- bindGroupSets: {
316
- 'material1': [uniforms, sampler, texture1],
317
- 'material2': [uniforms, sampler, texture2],
318
- 'material3': [uniforms, sampler, texture3],
319
- }
320
- });
321
-
322
- // Switch between materials
323
- renderer.switchBindGroupSet('main', 'material1');
324
- renderer.switchBindGroupSet('main', 'material2');
325
- renderer.switchBindGroupSet('main', 'material3');
326
- ```
327
-
328
- **Example: Dynamic Texture Switching**
329
-
330
- ```typescript
331
- // Create multiple textures
332
- const textures = [
333
- await renderer.loadImageTexture('texture1.png'),
334
- await renderer.loadImageTexture('texture2.png'),
335
- await renderer.loadImageTexture('texture3.png'),
336
- ];
337
-
338
- // Add pass with bind group sets
339
- renderer.addPass({
340
- name: 'renderer',
341
- shaderCode: textureShader,
342
- resources: [uniforms, sampler, textures[0]], // Default
343
- bindGroupSets: {
344
- 'texture0': [uniforms, sampler, textures[0]],
345
- 'texture1': [uniforms, sampler, textures[1]],
346
- 'texture2': [uniforms, sampler, textures[2]],
347
- }
348
- });
349
-
350
- // User controls
351
- document.getElementById('btn1').onclick = () => {
352
- renderer.switchBindGroupSet('renderer', 'texture0');
353
- };
354
- document.getElementById('btn2').onclick = () => {
355
- renderer.switchBindGroupSet('renderer', 'texture1');
356
- };
357
- document.getElementById('btn3').onclick = () => {
358
- renderer.switchBindGroupSet('renderer', 'texture2');
359
- };
360
- ```
361
-
362
- #### renderer.updateBindGroupSetResources(passName, setName, resources)
363
-
364
- Dynamically update or add a bind group set with new resources. This allows runtime modification of bind groups without recreating the entire pass.
365
-
366
- ```typescript
367
- // Update a bind group set with new texture
368
- const newTexture = renderer.createTexture({ /* options */ });
369
- renderer.updateBindGroupSetResources('main', 'textureSet', [
370
- uniforms,
371
- sampler,
372
- newTexture,
373
- ]);
374
-
375
- // Create a new bind group set on the fly
376
- renderer.updateBindGroupSetResources('main', 'newSet', [
377
- newUniforms,
378
- newSampler,
379
- anotherTexture,
380
- ]);
381
- renderer.switchBindGroupSet('main', 'newSet');
382
- ```
383
-
384
- This is useful for:
385
- - Streaming textures in real-time
386
- - Updating shader parameters dynamically
387
- - Creating procedural content at runtime
388
- - Memory-efficient resource management
389
-
390
- ## Render Pass Management
391
-
392
- The renderer provides flexible pass management capabilities, allowing you to enable, disable, and remove passes dynamically.
393
-
394
- ### renderer.enablePass(passName)
395
-
396
- Enable a render pass for rendering.
397
-
398
- ```typescript
399
- renderer.enablePass('background-effect');
400
- ```
401
-
402
- ### renderer.disablePass(passName)
403
-
404
- Disable a render pass (it will be skipped during rendering).
405
-
406
- ```typescript
407
- renderer.disablePass('post-process');
408
- ```
409
-
410
- ### renderer.isPassEnabled(passName)
411
-
412
- Check if a pass is currently enabled.
413
-
414
- ```typescript
415
- if (renderer.isPassEnabled('main-effect')) {
416
- console.log('Main effect is active');
417
- }
418
- ```
419
-
420
- ### renderer.removePass(passName)
421
-
422
- Permanently remove a render pass from the pipeline.
423
-
424
- ```typescript
425
- const removed = renderer.removePass('debug-pass');
426
- if (removed) {
427
- console.log('Pass successfully removed');
428
- }
429
- ```
430
-
431
- ### renderer.getAllPasses()
432
-
433
- Get all passes (both enabled and disabled).
434
-
435
- ```typescript
436
- const allPasses = renderer.getAllPasses();
437
- allPasses.forEach(pass => {
438
- console.log(`Pass: ${pass.name}, Enabled: ${pass.enabled}`);
439
- });
440
- ```
441
-
442
- ### renderer.getEnabledPasses()
443
-
444
- Get only the enabled passes.
445
-
446
- ```typescript
447
- const activePasses = renderer.getEnabledPasses();
448
- console.log(`Active passes: ${activePasses.length}`);
449
- ```
450
-
451
- ### Pass Management Use Cases
452
-
453
- **Debugging and Development**
454
- ```typescript
455
- // Isolate a specific pass for debugging
456
- renderer.disablePass('post-process');
457
- renderer.disablePass('effects');
458
- // Only background will render
459
-
460
- // Re-enable all passes
461
- const allPasses = renderer.getAllPasses();
462
- allPasses.forEach(pass => renderer.enablePass(pass.name));
463
- ```
464
-
465
- **Performance Optimization**
466
- ```typescript
467
- // Disable expensive effects on low-end devices
468
- if (isLowEndDevice) {
469
- renderer.disablePass('bloom');
470
- renderer.disablePass('ssao');
471
- }
472
- ```
473
-
474
- **Dynamic Feature Toggling**
475
- ```typescript
476
- // UI controls for enabling/disabling effects
477
- document.getElementById('toggle-bloom').onclick = () => {
478
- if (renderer.isPassEnabled('bloom')) {
479
- renderer.disablePass('bloom');
480
- } else {
481
- renderer.enablePass('bloom');
482
- }
483
- };
484
- ```
485
-
486
- ### renderer.createSampler(options?)
487
-
488
- Create sampler with default parameters:
489
-
490
- ```ts
491
- const options = {
492
- magFilter: 'linear',
493
- minFilter: 'linear',
494
- addressModeU: 'clamp-to-edge',
495
- addressModeV: 'clamp-to-edge',
496
- }
497
-
498
- const sampler = renderer.createSampler(options)
499
- ```
500
-
501
- ## 🎯 Pass Flow
502
-
503
- The renderer provides the following management features:
504
-
505
- 1. **User-defined all Passes**
506
- - Users have complete control over all resource binding
507
- - Can get output texture of any pass through `getPassTexture(passName)`
508
- - Can get pass object through `getPassByName(passName)`
509
-
510
- 2. **Texture Management**
511
- - Each pass automatically creates output texture (format: `{passName}_output`)
512
- - Users can manually bind these textures to other passes
513
- - The last pass automatically renders to canvas
514
-
515
- 3. **Complete Flexibility**
516
- - Users decide binding order and method
517
- - Support arbitrarily complex pass connections
518
- - Can create circular dependencies (if needed)
519
-
520
- **Example Usage:**
521
- ```typescript
522
- // Method 1: Simple chain reference
523
- renderer.addPass({
524
- name: 'background',
525
- resources: [bgTexture, sampler1],
526
- })
527
-
528
- renderer.addPass({
529
- name: 'main_effect',
530
- resources: [renderer.getPassTexture('background'), sampler2], // Reference background output
531
- })
532
-
533
- renderer.addPass({
534
- name: 'post_process',
535
- resources: [renderer.getPassTexture('main_effect'), sampler3], // Reference main_effect output
536
- })
537
-
538
- // Method 2: Complex multi-pass blending
539
- renderer.addPass({ name: 'layer1', resources: [textureA, sampler] })
540
- renderer.addPass({ name: 'layer2', resources: [textureB, sampler] })
541
- renderer.addPass({ name: 'layer3', resources: [textureC, sampler] })
542
-
543
- // Create blend pass, referencing multiple different passes simultaneously
544
- const layer1Output = renderer.getPassTexture('layer1')
545
- const layer2Output = renderer.getPassTexture('layer2')
546
- const layer3Output = renderer.getPassTexture('layer3')
547
-
548
- renderer.addPass({
549
- name: 'composite',
550
- resources: [layer1Output, layer2Output, layer3Output, finalSampler],
551
- })
552
-
553
- // Method 3: Dynamic update binding
554
- const mainPass = renderer.getPassByName('main_effect')
555
- if (mainPass) {
556
-
557
- // Dynamically change reference relationship at runtime
558
- mainPass.updateBindGroup([renderer.getPassTexture('layer1'), newSampler])
559
- }
560
- ```
561
-
562
- **Error Handling Example:**
563
- ```typescript
564
- // If referencing non-existent pass, will throw detailed error during rendering
565
- const invalidTexture = renderer.getPassTexture('nonexistent_pass') // This pass doesn't exist
566
- renderer.addPass({
567
- name: 'test',
568
- resources: [invalidTexture, sampler], // Will throw error during rendering
569
- })
570
-
571
- // Error message: Cannot find pass named 'nonexistent_pass'. Available passes: [background, main_effect, ...]
572
- ```
573
-
574
- ## 🛠️ Development
575
-
576
- ```bash
577
- # Development mode
578
- pnpm dev
579
-
580
- # Build
581
- pnpm build
582
- ```
583
-
584
- ## 📝 License
585
-
586
- MIT License
587
-
588
- ## 🤝 Contributing
589
-
1
+ # WGSL Multi-Pass Renderer
2
+
3
+ English | [中文](./README.zh-CN.md)
4
+
5
+ A multi-pass renderer based on WebGPU and WGSL.
6
+
7
+ ## ✨ Features
8
+
9
+ - 🖼️ **Multi-Pass Rendering** - Support for texture rendering, post-processing effects, and other multi-pass rendering
10
+ - ⚡ **High-Performance Rendering Loop** - Support for single-frame rendering and loop rendering modes
11
+ - 🛠️ **TypeScript Support** - Complete type definitions and clear API separation
12
+ - 🎮 **Uniform System** - Built-in uniform buffer management with dynamic parameter support
13
+
14
+ ## 🚀 Quick Start
15
+
16
+ ### Installation
17
+
18
+ ```bash
19
+ npm i wgls-renderer
20
+ ```
21
+
22
+ ### Add Pass
23
+
24
+ ```typescript
25
+ import { createWGSLRenderer } from 'wgls-renderer'
26
+
27
+ const canvas = document.querySelector('canvas')
28
+ const renderer = await createWGSLRenderer(canvas)
29
+
30
+ renderer.addPass({
31
+ name: 'my-pass',
32
+ shaderCode: `
33
+ struct VSOut {
34
+ @builtin(position) pos: vec4<f32>,
35
+ @location(0) uv: vec2<f32>,
36
+ };
37
+
38
+ @vertex
39
+ fn vs_main(@location(0) p: vec3<f32>) -> VSOut {
40
+ var o: VSOut;
41
+ o.pos = vec4<f32>(p, 1.0);
42
+ o.uv = p.xy * 0.5 + vec2<f32>(0.5, 0.5);
43
+ o.uv.y = 1.0 - o.uv.y;
44
+ return o;
45
+ }
46
+
47
+ @fragment
48
+ fn fs_main(@location(0) uv: vec2<f32>) -> @location(0) vec4<f32> {
49
+ return vec4(1.0, 1.0, 0.0, 1.0);
50
+ }`,
51
+ })
52
+
53
+ renderer.renderFrame()
54
+ ```
55
+
56
+
57
+
58
+ ### Basic Multi-Pass Usage
59
+
60
+ ```typescript
61
+ import { createWGSLRenderer } from 'wgls-renderer'
62
+
63
+ const canvas = document.getElementById('canvas')
64
+ const renderer = await createWGSLRenderer(canvas)
65
+
66
+ // Create sampler
67
+ const sampler = renderer.createSampler()
68
+
69
+ // Load image texture
70
+ const { texture, width, height } = await renderer.loadImageTexture('image.jpg')
71
+
72
+ // Add Pass 1: Render texture
73
+ renderer.addPass({
74
+ name: 'texture_pass',
75
+ shaderCode: textureShader,
76
+ resources: [texture, sampler], // binding 0, 1
77
+ })
78
+
79
+ // Add Pass 2: Post-processing effect
80
+ const uniforms = renderer.createUniforms(8) // Create uniform variable binding
81
+
82
+ // Get Pass 1 output texture and bind to Pass 2
83
+ const texturePassOutput = renderer.getPassTexture('texture_pass')
84
+ renderer.addPass({
85
+ name: 'post_process',
86
+ shaderCode: postProcessShader,
87
+ resources: [
88
+ texturePassOutput, // @group(0) @binding(0)
89
+ sampler, // @group(0) @binding(1)
90
+ uniforms.getBuffer(), // @group(0) @binding(2)
91
+ ],
92
+ })
93
+
94
+ // Start loop rendering, can update uniforms in callback
95
+ renderer.loopRender(t => {
96
+
97
+ // Update uniforms (Note WebGPU memory alignment rules)
98
+ uniforms.values[0] = canvas.width // resolution.x
99
+ uniforms.values[1] = canvas.height // resolution.y
100
+ uniforms.values[2] = t / 1000 // time
101
+ uniforms.values[3] = 0 // padding (leave empty)
102
+ uniforms.values[4] = width // textureResolution.x
103
+ uniforms.values[5] = height // textureResolution.y
104
+ uniforms.apply() // Apply to GPU
105
+ })
106
+
107
+ // Or manually execute single frame render
108
+ renderer.renderFrame()
109
+ ```
110
+
111
+ ## 🎨 Shader Examples
112
+
113
+ ### Pass 1: Texture Rendering
114
+
115
+ ```wgsl
116
+ // textureShader
117
+ struct VSOut {
118
+ @builtin(position) pos: vec4<f32>,
119
+ @location(0) uv: vec2<f32>,
120
+ };
121
+
122
+ @vertex
123
+ fn vs_main(@location(0) p: vec3<f32>) -> VSOut {
124
+ var o: VSOut;
125
+ o.pos = vec4<f32>(p, 1.0);
126
+ o.uv = p.xy * 0.5 + vec2<f32>(0.5, 0.5);
127
+ o.uv.y = 1.0 - o.uv.y;
128
+ return o;
129
+ }
130
+
131
+ @group(0) @binding(0) var myTexture: texture_2d<f32>;
132
+ @group(0) @binding(1) var mySampler: sampler;
133
+
134
+ @fragment
135
+ fn fs_main(@location(0) uv: vec2<f32>) -> @location(0) vec4<f32> {
136
+ return textureSample(myTexture, mySampler, uv);
137
+ }
138
+ ```
139
+
140
+ ### Pass 2: Brightness & Contrast Adjustment
141
+
142
+ ```wgsl
143
+ // postProcessShader
144
+ struct Uniforms {
145
+ brightness: f32, // offset 0
146
+ contrast: f32, // offset 4
147
+ saturation: f32, // offset 8
148
+ // 4 bytes padding for vec3 alignment
149
+ }
150
+
151
+ @group(0) @binding(0) var prevTexture: texture_2d<f32>; // Pass 1 output texture
152
+ @group(0) @binding(1) var mySampler: sampler;
153
+ @group(0) @binding(2) var<uniform> uniforms: Uniforms;
154
+
155
+ @fragment
156
+ fn fs_main(@location(0) uv: vec2<f32>) -> @location(0) vec4<f32> {
157
+ var color = textureSample(prevTexture, mySampler, uv);
158
+
159
+ // Apply brightness
160
+ color.rgb += uniforms.brightness;
161
+
162
+ // Apply contrast
163
+ color.rgb = (color.rgb - 0.5) * uniforms.contrast + 0.5;
164
+
165
+ // Apply saturation
166
+ let gray = dot(color.rgb, vec3<f32>(0.299, 0.587, 0.114));
167
+ color.rgb = mix(vec3<f32>(gray), color.rgb, uniforms.saturation);
168
+
169
+ return clamp(color, vec4<f32>(0.0), vec4<f32>(1.0));
170
+ }
171
+ ```
172
+
173
+
174
+ ## 📋 API
175
+
176
+ ### createWGSLRenderer(canvas, options?)
177
+
178
+ Create WGSL renderer instance.
179
+
180
+ ```typescript
181
+ import { createWGSLRenderer } from 'wgsl-renderer'
182
+
183
+ const renderer = await createWGSLRenderer(canvas)
184
+ ```
185
+
186
+ options:
187
+
188
+ ```ts
189
+ interface WGSLRendererOptions { config?: GPUCanvasConfiguration; }
190
+ ```
191
+
192
+ ### renderer.addPass(passOptions)
193
+
194
+ Add a render pass.
195
+
196
+ ```ts
197
+ interface RenderPassOptions {
198
+ name: string;
199
+ shaderCode: string;
200
+ entryPoints?: {
201
+ vertex?: string; // Default is 'vs_main' function
202
+ fragment?: string; // Default is 'fs_main' function
203
+ };
204
+ clearColor?: { r: number; g: number; b: number; a: number };
205
+ blendMode?: 'additive' | 'alpha' | 'multiply' | 'none';
206
+ resources?: GPUBindingResource[];
207
+ bindGroupSets?: { [setName: string]: GPUBindingResource[] }; // Multiple bind group sets
208
+ renderToCanvas?: boolean; // Optional render current pass to canvas, default is false and the lastest pass always true.
209
+ view?: GPUTextureView; // Optional custom view for this pass, invalid when rederToCanvas is ture.
210
+ format?: GPUTextureFormat; // Optional format for the view (required when using custom view with different format)
211
+ }
212
+ ```
213
+
214
+ ### renderer.getPassTexture(passName)
215
+
216
+ Get the output texture of the specified pass. The return value is not a real texture but a placeholder that automatically binds the output texture to the shader during actual rendering.
217
+
218
+ ```typescript
219
+ // Get output texture of my_pass
220
+ const passOutputTexture = renderer.getPassTexture('my_pass')
221
+ const sampler = renderer.createSampler()
222
+ renderer.addPass({
223
+ name: 'my_pass2',
224
+ shaderCode: wgslShaderCode,
225
+ resources: [
226
+ passOutputTexture,
227
+ sampler,
228
+ ],
229
+ })
230
+ ```
231
+
232
+ **Corresponding WGSL binding:**
233
+
234
+ ```wgsl
235
+ @group(0) @binding(0) var myTexture: texture_2d<f32>; // resources[0]
236
+ @group(0) @binding(1) var mySampler: sampler; // resources[1]
237
+ ```
238
+
239
+
240
+
241
+ ### renderer.createUniforms(length)
242
+
243
+ Create uniform variables using Float32Array, length unit is the number of floats.
244
+
245
+ ```typescript
246
+ const myUniforms = renderer.createUniforms(8) // 8 floats
247
+
248
+ // Bind to shader
249
+ renderer.addPass({
250
+ name: 'my_pass',
251
+ shaderCode: wgslShaderCode,
252
+ resources: [
253
+ myUniforms.getBuffer(), // group(0) binding(0) var<uniform>
254
+ ],
255
+ })
256
+
257
+ myUniforms.values[0] = 1.0 // Set value
258
+ myUniforms.apply() // Apply to GPU
259
+ ```
260
+
261
+ ### renderer.getContext()
262
+
263
+ Get WebGPU canvas context.
264
+
265
+ ```typescript
266
+ const context = renderer.getContext()
267
+ ```
268
+
269
+ ### renderer.getDevice()
270
+
271
+ Get WebGPU device object.
272
+
273
+ ```typescript
274
+ const device = renderer.getDevice()
275
+ ```
276
+
277
+ ### Render Control
278
+
279
+ #### renderer.renderFrame()
280
+ Single frame rendering.
281
+
282
+ #### renderer.loopRender(callback?)
283
+ Built-in loop rendering with per-frame callback for real-time uniform updates.
284
+
285
+ ```typescript
286
+ renderer.loopRender(time => {
287
+
288
+ // Update uniforms every frame
289
+ myUniforms.values[2] = time * 0.001
290
+ myUniforms.apply()
291
+ })
292
+ ```
293
+
294
+ #### renderer.stopLoop()
295
+ Stop loop rendering.
296
+
297
+ ### Bind Group Switching
298
+
299
+ The renderer supports switching between different bind group sets at runtime. This is useful for:
300
+ - Switching between different textures
301
+ - Changing shader parameters dynamically
302
+ - Implementing multi-material rendering
303
+
304
+ #### renderer.switchBindGroupSet(passName, setName)
305
+
306
+ Switch to a different bind group set for a specific pass.
307
+
308
+ ```typescript
309
+ // Add pass with multiple bind group sets
310
+ renderer.addPass({
311
+ name: 'main',
312
+ shaderCode: myShader,
313
+ resources: [uniforms, sampler, texture1], // Default resources
314
+ bindGroupSets: {
315
+ material1: [uniforms, sampler, texture1],
316
+ material2: [uniforms, sampler, texture2],
317
+ material3: [uniforms, sampler, texture3],
318
+ },
319
+ })
320
+
321
+ // Switch between materials
322
+ renderer.switchBindGroupSet('main', 'material1')
323
+ renderer.switchBindGroupSet('main', 'material2')
324
+ renderer.switchBindGroupSet('main', 'material3')
325
+ ```
326
+
327
+ **Example: Dynamic Texture Switching**
328
+
329
+ ```typescript
330
+ // Create multiple textures
331
+ const textures = [
332
+ await renderer.loadImageTexture('texture1.png'),
333
+ await renderer.loadImageTexture('texture2.png'),
334
+ await renderer.loadImageTexture('texture3.png'),
335
+ ]
336
+
337
+ // Add pass with bind group sets
338
+ renderer.addPass({
339
+ name: 'renderer',
340
+ shaderCode: textureShader,
341
+ resources: [uniforms, sampler, textures[0]], // Default
342
+ bindGroupSets: {
343
+ texture0: [uniforms, sampler, textures[0]],
344
+ texture1: [uniforms, sampler, textures[1]],
345
+ texture2: [uniforms, sampler, textures[2]],
346
+ },
347
+ })
348
+
349
+ // User controls
350
+ document.getElementById('btn1').onclick = () => {
351
+ renderer.switchBindGroupSet('renderer', 'texture0')
352
+ }
353
+ document.getElementById('btn2').onclick = () => {
354
+ renderer.switchBindGroupSet('renderer', 'texture1')
355
+ }
356
+ document.getElementById('btn3').onclick = () => {
357
+ renderer.switchBindGroupSet('renderer', 'texture2')
358
+ }
359
+ ```
360
+
361
+ #### renderer.updateBindGroupSetResources(passName, setName, resources)
362
+
363
+ Dynamically update or add a bind group set with new resources. This allows runtime modification of bind groups without recreating the entire pass.
364
+
365
+ ```typescript
366
+ // Update a bind group set with new texture
367
+ const newTexture = renderer.createTexture({ /* options */ })
368
+ renderer.updateBindGroupSetResources('main', 'textureSet', [
369
+ uniforms,
370
+ sampler,
371
+ newTexture,
372
+ ])
373
+
374
+ // Create a new bind group set on the fly
375
+ renderer.updateBindGroupSetResources('main', 'newSet', [
376
+ newUniforms,
377
+ newSampler,
378
+ anotherTexture,
379
+ ])
380
+ renderer.switchBindGroupSet('main', 'newSet')
381
+ ```
382
+
383
+ This is useful for:
384
+ - Streaming textures in real-time
385
+ - Updating shader parameters dynamically
386
+ - Creating procedural content at runtime
387
+ - Memory-efficient resource management
388
+
389
+ ## Render Pass Management
390
+
391
+ The renderer provides flexible pass management capabilities, allowing you to enable, disable, and remove passes dynamically.
392
+
393
+ ### renderer.enablePass(passName)
394
+
395
+ Enable a render pass for rendering.
396
+
397
+ ```typescript
398
+ renderer.enablePass('background-effect')
399
+ ```
400
+
401
+ ### renderer.disablePass(passName)
402
+
403
+ Disable a render pass (it will be skipped during rendering).
404
+
405
+ ```typescript
406
+ renderer.disablePass('post-process')
407
+ ```
408
+
409
+ ### renderer.isPassEnabled(passName)
410
+
411
+ Check if a pass is currently enabled.
412
+
413
+ ```typescript
414
+ if (renderer.isPassEnabled('main-effect')) {
415
+ console.log('Main effect is active')
416
+ }
417
+ ```
418
+
419
+ ### renderer.removePass(passName)
420
+
421
+ Permanently remove a render pass from the pipeline.
422
+
423
+ ```typescript
424
+ const removed = renderer.removePass('debug-pass')
425
+ if (removed) {
426
+ console.log('Pass successfully removed')
427
+ }
428
+ ```
429
+
430
+ ### renderer.getAllPasses()
431
+
432
+ Get all passes (both enabled and disabled).
433
+
434
+ ```typescript
435
+ const allPasses = renderer.getAllPasses()
436
+ allPasses.forEach(pass => {
437
+ console.log(`Pass: ${pass.name}, Enabled: ${pass.enabled}`)
438
+ })
439
+ ```
440
+
441
+ ### renderer.getEnabledPasses()
442
+
443
+ Get only the enabled passes.
444
+
445
+ ```typescript
446
+ const activePasses = renderer.getEnabledPasses()
447
+ console.log(`Active passes: ${activePasses.length}`)
448
+ ```
449
+
450
+ ### Pass Management Use Cases
451
+
452
+ **Debugging and Development**
453
+ ```typescript
454
+ // Isolate a specific pass for debugging
455
+ renderer.disablePass('post-process')
456
+ renderer.disablePass('effects')
457
+
458
+ // Only background will render
459
+
460
+ // Re-enable all passes
461
+ const allPasses = renderer.getAllPasses()
462
+ allPasses.forEach(pass => renderer.enablePass(pass.name))
463
+ ```
464
+
465
+ **Performance Optimization**
466
+ ```typescript
467
+ // Disable expensive effects on low-end devices
468
+ if (isLowEndDevice) {
469
+ renderer.disablePass('bloom')
470
+ renderer.disablePass('ssao')
471
+ }
472
+ ```
473
+
474
+ **Dynamic Feature Toggling**
475
+ ```typescript
476
+ // UI controls for enabling/disabling effects
477
+ document.getElementById('toggle-bloom').onclick = () => {
478
+ if (renderer.isPassEnabled('bloom')) {
479
+ renderer.disablePass('bloom')
480
+ }
481
+ else {
482
+ renderer.enablePass('bloom')
483
+ }
484
+ }
485
+ ```
486
+
487
+ ### renderer.createSampler(options?)
488
+
489
+ Create sampler with default parameters:
490
+
491
+ ```ts
492
+ const options = {
493
+ magFilter: 'linear',
494
+ minFilter: 'linear',
495
+ addressModeU: 'clamp-to-edge',
496
+ addressModeV: 'clamp-to-edge',
497
+ }
498
+
499
+ const sampler = renderer.createSampler(options)
500
+ ```
501
+
502
+ ## 🎯 Pass Flow
503
+
504
+ The renderer provides the following management features:
505
+
506
+ 1. **User-defined all Passes**
507
+ - Users have complete control over all resource binding
508
+ - Can get output texture of any pass through `getPassTexture(passName)`
509
+ - Can get pass object through `getPassByName(passName)`
510
+
511
+ 2. **Texture Management**
512
+ - Each pass automatically creates output texture (format: `{passName}_output`)
513
+ - Users can manually bind these textures to other passes
514
+ - The last pass automatically renders to canvas
515
+
516
+ 3. **Complete Flexibility**
517
+ - Users decide binding order and method
518
+ - Support arbitrarily complex pass connections
519
+ - Can create circular dependencies (if needed)
520
+
521
+ **Example Usage:**
522
+ ```typescript
523
+ // Method 1: Simple chain reference
524
+ renderer.addPass({
525
+ name: 'background',
526
+ resources: [bgTexture, sampler1],
527
+ })
528
+
529
+ renderer.addPass({
530
+ name: 'main_effect',
531
+ resources: [renderer.getPassTexture('background'), sampler2], // Reference background output
532
+ })
533
+
534
+ renderer.addPass({
535
+ name: 'post_process',
536
+ resources: [renderer.getPassTexture('main_effect'), sampler3], // Reference main_effect output
537
+ })
538
+
539
+ // Method 2: Complex multi-pass blending
540
+ renderer.addPass({ name: 'layer1', resources: [textureA, sampler] })
541
+ renderer.addPass({ name: 'layer2', resources: [textureB, sampler] })
542
+ renderer.addPass({ name: 'layer3', resources: [textureC, sampler] })
543
+
544
+ // Create blend pass, referencing multiple different passes simultaneously
545
+ const layer1Output = renderer.getPassTexture('layer1')
546
+ const layer2Output = renderer.getPassTexture('layer2')
547
+ const layer3Output = renderer.getPassTexture('layer3')
548
+
549
+ renderer.addPass({
550
+ name: 'composite',
551
+ resources: [layer1Output, layer2Output, layer3Output, finalSampler],
552
+ })
553
+
554
+ // Method 3: Dynamic update binding
555
+ const mainPass = renderer.getPassByName('main_effect')
556
+ if (mainPass) {
557
+
558
+ // Dynamically change reference relationship at runtime
559
+ mainPass.updateBindGroup([renderer.getPassTexture('layer1'), newSampler])
560
+ }
561
+ ```
562
+
563
+ **Error Handling Example:**
564
+ ```typescript
565
+ // If referencing non-existent pass, will throw detailed error during rendering
566
+ const invalidTexture = renderer.getPassTexture('nonexistent_pass') // This pass doesn't exist
567
+ renderer.addPass({
568
+ name: 'test',
569
+ resources: [invalidTexture, sampler], // Will throw error during rendering
570
+ })
571
+
572
+ // Error message: Cannot find pass named 'nonexistent_pass'. Available passes: [background, main_effect, ...]
573
+ ```
574
+
575
+ ## 🛠️ Development
576
+
577
+ ```bash
578
+ # Development mode
579
+ pnpm dev
580
+
581
+ # Build
582
+ pnpm build
583
+ ```
584
+
585
+ ## 📝 License
586
+
587
+ MIT License
588
+
589
+ ## 🤝 Contributing
590
+
590
591
  Issues and Pull Requests are welcome!