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/LICENSE.md +8 -8
- package/README.md +590 -589
- package/README.zh-CN.md +623 -621
- package/dist/cjs/index.js +20 -19
- package/dist/esm/index.d.ts +1 -1
- package/dist/esm/index.js +15 -14
- package/package.json +9 -10
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,
|
|
89
|
-
sampler,
|
|
90
|
-
uniforms.getBuffer(),
|
|
91
|
-
],
|
|
92
|
-
})
|
|
93
|
-
|
|
94
|
-
// Start loop rendering, can update uniforms in callback
|
|
95
|
-
renderer.loopRender(
|
|
96
|
-
|
|
97
|
-
// Update uniforms (Note WebGPU memory alignment rules)
|
|
98
|
-
uniforms.values[0] = canvas.width
|
|
99
|
-
uniforms.values[1] = canvas.height
|
|
100
|
-
uniforms.values[2] = t / 1000
|
|
101
|
-
uniforms.values[3] = 0
|
|
102
|
-
uniforms.values[4] = width
|
|
103
|
-
uniforms.values[5] = height
|
|
104
|
-
uniforms.apply()
|
|
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
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
};
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
const
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
@group(0) @binding(
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
myUniforms.
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
myUniforms.
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
-
|
|
302
|
-
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
renderer.switchBindGroupSet('main', '
|
|
324
|
-
renderer.switchBindGroupSet('main', '
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
await renderer.loadImageTexture('
|
|
334
|
-
await renderer.loadImageTexture('
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
renderer
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
-
|
|
386
|
-
-
|
|
387
|
-
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
renderer.disablePass('
|
|
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
|
-
|
|
482
|
-
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
-
|
|
508
|
-
- Can get pass
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
-
|
|
513
|
-
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
-
|
|
518
|
-
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
renderer.addPass({ name: '
|
|
541
|
-
renderer.addPass({ name: '
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
const
|
|
546
|
-
const
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
renderer.
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
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!
|