wgsl-renderer 0.0.5 → 0.1.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
@@ -198,13 +198,14 @@ Add a render pass.
198
198
  interface RenderPassOptions {
199
199
  name: string;
200
200
  shaderCode: string;
201
- entryPoints?: {
201
+ entryPoints?: {
202
202
  vertex?: string; // Default is 'vs_main' function
203
203
  fragment?: string; // Default is 'fs_main' function
204
204
  };
205
205
  clearColor?: { r: number; g: number; b: number; a: number };
206
206
  blendMode?: 'additive' | 'alpha' | 'multiply' | 'none';
207
207
  resources?: GPUBindingResource[];
208
+ bindGroupSets?: { [setName: string]: GPUBindingResource[] }; // Multiple bind group sets
208
209
  view?: GPUTextureView; // Optional custom view for this pass
209
210
  format?: GPUTextureFormat; // Optional format for the view (required when using custom view with different format)
210
211
  }
@@ -293,7 +294,193 @@ renderer.loopRender(time => {
293
294
  #### renderer.stopLoop()
294
295
  Stop loop rendering.
295
296
 
297
+ ### Bind Group Switching
296
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
+ // Only background will render
458
+
459
+ // Re-enable all passes
460
+ const allPasses = renderer.getAllPasses();
461
+ allPasses.forEach(pass => renderer.enablePass(pass.name));
462
+ ```
463
+
464
+ **Performance Optimization**
465
+ ```typescript
466
+ // Disable expensive effects on low-end devices
467
+ if (isLowEndDevice) {
468
+ renderer.disablePass('bloom');
469
+ renderer.disablePass('ssao');
470
+ }
471
+ ```
472
+
473
+ **Dynamic Feature Toggling**
474
+ ```typescript
475
+ // UI controls for enabling/disabling effects
476
+ document.getElementById('toggle-bloom').onclick = () => {
477
+ if (renderer.isPassEnabled('bloom')) {
478
+ renderer.disablePass('bloom');
479
+ } else {
480
+ renderer.enablePass('bloom');
481
+ }
482
+ };
483
+ ```
297
484
 
298
485
  ### renderer.createSampler(options?)
299
486
 
package/README.zh-CN.md CHANGED
@@ -201,7 +201,7 @@ const renderer = await createWGSLRenderer(canvas)
201
201
 
202
202
  options:
203
203
 
204
- ```ts
204
+ ```typescript
205
205
  interface WGSLRendererOptions {
206
206
  config?: GPUCanvasConfiguration;
207
207
  }
@@ -211,7 +211,7 @@ interface WGSLRendererOptions {
211
211
 
212
212
  添加渲染通道。
213
213
 
214
- ```ts
214
+ ```typescript
215
215
  interface RenderPassOptions {
216
216
  name: string;
217
217
  shaderCode: string;
@@ -221,7 +221,8 @@ interface RenderPassOptions {
221
221
  };
222
222
  clearColor?: { r: number; g: number; b: number; a: number };
223
223
  blendMode?: 'additive' | 'alpha' | 'multiply' | 'none';
224
- resources: GPUBindingResource[];
224
+ resources?: GPUBindingResource[];
225
+ bindGroupSets?: { [setName: string]: GPUBindingResource[] }; // 可选的设置多个绑定组,用于动态切换
225
226
  view?: GPUTextureView; // 可选的自定义View
226
227
  format?: GPUTextureFormat; // 可选的自定义格式(使用自定义View时需要指定格式一致)
227
228
  }
@@ -312,11 +313,214 @@ renderer.loopRender((time) => {
312
313
  #### renderer.stopLoop()
313
314
  停止循环渲染。
314
315
 
316
+ ### 切换绑定组
317
+
318
+ 渲染器支持在运行时在不同的绑定组之间切换,用于:
319
+
320
+ - 切换不同的纹理
321
+ - 动态修改着色器参数
322
+ - 实现多材质渲染
323
+
324
+ #### renderer.switchBindGroupSet(passName, setName)
325
+
326
+ 给指定的通道切换绑定组
327
+
328
+ ```typescript
329
+ // 给渲染通道添加多个绑定组
330
+ renderer.addPass({
331
+ name: 'main',
332
+ shaderCode: myShader,
333
+ resources: [uniforms, sampler, texture1], // Default resources
334
+ bindGroupSets: {
335
+ 'material1': [uniforms, sampler, texture1],
336
+ 'material2': [uniforms, sampler, texture2],
337
+ 'material3': [uniforms, sampler, texture3],
338
+ }
339
+ });
340
+
341
+ // Switch between materials
342
+ renderer.switchBindGroupSet('main', 'material1');
343
+ renderer.switchBindGroupSet('main', 'material2');
344
+ renderer.switchBindGroupSet('main', 'material3');
345
+ ```
346
+
347
+ **示例:动态切换纹理**
348
+
349
+
350
+
351
+
352
+
353
+ ```typescript
354
+ // 创建多个纹理
355
+ const textures = [
356
+ await renderer.loadImageTexture('texture1.png'),
357
+ await renderer.loadImageTexture('texture2.png'),
358
+ await renderer.loadImageTexture('texture3.png'),
359
+ ];
360
+
361
+ // 给渲染通道设置多个绑定
362
+ renderer.addPass({
363
+ name: 'renderer',
364
+ shaderCode: textureShader,
365
+ resources: [uniforms, sampler, textures[0]], // Default
366
+ bindGroupSets: {
367
+ 'texture0': [uniforms, sampler, textures[0]],
368
+ 'texture1': [uniforms, sampler, textures[1]],
369
+ 'texture2': [uniforms, sampler, textures[2]],
370
+ }
371
+ });
372
+
373
+ // 用户控制
374
+ document.getElementById('btn1').onclick = () => {
375
+ renderer.switchBindGroupSet('renderer', 'texture0');
376
+ };
377
+ document.getElementById('btn2').onclick = () => {
378
+ renderer.switchBindGroupSet('renderer', 'texture1');
379
+ };
380
+ document.getElementById('btn3').onclick = () => {
381
+ renderer.switchBindGroupSet('renderer', 'texture2');
382
+ };
383
+ ```
384
+
385
+ #### renderer.updateBindGroupSetResources(passName, setName, resources)
386
+
387
+ 动态增、改设置的绑定组。这可以让你在运行时修改绑定组。
388
+
389
+ ```typescript
390
+ // 添加新纹理到已有绑定组(假设textureSet绑定组已经添加到了main通道)
391
+ const newTexture = renderer.createTexture({ /* options */ });
392
+ renderer.updateBindGroupSetResources('main', 'textureSet', [
393
+ uniforms,
394
+ sampler,
395
+ newTexture,
396
+ ]);
397
+
398
+ // 即时创建一个新的绑定组
399
+ renderer.updateBindGroupSetResources('main', 'newSet', [
400
+ newUniforms,
401
+ newSampler,
402
+ anotherTexture,
403
+ ]);
404
+ renderer.switchBindGroupSet('main', 'newSet');
405
+ ```
406
+
407
+ 可用于:
408
+
409
+ - 实时流式传输纹理
410
+ - 动态更新着色器参数
411
+ - 运行时创建编程式内容
412
+ - 高效内存资源管理
413
+
414
+ ### 渲染通道管理
415
+
416
+ 渲染器提供了灵活的渲染通道管理功能,你可以动态的开启、关闭、移除渲染通道。
417
+
418
+ #### renderer.enablePass(passName)
419
+
420
+ 开启渲染通道。
421
+
422
+ ```typescript
423
+ renderer.enablePass('background-effect');
424
+ ```
425
+
426
+ #### renderer.disablePass(passName)
427
+
428
+ 禁用渲染通道(在渲染过程中将跳过该通道的渲染)。
429
+
430
+ ```typescript
431
+ renderer.disablePass('post-process');
432
+ ```
433
+
434
+ #### renderer.isPassEnabled(passName)
435
+
436
+ 检查通道当前是否开启。
437
+
438
+ ```typescript
439
+ if (renderer.isPassEnabled('main-effect')) {
440
+ console.log('Main effect is active');
441
+ }
442
+ ```
443
+
444
+
445
+
446
+ #### renderer.removePass(passName)
447
+
448
+ 从渲染管线中永久删除渲染通道。
449
+
450
+ ```typescript
451
+ const removed = renderer.removePass('debug-pass');
452
+ if (removed) {
453
+ console.log('Pass successfully removed');
454
+ }
455
+ ```
456
+
457
+ #### renderer.getEnabledPasses()
458
+
459
+ 获取所有开启的渲染通道。
460
+
461
+ ```typescript
462
+ const activePasses = renderer.getEnabledPasses();
463
+ console.log(`Active passes: ${activePasses.length}`);
464
+ ```
465
+
466
+
467
+
468
+ #### renderer.getAllPasses()
469
+
470
+ 获取所有渲染通道(包括开启和禁用的)。
471
+
472
+ ```typescript
473
+ const allPasses = renderer.getAllPasses();
474
+ allPasses.forEach(pass => {
475
+ console.log(`Pass: ${pass.name}, Enabled: ${pass.enabled}`);
476
+ });
477
+ ```
478
+
479
+ #### 通道管理示例
480
+
481
+ **开发调试**
482
+
483
+ ```typescript
484
+ // Isolate a specific pass for debugging
485
+ renderer.disablePass('post-process');
486
+ renderer.disablePass('effects');
487
+ // Only background will render
488
+
489
+ // Re-enable all passes
490
+ const allPasses = renderer.getAllPasses();
491
+ allPasses.forEach(pass => renderer.enablePass(pass.name));
492
+ ```
493
+
494
+ **性能优化**
495
+
496
+ ```typescript
497
+ // Disable expensive effects on low-end devices
498
+ if (isLowEndDevice) {
499
+ renderer.disablePass('bloom');
500
+ renderer.disablePass('ssao');
501
+ }
502
+ ```
503
+
504
+ **动态功能切换**
505
+
506
+ ```typescript
507
+ // UI controls for enabling/disabling effects
508
+ document.getElementById('toggle-bloom').onclick = () => {
509
+ if (renderer.isPassEnabled('bloom')) {
510
+ renderer.disablePass('bloom');
511
+ } else {
512
+ renderer.enablePass('bloom');
513
+ }
514
+ };
515
+ ```
516
+
517
+
518
+
315
519
  ### renderer.createSampler(options?)
316
520
 
317
521
  创建采样器,默认参数:
318
522
 
319
- ```ts
523
+ ```typescript
320
524
  const options = {
321
525
  magFilter: 'linear',
322
526
  minFilter: 'linear',
package/dist/cjs/index.js CHANGED
@@ -10,9 +10,14 @@ var RenderPass = class {
10
10
  view;
11
11
  format;
12
12
  passResources = [];
13
+ bindGroups = {};
14
+ activeBindGroupSet = "default";
13
15
  device;
16
+ descriptor;
17
+ enabled = true;
14
18
  constructor(descriptor, device, format, layout) {
15
19
  this.device = device;
20
+ this.descriptor = descriptor;
16
21
  this.name = descriptor.name;
17
22
  this.clearColor = descriptor.clearColor || {
18
23
  r: 0,
@@ -24,7 +29,10 @@ var RenderPass = class {
24
29
  this.view = descriptor.view;
25
30
  this.format = descriptor.format;
26
31
  const actualFormat = descriptor.format || format;
27
- const module$1 = this.device.createShaderModule({ code: descriptor.shaderCode });
32
+ const module$1 = this.device.createShaderModule({
33
+ code: descriptor.shaderCode,
34
+ label: `Shader for ${descriptor.name}`
35
+ });
28
36
  this.vertexBuffer = this.device.createBuffer({
29
37
  size: 36,
30
38
  usage: GPUBufferUsage.VERTEX,
@@ -79,6 +87,59 @@ var RenderPass = class {
79
87
  layout: bindGroupLayout,
80
88
  entries: newEntries
81
89
  });
90
+ this.bindGroups.default = this.bindGroup;
91
+ }
92
+ /**
93
+ * Update a specific bind group set with new entries
94
+ */
95
+ updateBindGroupSet(setName, newEntries) {
96
+ if (!this.bindGroups[setName]) {
97
+ const bindGroupLayout = this.pipeline.getBindGroupLayout(0);
98
+ this.bindGroups[setName] = this.device.createBindGroup({
99
+ layout: bindGroupLayout,
100
+ entries: newEntries
101
+ });
102
+ }
103
+ }
104
+ /**
105
+ * Switch to a different bind group set
106
+ */
107
+ switchBindGroupSet(setName) {
108
+ if (this.bindGroups[setName]) {
109
+ this.activeBindGroupSet = setName;
110
+ this.bindGroup = this.bindGroups[setName];
111
+ } else throw new Error(`Bind group set '${setName}' not found. Available sets: ${Object.keys(this.bindGroups).join(", ")}`);
112
+ }
113
+ /**
114
+ * Get the current active bind group
115
+ */
116
+ getActiveBindGroup() {
117
+ return this.bindGroups[this.activeBindGroupSet] || this.bindGroup;
118
+ }
119
+ /**
120
+ * Get all available bind group set names
121
+ */
122
+ getBindGroupSets() {
123
+ return Object.keys(this.bindGroups);
124
+ }
125
+ /**
126
+ * Update or add a bind group set with new resources
127
+ * This allows dynamic modification of bind groups at runtime
128
+ */
129
+ updateBindGroupSetResources(setName, resources) {
130
+ const entries = [];
131
+ resources.forEach((resource, index) => {
132
+ if (resource) entries.push({
133
+ binding: index,
134
+ resource
135
+ });
136
+ });
137
+ const bindGroupLayout = this.pipeline.getBindGroupLayout(0);
138
+ this.bindGroups[setName] = this.device.createBindGroup({
139
+ layout: bindGroupLayout,
140
+ entries
141
+ });
142
+ if (this.activeBindGroupSet === setName) this.bindGroup = this.bindGroups[setName];
82
143
  }
83
144
  getBlendState() {
84
145
  switch (this.blendMode) {
@@ -270,6 +331,74 @@ var WGSLRenderer = class {
270
331
  return this.passes.find((pass) => pass.name === passName);
271
332
  }
272
333
  /**
334
+ * Disable a render pass (it will be skipped during rendering)
335
+ */
336
+ disablePass(passName) {
337
+ const pass = this.getPassByName(passName);
338
+ if (!pass) throw new Error(`Cannot find pass named '${passName}'. Available passes: [${this.passes.map((p) => p.name).join(", ")}]`);
339
+ pass.enabled = false;
340
+ }
341
+ /**
342
+ * Enable a render pass
343
+ */
344
+ enablePass(passName) {
345
+ const pass = this.getPassByName(passName);
346
+ if (!pass) throw new Error(`Cannot find pass named '${passName}'. Available passes: [${this.passes.map((p) => p.name).join(", ")}]`);
347
+ pass.enabled = true;
348
+ }
349
+ /**
350
+ * Check if a pass is enabled
351
+ */
352
+ isPassEnabled(passName) {
353
+ const pass = this.getPassByName(passName);
354
+ if (!pass) throw new Error(`Cannot find pass named '${passName}'. Available passes: [${this.passes.map((p) => p.name).join(", ")}]`);
355
+ return pass.enabled;
356
+ }
357
+ /**
358
+ * Remove a render pass permanently
359
+ */
360
+ removePass(passName) {
361
+ const index = this.passes.findIndex((pass) => pass.name === passName);
362
+ if (index !== -1) {
363
+ this.passes.splice(index, 1);
364
+ return true;
365
+ }
366
+ return false;
367
+ }
368
+ /**
369
+ * Get all passes (enabled and disabled)
370
+ */
371
+ getAllPasses() {
372
+ return [...this.passes];
373
+ }
374
+ /**
375
+ * Get only enabled passes
376
+ */
377
+ getEnabledPasses() {
378
+ return this.passes.filter((pass) => pass.enabled);
379
+ }
380
+ /**
381
+ * Switch bind group set for a specific pass
382
+ */
383
+ switchBindGroupSet(passName, setName) {
384
+ const pass = this.getPassByName(passName);
385
+ if (!pass) throw new Error(`Cannot find pass named '${passName}'. Available passes: [${this.passes.map((p) => p.name).join(", ")}]`);
386
+ pass.switchBindGroupSet(setName);
387
+ }
388
+ /**
389
+ * Update bind group set resources for a specific pass
390
+ * This allows dynamic modification of bind groups at runtime
391
+ */
392
+ updateBindGroupSetResources(passName, setName, resources) {
393
+ const pass = this.getPassByName(passName);
394
+ if (!pass) throw new Error(`Cannot find pass named '${passName}'. Available passes: [${this.passes.map((p) => p.name).join(", ")}]`);
395
+ const resolvedResources = resources.map((resource) => {
396
+ if (resource && isPassTextureRef(resource)) return this.resolveResource(resource);
397
+ return resource;
398
+ });
399
+ pass.updateBindGroupSetResources(setName, resolvedResources);
400
+ }
401
+ /**
273
402
  * Add a render pass to the multi-pass pipeline
274
403
  */
275
404
  addPass(descriptor) {
@@ -280,6 +409,11 @@ var WGSLRenderer = class {
280
409
  resource
281
410
  });
282
411
  });
412
+ let bindGroupSetsCopy = void 0;
413
+ if (descriptor.bindGroupSets) {
414
+ bindGroupSetsCopy = {};
415
+ for (const [setName, resources] of Object.entries(descriptor.bindGroupSets)) bindGroupSetsCopy[setName] = [...resources];
416
+ }
283
417
  const internalDescriptor = {
284
418
  name: descriptor.name,
285
419
  shaderCode: descriptor.shaderCode,
@@ -287,6 +421,7 @@ var WGSLRenderer = class {
287
421
  clearColor: descriptor.clearColor,
288
422
  blendMode: descriptor.blendMode,
289
423
  bindGroupEntries: finalBindGroupEntries,
424
+ bindGroupSets: bindGroupSetsCopy,
290
425
  view: descriptor.view,
291
426
  format: descriptor.format
292
427
  };
@@ -317,6 +452,19 @@ var WGSLRenderer = class {
317
452
  });
318
453
  });
319
454
  pass.updateBindGroup(finalBindGroupEntries);
455
+ if (pass.descriptor && pass.descriptor.bindGroupSets) for (const [setName, resources] of Object.entries(pass.descriptor.bindGroupSets)) {
456
+ const entries = [];
457
+ resources.forEach((resource, index) => {
458
+ if (resource) {
459
+ const resolved = this.resolveResource(resource);
460
+ entries.push({
461
+ binding: index,
462
+ resource: resolved
463
+ });
464
+ }
465
+ });
466
+ pass.updateBindGroupSet(setName, entries);
467
+ }
320
468
  });
321
469
  }
322
470
  /**
@@ -349,13 +497,22 @@ var WGSLRenderer = class {
349
497
  addressModeV: "clamp-to-edge"
350
498
  }, options));
351
499
  }
352
- async loadImageTexture(url) {
353
- const resp = fetch(url);
354
- resp.catch((err) => {
355
- console.error("Failed to load texture:", err);
356
- });
357
- const res = await resp;
358
- const future = createImageBitmap(await res.blob());
500
+ async loadImageTexture(image, format) {
501
+ if (typeof image === "string") if (image.startsWith("data:")) {
502
+ const base64Data = image.split(",")[1];
503
+ const binaryString = atob(base64Data);
504
+ const len = binaryString.length;
505
+ const bytes = new Uint8Array(len);
506
+ for (let i = 0; i < len; i++) bytes[i] = binaryString.charCodeAt(i);
507
+ image = new Blob([bytes], { type: "application/octet-stream" });
508
+ } else {
509
+ const resp = fetch(image);
510
+ resp.catch((err) => {
511
+ console.error("Failed to load texture:", err);
512
+ });
513
+ image = await (await resp).blob();
514
+ }
515
+ const future = createImageBitmap(image);
359
516
  future.catch((err) => {
360
517
  console.error("Failed to load texture:", err);
361
518
  });
@@ -366,24 +523,27 @@ var WGSLRenderer = class {
366
523
  imgBitmap.height,
367
524
  1
368
525
  ],
369
- format: "rgba8unorm",
526
+ format: format || "rgba8unorm",
370
527
  usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT
371
528
  });
372
529
  this.device.queue.copyExternalImageToTexture({ source: imgBitmap }, { texture }, [imgBitmap.width, imgBitmap.height]);
373
530
  return {
374
531
  texture,
532
+ bitMap: imgBitmap,
375
533
  width: imgBitmap.width,
376
534
  height: imgBitmap.height
377
535
  };
378
536
  }
379
537
  renderFrame() {
380
538
  if (this.passes.length === 0) return;
539
+ const enabledPasses = this.getEnabledPasses();
540
+ if (enabledPasses.length === 0) return;
381
541
  this.updateBindGroups();
382
542
  const commandEncoder = this.device.createCommandEncoder();
383
- for (let i = 0; i < this.passes.length; i++) {
384
- const pass = this.passes[i];
543
+ for (let i = 0; i < enabledPasses.length; i++) {
544
+ const pass = enabledPasses[i];
385
545
  let loadOp = "load";
386
- const isLast = i === this.passes.length - 1;
546
+ const isLast = i === enabledPasses.length - 1;
387
547
  if (isLast) loadOp = "clear";
388
548
  let renderTarget;
389
549
  if (pass.view) renderTarget = pass.view;
@@ -391,7 +551,7 @@ var WGSLRenderer = class {
391
551
  else {
392
552
  const textureName = `pass_${i}_output`;
393
553
  let texture = this.textureManager.getTexture(textureName);
394
- if (!texture) texture = this.textureManager.createTexture(textureName, "rgba16float");
554
+ if (!texture) texture = this.textureManager.createTexture(textureName, pass.format || this.format);
395
555
  renderTarget = texture.createView();
396
556
  }
397
557
  const renderPass = commandEncoder.beginRenderPass({ colorAttachments: [{
@@ -401,7 +561,8 @@ var WGSLRenderer = class {
401
561
  clearValue: pass.clearColor
402
562
  }] });
403
563
  renderPass.setPipeline(pass.pipeline);
404
- if (pass.bindGroup) renderPass.setBindGroup(0, pass.bindGroup);
564
+ const activeBindGroup = pass.getActiveBindGroup();
565
+ if (activeBindGroup) renderPass.setBindGroup(0, activeBindGroup);
405
566
  renderPass.setVertexBuffer(0, pass.vertexBuffer);
406
567
  renderPass.draw(3, 1, 0, 0);
407
568
  renderPass.end();
@@ -30,6 +30,9 @@ interface RenderPassOptions {
30
30
  };
31
31
  blendMode?: 'additive' | 'alpha' | 'multiply' | 'none';
32
32
  resources?: BandingResource[];
33
+ bindGroupSets?: {
34
+ [setName: string]: BandingResource[];
35
+ };
33
36
  view?: GPUTextureView;
34
37
  format?: GPUTextureFormat;
35
38
  }
@@ -48,6 +51,9 @@ interface InternalRenderPassDescriptor {
48
51
  };
49
52
  blendMode?: 'additive' | 'alpha' | 'multiply' | 'none';
50
53
  bindGroupEntries: BindingEntry[];
54
+ bindGroupSets?: {
55
+ [setName: string]: BandingResource[];
56
+ };
51
57
  view?: GPUTextureView;
52
58
  format?: GPUTextureFormat;
53
59
  }
@@ -66,12 +72,45 @@ declare class RenderPass {
66
72
  view?: GPUTextureView;
67
73
  format?: GPUTextureFormat;
68
74
  passResources: BandingResource[];
75
+ bindGroups: {
76
+ [setName: string]: GPUBindGroup;
77
+ };
78
+ activeBindGroupSet: string;
69
79
  private device;
80
+ descriptor: InternalRenderPassDescriptor;
81
+ enabled: boolean;
70
82
  constructor(descriptor: InternalRenderPassDescriptor, device: GPUDevice, format: GPUTextureFormat, layout: GPUPipelineLayout | 'auto');
71
83
  /**
72
84
  * Update bind group with new entries (e.g., after texture resize)
73
85
  */
74
- updateBindGroup(newEntries: BindingEntry[]): void;
86
+ updateBindGroup(newEntries: {
87
+ binding: number;
88
+ resource: GPUBindingResource;
89
+ }[]): void;
90
+ /**
91
+ * Update a specific bind group set with new entries
92
+ */
93
+ updateBindGroupSet(setName: string, newEntries: {
94
+ binding: number;
95
+ resource: GPUBindingResource;
96
+ }[]): void;
97
+ /**
98
+ * Switch to a different bind group set
99
+ */
100
+ switchBindGroupSet(setName: string): void;
101
+ /**
102
+ * Get the current active bind group
103
+ */
104
+ getActiveBindGroup(): GPUBindGroup | null;
105
+ /**
106
+ * Get all available bind group set names
107
+ */
108
+ getBindGroupSets(): string[];
109
+ /**
110
+ * Update or add a bind group set with new resources
111
+ * This allows dynamic modification of bind groups at runtime
112
+ */
113
+ updateBindGroupSetResources(setName: string, resources: BandingResource[]): void;
75
114
  private getBlendState;
76
115
  }
77
116
  //#endregion
@@ -110,6 +149,39 @@ declare class WGSLRenderer {
110
149
  * Get pass by name
111
150
  */
112
151
  getPassByName(passName: string): RenderPass | undefined;
152
+ /**
153
+ * Disable a render pass (it will be skipped during rendering)
154
+ */
155
+ disablePass(passName: string): void;
156
+ /**
157
+ * Enable a render pass
158
+ */
159
+ enablePass(passName: string): void;
160
+ /**
161
+ * Check if a pass is enabled
162
+ */
163
+ isPassEnabled(passName: string): boolean;
164
+ /**
165
+ * Remove a render pass permanently
166
+ */
167
+ removePass(passName: string): boolean;
168
+ /**
169
+ * Get all passes (enabled and disabled)
170
+ */
171
+ getAllPasses(): RenderPass[];
172
+ /**
173
+ * Get only enabled passes
174
+ */
175
+ getEnabledPasses(): RenderPass[];
176
+ /**
177
+ * Switch bind group set for a specific pass
178
+ */
179
+ switchBindGroupSet(passName: string, setName: string): void;
180
+ /**
181
+ * Update bind group set resources for a specific pass
182
+ * This allows dynamic modification of bind groups at runtime
183
+ */
184
+ updateBindGroupSetResources(passName: string, setName: string, resources: BandingResource[]): void;
113
185
  /**
114
186
  * Add a render pass to the multi-pass pipeline
115
187
  */
@@ -138,8 +210,9 @@ declare class WGSLRenderer {
138
210
  * Create a sampler
139
211
  */
140
212
  createSampler(options?: GPUSamplerDescriptor): GPUSampler;
141
- loadImageTexture(url: string): Promise<{
213
+ loadImageTexture(image: Blob | string, format?: GPUTextureFormat): Promise<{
142
214
  texture: GPUTexture;
215
+ bitMap: ImageBitmap;
143
216
  width: number;
144
217
  height: number;
145
218
  }>;
@@ -151,4 +224,4 @@ declare class WGSLRenderer {
151
224
  }
152
225
  declare function createWGSLRenderer(cvs: HTMLCanvasElement, options?: WGSLRendererOptions): Promise<WGSLRenderer>;
153
226
  //#endregion
154
- export { MultiPassDescriptor, createWGSLRenderer };
227
+ export { MultiPassDescriptor, type WGSLRenderer, createWGSLRenderer };
package/dist/esm/index.js CHANGED
@@ -9,9 +9,14 @@ var RenderPass = class {
9
9
  view;
10
10
  format;
11
11
  passResources = [];
12
+ bindGroups = {};
13
+ activeBindGroupSet = "default";
12
14
  device;
15
+ descriptor;
16
+ enabled = true;
13
17
  constructor(descriptor, device, format, layout) {
14
18
  this.device = device;
19
+ this.descriptor = descriptor;
15
20
  this.name = descriptor.name;
16
21
  this.clearColor = descriptor.clearColor || {
17
22
  r: 0,
@@ -23,7 +28,10 @@ var RenderPass = class {
23
28
  this.view = descriptor.view;
24
29
  this.format = descriptor.format;
25
30
  const actualFormat = descriptor.format || format;
26
- const module = this.device.createShaderModule({ code: descriptor.shaderCode });
31
+ const module = this.device.createShaderModule({
32
+ code: descriptor.shaderCode,
33
+ label: `Shader for ${descriptor.name}`
34
+ });
27
35
  this.vertexBuffer = this.device.createBuffer({
28
36
  size: 36,
29
37
  usage: GPUBufferUsage.VERTEX,
@@ -78,6 +86,59 @@ var RenderPass = class {
78
86
  layout: bindGroupLayout,
79
87
  entries: newEntries
80
88
  });
89
+ this.bindGroups.default = this.bindGroup;
90
+ }
91
+ /**
92
+ * Update a specific bind group set with new entries
93
+ */
94
+ updateBindGroupSet(setName, newEntries) {
95
+ if (!this.bindGroups[setName]) {
96
+ const bindGroupLayout = this.pipeline.getBindGroupLayout(0);
97
+ this.bindGroups[setName] = this.device.createBindGroup({
98
+ layout: bindGroupLayout,
99
+ entries: newEntries
100
+ });
101
+ }
102
+ }
103
+ /**
104
+ * Switch to a different bind group set
105
+ */
106
+ switchBindGroupSet(setName) {
107
+ if (this.bindGroups[setName]) {
108
+ this.activeBindGroupSet = setName;
109
+ this.bindGroup = this.bindGroups[setName];
110
+ } else throw new Error(`Bind group set '${setName}' not found. Available sets: ${Object.keys(this.bindGroups).join(", ")}`);
111
+ }
112
+ /**
113
+ * Get the current active bind group
114
+ */
115
+ getActiveBindGroup() {
116
+ return this.bindGroups[this.activeBindGroupSet] || this.bindGroup;
117
+ }
118
+ /**
119
+ * Get all available bind group set names
120
+ */
121
+ getBindGroupSets() {
122
+ return Object.keys(this.bindGroups);
123
+ }
124
+ /**
125
+ * Update or add a bind group set with new resources
126
+ * This allows dynamic modification of bind groups at runtime
127
+ */
128
+ updateBindGroupSetResources(setName, resources) {
129
+ const entries = [];
130
+ resources.forEach((resource, index) => {
131
+ if (resource) entries.push({
132
+ binding: index,
133
+ resource
134
+ });
135
+ });
136
+ const bindGroupLayout = this.pipeline.getBindGroupLayout(0);
137
+ this.bindGroups[setName] = this.device.createBindGroup({
138
+ layout: bindGroupLayout,
139
+ entries
140
+ });
141
+ if (this.activeBindGroupSet === setName) this.bindGroup = this.bindGroups[setName];
81
142
  }
82
143
  getBlendState() {
83
144
  switch (this.blendMode) {
@@ -269,6 +330,74 @@ var WGSLRenderer = class {
269
330
  return this.passes.find((pass) => pass.name === passName);
270
331
  }
271
332
  /**
333
+ * Disable a render pass (it will be skipped during rendering)
334
+ */
335
+ disablePass(passName) {
336
+ const pass = this.getPassByName(passName);
337
+ if (!pass) throw new Error(`Cannot find pass named '${passName}'. Available passes: [${this.passes.map((p) => p.name).join(", ")}]`);
338
+ pass.enabled = false;
339
+ }
340
+ /**
341
+ * Enable a render pass
342
+ */
343
+ enablePass(passName) {
344
+ const pass = this.getPassByName(passName);
345
+ if (!pass) throw new Error(`Cannot find pass named '${passName}'. Available passes: [${this.passes.map((p) => p.name).join(", ")}]`);
346
+ pass.enabled = true;
347
+ }
348
+ /**
349
+ * Check if a pass is enabled
350
+ */
351
+ isPassEnabled(passName) {
352
+ const pass = this.getPassByName(passName);
353
+ if (!pass) throw new Error(`Cannot find pass named '${passName}'. Available passes: [${this.passes.map((p) => p.name).join(", ")}]`);
354
+ return pass.enabled;
355
+ }
356
+ /**
357
+ * Remove a render pass permanently
358
+ */
359
+ removePass(passName) {
360
+ const index = this.passes.findIndex((pass) => pass.name === passName);
361
+ if (index !== -1) {
362
+ this.passes.splice(index, 1);
363
+ return true;
364
+ }
365
+ return false;
366
+ }
367
+ /**
368
+ * Get all passes (enabled and disabled)
369
+ */
370
+ getAllPasses() {
371
+ return [...this.passes];
372
+ }
373
+ /**
374
+ * Get only enabled passes
375
+ */
376
+ getEnabledPasses() {
377
+ return this.passes.filter((pass) => pass.enabled);
378
+ }
379
+ /**
380
+ * Switch bind group set for a specific pass
381
+ */
382
+ switchBindGroupSet(passName, setName) {
383
+ const pass = this.getPassByName(passName);
384
+ if (!pass) throw new Error(`Cannot find pass named '${passName}'. Available passes: [${this.passes.map((p) => p.name).join(", ")}]`);
385
+ pass.switchBindGroupSet(setName);
386
+ }
387
+ /**
388
+ * Update bind group set resources for a specific pass
389
+ * This allows dynamic modification of bind groups at runtime
390
+ */
391
+ updateBindGroupSetResources(passName, setName, resources) {
392
+ const pass = this.getPassByName(passName);
393
+ if (!pass) throw new Error(`Cannot find pass named '${passName}'. Available passes: [${this.passes.map((p) => p.name).join(", ")}]`);
394
+ const resolvedResources = resources.map((resource) => {
395
+ if (resource && isPassTextureRef(resource)) return this.resolveResource(resource);
396
+ return resource;
397
+ });
398
+ pass.updateBindGroupSetResources(setName, resolvedResources);
399
+ }
400
+ /**
272
401
  * Add a render pass to the multi-pass pipeline
273
402
  */
274
403
  addPass(descriptor) {
@@ -279,6 +408,11 @@ var WGSLRenderer = class {
279
408
  resource
280
409
  });
281
410
  });
411
+ let bindGroupSetsCopy = void 0;
412
+ if (descriptor.bindGroupSets) {
413
+ bindGroupSetsCopy = {};
414
+ for (const [setName, resources] of Object.entries(descriptor.bindGroupSets)) bindGroupSetsCopy[setName] = [...resources];
415
+ }
282
416
  const internalDescriptor = {
283
417
  name: descriptor.name,
284
418
  shaderCode: descriptor.shaderCode,
@@ -286,6 +420,7 @@ var WGSLRenderer = class {
286
420
  clearColor: descriptor.clearColor,
287
421
  blendMode: descriptor.blendMode,
288
422
  bindGroupEntries: finalBindGroupEntries,
423
+ bindGroupSets: bindGroupSetsCopy,
289
424
  view: descriptor.view,
290
425
  format: descriptor.format
291
426
  };
@@ -316,6 +451,19 @@ var WGSLRenderer = class {
316
451
  });
317
452
  });
318
453
  pass.updateBindGroup(finalBindGroupEntries);
454
+ if (pass.descriptor && pass.descriptor.bindGroupSets) for (const [setName, resources] of Object.entries(pass.descriptor.bindGroupSets)) {
455
+ const entries = [];
456
+ resources.forEach((resource, index) => {
457
+ if (resource) {
458
+ const resolved = this.resolveResource(resource);
459
+ entries.push({
460
+ binding: index,
461
+ resource: resolved
462
+ });
463
+ }
464
+ });
465
+ pass.updateBindGroupSet(setName, entries);
466
+ }
319
467
  });
320
468
  }
321
469
  /**
@@ -348,13 +496,22 @@ var WGSLRenderer = class {
348
496
  addressModeV: "clamp-to-edge"
349
497
  }, options));
350
498
  }
351
- async loadImageTexture(url) {
352
- const resp = fetch(url);
353
- resp.catch((err) => {
354
- console.error("Failed to load texture:", err);
355
- });
356
- const res = await resp;
357
- const future = createImageBitmap(await res.blob());
499
+ async loadImageTexture(image, format) {
500
+ if (typeof image === "string") if (image.startsWith("data:")) {
501
+ const base64Data = image.split(",")[1];
502
+ const binaryString = atob(base64Data);
503
+ const len = binaryString.length;
504
+ const bytes = new Uint8Array(len);
505
+ for (let i = 0; i < len; i++) bytes[i] = binaryString.charCodeAt(i);
506
+ image = new Blob([bytes], { type: "application/octet-stream" });
507
+ } else {
508
+ const resp = fetch(image);
509
+ resp.catch((err) => {
510
+ console.error("Failed to load texture:", err);
511
+ });
512
+ image = await (await resp).blob();
513
+ }
514
+ const future = createImageBitmap(image);
358
515
  future.catch((err) => {
359
516
  console.error("Failed to load texture:", err);
360
517
  });
@@ -365,24 +522,27 @@ var WGSLRenderer = class {
365
522
  imgBitmap.height,
366
523
  1
367
524
  ],
368
- format: "rgba8unorm",
525
+ format: format || "rgba8unorm",
369
526
  usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT
370
527
  });
371
528
  this.device.queue.copyExternalImageToTexture({ source: imgBitmap }, { texture }, [imgBitmap.width, imgBitmap.height]);
372
529
  return {
373
530
  texture,
531
+ bitMap: imgBitmap,
374
532
  width: imgBitmap.width,
375
533
  height: imgBitmap.height
376
534
  };
377
535
  }
378
536
  renderFrame() {
379
537
  if (this.passes.length === 0) return;
538
+ const enabledPasses = this.getEnabledPasses();
539
+ if (enabledPasses.length === 0) return;
380
540
  this.updateBindGroups();
381
541
  const commandEncoder = this.device.createCommandEncoder();
382
- for (let i = 0; i < this.passes.length; i++) {
383
- const pass = this.passes[i];
542
+ for (let i = 0; i < enabledPasses.length; i++) {
543
+ const pass = enabledPasses[i];
384
544
  let loadOp = "load";
385
- const isLast = i === this.passes.length - 1;
545
+ const isLast = i === enabledPasses.length - 1;
386
546
  if (isLast) loadOp = "clear";
387
547
  let renderTarget;
388
548
  if (pass.view) renderTarget = pass.view;
@@ -390,7 +550,7 @@ var WGSLRenderer = class {
390
550
  else {
391
551
  const textureName = `pass_${i}_output`;
392
552
  let texture = this.textureManager.getTexture(textureName);
393
- if (!texture) texture = this.textureManager.createTexture(textureName, "rgba16float");
553
+ if (!texture) texture = this.textureManager.createTexture(textureName, pass.format || this.format);
394
554
  renderTarget = texture.createView();
395
555
  }
396
556
  const renderPass = commandEncoder.beginRenderPass({ colorAttachments: [{
@@ -400,7 +560,8 @@ var WGSLRenderer = class {
400
560
  clearValue: pass.clearColor
401
561
  }] });
402
562
  renderPass.setPipeline(pass.pipeline);
403
- if (pass.bindGroup) renderPass.setBindGroup(0, pass.bindGroup);
563
+ const activeBindGroup = pass.getActiveBindGroup();
564
+ if (activeBindGroup) renderPass.setBindGroup(0, activeBindGroup);
404
565
  renderPass.setVertexBuffer(0, pass.vertexBuffer);
405
566
  renderPass.draw(3, 1, 0, 0);
406
567
  renderPass.end();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wgsl-renderer",
3
- "version": "0.0.5",
3
+ "version": "0.1.1",
4
4
  "description": "一个基于WebGPU和WGSL的多通道渲染器",
5
5
  "type": "module",
6
6
  "main": "./dist/cjs/index.js",
@@ -33,12 +33,11 @@
33
33
  "files": [
34
34
  "dist"
35
35
  ],
36
- "dependencies": {
37
- "@webgpu/types": "^0.1.66"
38
- },
36
+ "dependencies": {},
39
37
  "devDependencies": {
40
38
  "@taiyuuki/eslint-config": "^1.4.14",
41
39
  "@types/node": "^20.14.2",
40
+ "@webgpu/types": "^0.1.66",
42
41
  "eslint": "^9.5.0",
43
42
  "rimraf": "^6.1.2",
44
43
  "rolldown": "1.0.0-beta.52",