wgsl-renderer 0.3.3 → 0.4.0

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/dist/cjs/index.js CHANGED
@@ -314,6 +314,8 @@ var TextureManager = class {
314
314
  device;
315
315
  width;
316
316
  height;
317
+ renderTarget = null;
318
+ outputTexture = null;
317
319
  constructor(device, width, height) {
318
320
  this.device = device;
319
321
  this.width = width;
@@ -348,9 +350,64 @@ var TextureManager = class {
348
350
  setTexture(name, texture) {
349
351
  this.textures.set(name, texture);
350
352
  }
353
+ /**
354
+ * 获取或创建渲染目标纹理
355
+ * 所有渲染到这里的内容会再复制到 canvas 和输出纹理
356
+ * 必须支持 COPY_SRC 以便复制到 canvas 和输出纹理
357
+ */
358
+ getOrCreateRenderTarget(width, height, format) {
359
+ if (!this.renderTarget || this.renderTarget.width !== width || this.renderTarget.height !== height) {
360
+ this.renderTarget?.destroy();
361
+ this.renderTarget = this.device.createTexture({
362
+ size: [
363
+ width,
364
+ height,
365
+ 1
366
+ ],
367
+ format,
368
+ usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC | GPUTextureUsage.TEXTURE_BINDING
369
+ });
370
+ }
371
+ return this.renderTarget;
372
+ }
373
+ /**
374
+ * 获取当前渲染目标纹理
375
+ */
376
+ getRenderTarget() {
377
+ return this.renderTarget;
378
+ }
379
+ /**
380
+ * 获取或创建输出纹理
381
+ * 用于视频导出,始终包含最新的渲染结果
382
+ */
383
+ getOrCreateOutputTexture(width, height, format) {
384
+ if (!this.outputTexture || this.outputTexture.width !== width || this.outputTexture.height !== height) {
385
+ this.outputTexture?.destroy();
386
+ this.outputTexture = this.device.createTexture({
387
+ size: [
388
+ width,
389
+ height,
390
+ 1
391
+ ],
392
+ format,
393
+ usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC | GPUTextureUsage.TEXTURE_BINDING
394
+ });
395
+ }
396
+ return this.outputTexture;
397
+ }
398
+ /**
399
+ * 获取当前输出纹理
400
+ */
401
+ getOutputTexture() {
402
+ return this.outputTexture;
403
+ }
351
404
  destroy() {
352
405
  this.textures.forEach((texture) => texture.destroy());
353
406
  this.textures.clear();
407
+ this.renderTarget?.destroy();
408
+ this.renderTarget = null;
409
+ this.outputTexture?.destroy();
410
+ this.outputTexture = null;
354
411
  }
355
412
  getPixelSize() {
356
413
  return {
@@ -398,6 +455,11 @@ function createSamplingView(texture, ref) {
398
455
 
399
456
  //#endregion
400
457
  //#region src/index.ts
458
+ var RenderMode = /* @__PURE__ */ function(RenderMode$1) {
459
+ RenderMode$1["NORMAL"] = "normal";
460
+ RenderMode$1["EXPORT"] = "export";
461
+ return RenderMode$1;
462
+ }(RenderMode || {});
401
463
  var WGSLRenderer = class {
402
464
  ctx;
403
465
  device;
@@ -406,6 +468,9 @@ var WGSLRenderer = class {
406
468
  textureManager;
407
469
  animationFrameId = null;
408
470
  isResizing = false;
471
+ renderMode = RenderMode.NORMAL;
472
+ readBuffer = null;
473
+ hasClearedCanvasThisFrame = false;
409
474
  constructor(canvas, options) {
410
475
  this.canvas = canvas;
411
476
  this.options = options;
@@ -442,6 +507,19 @@ var WGSLRenderer = class {
442
507
  this.textureManager.resize(width, height);
443
508
  this.isResizing = false;
444
509
  }
510
+ /**
511
+ * 设置渲染模式
512
+ * @param mode NORMAL模式渲染到canvas和outputTexture,EXPORT模式只渲染到outputTexture
513
+ */
514
+ setRenderMode(mode) {
515
+ this.renderMode = mode;
516
+ }
517
+ /**
518
+ * 获取当前渲染模式
519
+ */
520
+ getRenderMode() {
521
+ return this.renderMode;
522
+ }
445
523
  getContext() {
446
524
  return this.ctx;
447
525
  }
@@ -456,10 +534,6 @@ var WGSLRenderer = class {
456
534
  * @param options Optional texture creation options for when the texture needs to be created
457
535
  */
458
536
  getPassTexture(passName, options) {
459
- const pass = this.passes.find((pass$1) => pass$1.name === passName);
460
- if (!pass) throw new Error(`Cannot find pass named '${passName}'. Available passes: [${this.passes.map((p) => p.name).join(", ")}]`);
461
- const f = options?.format ?? "rgba8unorm";
462
- if (pass.format && f !== pass.format) throw new Error(`Format must be set to ${pass.format}, pass name: '${passName}'`);
463
537
  return PassTextureRef.create(passName, options);
464
538
  }
465
539
  /**
@@ -714,30 +788,45 @@ var WGSLRenderer = class {
714
788
  if (this.passes.length === 0) return;
715
789
  const enabledPasses = this.getEnabledPasses();
716
790
  if (enabledPasses.length === 0) return;
791
+ this.hasClearedCanvasThisFrame = false;
717
792
  this.updateBindGroups();
718
793
  const commandEncoder = this.device.createCommandEncoder();
794
+ const canvasWidth = this.canvas.width || this.canvas.clientWidth;
795
+ const canvasHeight = this.canvas.height || this.canvas.clientHeight;
719
796
  for (let i = 0; i < enabledPasses.length; i++) {
720
797
  const pass = enabledPasses[i];
721
798
  let loadOp = "load";
722
- if (i === 0) loadOp = "clear";
723
- let renderTarget;
724
- let resolveTarget;
725
- const canvasTexture = this.ctx.getCurrentTexture();
799
+ const isFirst = i === 0;
726
800
  const isLastPass = i === enabledPasses.length - 1;
727
- if (pass.renderToCanvas || isLastPass && !pass.view) renderTarget = canvasTexture.createView();
728
- else if (pass.view) renderTarget = pass.view;
729
- else {
801
+ if (isFirst) loadOp = "clear";
802
+ let renderTargetView;
803
+ let isRenderingToCanvas = false;
804
+ if (pass.view) renderTargetView = pass.view;
805
+ else if (this.renderMode === RenderMode.EXPORT && (pass.renderToCanvas || isLastPass)) {
806
+ renderTargetView = this.textureManager.getOrCreateOutputTexture(canvasWidth, canvasHeight, this.format).createView();
807
+ if (this.hasClearedCanvasThisFrame) {
808
+ if (loadOp === "clear") loadOp = "load";
809
+ } else {
810
+ if (loadOp === "load") loadOp = "clear";
811
+ this.hasClearedCanvasThisFrame = true;
812
+ }
813
+ } else if (pass.renderToCanvas || isLastPass) {
814
+ renderTargetView = this.ctx.getCurrentTexture().createView();
815
+ isRenderingToCanvas = true;
816
+ } else {
730
817
  const textureName = `pass_${i}_output`;
731
818
  let texture = this.textureManager.getTexture(textureName);
732
819
  if (!texture) texture = this.textureManager.createTexture(textureName, pass.format || this.format);
733
- renderTarget = texture.createView({
734
- baseMipLevel: 0,
735
- mipLevelCount: 1
736
- });
820
+ renderTargetView = texture.createView();
821
+ }
822
+ if (isRenderingToCanvas) if (this.hasClearedCanvasThisFrame) {
823
+ if (loadOp === "clear") loadOp = "load";
824
+ } else {
825
+ if (loadOp === "load") loadOp = "clear";
826
+ this.hasClearedCanvasThisFrame = true;
737
827
  }
738
828
  const renderPass = commandEncoder.beginRenderPass({ colorAttachments: [{
739
- view: renderTarget,
740
- resolveTarget,
829
+ view: renderTargetView,
741
830
  loadOp,
742
831
  storeOp: "store",
743
832
  clearValue: pass.clearColor
@@ -768,6 +857,44 @@ var WGSLRenderer = class {
768
857
  this.stopLoop();
769
858
  this.passes = [];
770
859
  this.textureManager.destroy();
860
+ this.readBuffer?.destroy();
861
+ this.readBuffer = null;
862
+ }
863
+ /**
864
+ * 快速捕获当前帧的像素数据
865
+ * 从outputTexture读取,不经过canvas,用于视频导出
866
+ * @returns Uint8Array格式的RGBA像素数据
867
+ */
868
+ async captureFrameFast() {
869
+ const outputTexture = this.textureManager.getOutputTexture();
870
+ if (!outputTexture) throw new Error("Output texture not available. Please render a frame first.");
871
+ const width = outputTexture.width;
872
+ const height = outputTexture.height;
873
+ const bytesPerRow = Math.ceil(width * 4 / 256) * 256;
874
+ const bufferSize = bytesPerRow * height;
875
+ if (!this.readBuffer || this.readBuffer.size !== bufferSize) {
876
+ this.readBuffer?.destroy();
877
+ this.readBuffer = this.device.createBuffer({
878
+ size: bufferSize,
879
+ usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ
880
+ });
881
+ }
882
+ const commandEncoder = this.device.createCommandEncoder();
883
+ commandEncoder.copyTextureToBuffer({ texture: outputTexture }, {
884
+ buffer: this.readBuffer,
885
+ bytesPerRow
886
+ }, [width, height]);
887
+ this.device.queue.submit([commandEncoder.finish()]);
888
+ await this.readBuffer.mapAsync(GPUMapMode.READ);
889
+ const mappedBuffer = new Uint8Array(this.readBuffer.getMappedRange().slice(0));
890
+ this.readBuffer.unmap();
891
+ const pixelData = new Uint8Array(width * height * 4);
892
+ for (let row = 0; row < height; row++) {
893
+ const srcOffset = row * bytesPerRow;
894
+ const dstOffset = row * width * 4;
895
+ pixelData.set(mappedBuffer.subarray(srcOffset, srcOffset + width * 4), dstOffset);
896
+ }
897
+ return pixelData;
771
898
  }
772
899
  };
773
900
  async function createWGSLRenderer(cvs, options) {
@@ -778,6 +905,7 @@ async function createWGSLRenderer(cvs, options) {
778
905
 
779
906
  //#endregion
780
907
  exports.PassTextureRef = PassTextureRef;
908
+ exports.RenderMode = RenderMode;
781
909
  exports.WGSLRenderer = WGSLRenderer;
782
910
  exports.createSamplingView = createSamplingView;
783
911
  exports.createWGSLRenderer = createWGSLRenderer;
@@ -140,6 +140,11 @@ declare class RenderPass {
140
140
  }
141
141
  //#endregion
142
142
  //#region src/index.d.ts
143
+ declare enum RenderMode {
144
+ NORMAL = "normal",
145
+ // 正常模式:渲染到canvas和outputTexture
146
+ EXPORT = "export",
147
+ }
143
148
  interface WGSLRendererOptions {
144
149
  config?: Partial<GPUCanvasConfiguration>;
145
150
  }
@@ -153,9 +158,21 @@ declare class WGSLRenderer {
153
158
  private textureManager;
154
159
  private animationFrameId;
155
160
  private isResizing;
161
+ private renderMode;
162
+ private readBuffer;
163
+ private hasClearedCanvasThisFrame;
156
164
  constructor(canvas: HTMLCanvasElement, options?: WGSLRendererOptions | undefined);
157
165
  init(): Promise<void>;
158
166
  resize(width: number, height: number): Promise<void>;
167
+ /**
168
+ * 设置渲染模式
169
+ * @param mode NORMAL模式渲染到canvas和outputTexture,EXPORT模式只渲染到outputTexture
170
+ */
171
+ setRenderMode(mode: RenderMode): void;
172
+ /**
173
+ * 获取当前渲染模式
174
+ */
175
+ getRenderMode(): RenderMode;
159
176
  getContext(): GPUCanvasContext;
160
177
  getDevice(): GPUDevice;
161
178
  /**
@@ -264,7 +281,13 @@ declare class WGSLRenderer {
264
281
  }): void;
265
282
  stopLoop(): void;
266
283
  reset(): void;
284
+ /**
285
+ * 快速捕获当前帧的像素数据
286
+ * 从outputTexture读取,不经过canvas,用于视频导出
287
+ * @returns Uint8Array格式的RGBA像素数据
288
+ */
289
+ captureFrameFast(): Promise<Uint8Array>;
267
290
  }
268
291
  declare function createWGSLRenderer(cvs: HTMLCanvasElement, options?: WGSLRendererOptions): Promise<WGSLRenderer>;
269
292
  //#endregion
270
- export { BindingResource, PassTextureRef, RenderPassOptions, WGSLRenderer, createSamplingView, createWGSLRenderer };
293
+ export { BindingResource, PassTextureRef, RenderMode, RenderPassOptions, WGSLRenderer, createSamplingView, createWGSLRenderer };
package/dist/esm/index.js CHANGED
@@ -313,6 +313,8 @@ var TextureManager = class {
313
313
  device;
314
314
  width;
315
315
  height;
316
+ renderTarget = null;
317
+ outputTexture = null;
316
318
  constructor(device, width, height) {
317
319
  this.device = device;
318
320
  this.width = width;
@@ -347,9 +349,64 @@ var TextureManager = class {
347
349
  setTexture(name, texture) {
348
350
  this.textures.set(name, texture);
349
351
  }
352
+ /**
353
+ * 获取或创建渲染目标纹理
354
+ * 所有渲染到这里的内容会再复制到 canvas 和输出纹理
355
+ * 必须支持 COPY_SRC 以便复制到 canvas 和输出纹理
356
+ */
357
+ getOrCreateRenderTarget(width, height, format) {
358
+ if (!this.renderTarget || this.renderTarget.width !== width || this.renderTarget.height !== height) {
359
+ this.renderTarget?.destroy();
360
+ this.renderTarget = this.device.createTexture({
361
+ size: [
362
+ width,
363
+ height,
364
+ 1
365
+ ],
366
+ format,
367
+ usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC | GPUTextureUsage.TEXTURE_BINDING
368
+ });
369
+ }
370
+ return this.renderTarget;
371
+ }
372
+ /**
373
+ * 获取当前渲染目标纹理
374
+ */
375
+ getRenderTarget() {
376
+ return this.renderTarget;
377
+ }
378
+ /**
379
+ * 获取或创建输出纹理
380
+ * 用于视频导出,始终包含最新的渲染结果
381
+ */
382
+ getOrCreateOutputTexture(width, height, format) {
383
+ if (!this.outputTexture || this.outputTexture.width !== width || this.outputTexture.height !== height) {
384
+ this.outputTexture?.destroy();
385
+ this.outputTexture = this.device.createTexture({
386
+ size: [
387
+ width,
388
+ height,
389
+ 1
390
+ ],
391
+ format,
392
+ usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC | GPUTextureUsage.TEXTURE_BINDING
393
+ });
394
+ }
395
+ return this.outputTexture;
396
+ }
397
+ /**
398
+ * 获取当前输出纹理
399
+ */
400
+ getOutputTexture() {
401
+ return this.outputTexture;
402
+ }
350
403
  destroy() {
351
404
  this.textures.forEach((texture) => texture.destroy());
352
405
  this.textures.clear();
406
+ this.renderTarget?.destroy();
407
+ this.renderTarget = null;
408
+ this.outputTexture?.destroy();
409
+ this.outputTexture = null;
353
410
  }
354
411
  getPixelSize() {
355
412
  return {
@@ -397,6 +454,11 @@ function createSamplingView(texture, ref) {
397
454
 
398
455
  //#endregion
399
456
  //#region src/index.ts
457
+ var RenderMode = /* @__PURE__ */ function(RenderMode$1) {
458
+ RenderMode$1["NORMAL"] = "normal";
459
+ RenderMode$1["EXPORT"] = "export";
460
+ return RenderMode$1;
461
+ }(RenderMode || {});
400
462
  var WGSLRenderer = class {
401
463
  ctx;
402
464
  device;
@@ -405,6 +467,9 @@ var WGSLRenderer = class {
405
467
  textureManager;
406
468
  animationFrameId = null;
407
469
  isResizing = false;
470
+ renderMode = RenderMode.NORMAL;
471
+ readBuffer = null;
472
+ hasClearedCanvasThisFrame = false;
408
473
  constructor(canvas, options) {
409
474
  this.canvas = canvas;
410
475
  this.options = options;
@@ -441,6 +506,19 @@ var WGSLRenderer = class {
441
506
  this.textureManager.resize(width, height);
442
507
  this.isResizing = false;
443
508
  }
509
+ /**
510
+ * 设置渲染模式
511
+ * @param mode NORMAL模式渲染到canvas和outputTexture,EXPORT模式只渲染到outputTexture
512
+ */
513
+ setRenderMode(mode) {
514
+ this.renderMode = mode;
515
+ }
516
+ /**
517
+ * 获取当前渲染模式
518
+ */
519
+ getRenderMode() {
520
+ return this.renderMode;
521
+ }
444
522
  getContext() {
445
523
  return this.ctx;
446
524
  }
@@ -455,10 +533,6 @@ var WGSLRenderer = class {
455
533
  * @param options Optional texture creation options for when the texture needs to be created
456
534
  */
457
535
  getPassTexture(passName, options) {
458
- const pass = this.passes.find((pass$1) => pass$1.name === passName);
459
- if (!pass) throw new Error(`Cannot find pass named '${passName}'. Available passes: [${this.passes.map((p) => p.name).join(", ")}]`);
460
- const f = options?.format ?? "rgba8unorm";
461
- if (pass.format && f !== pass.format) throw new Error(`Format must be set to ${pass.format}, pass name: '${passName}'`);
462
536
  return PassTextureRef.create(passName, options);
463
537
  }
464
538
  /**
@@ -713,30 +787,45 @@ var WGSLRenderer = class {
713
787
  if (this.passes.length === 0) return;
714
788
  const enabledPasses = this.getEnabledPasses();
715
789
  if (enabledPasses.length === 0) return;
790
+ this.hasClearedCanvasThisFrame = false;
716
791
  this.updateBindGroups();
717
792
  const commandEncoder = this.device.createCommandEncoder();
793
+ const canvasWidth = this.canvas.width || this.canvas.clientWidth;
794
+ const canvasHeight = this.canvas.height || this.canvas.clientHeight;
718
795
  for (let i = 0; i < enabledPasses.length; i++) {
719
796
  const pass = enabledPasses[i];
720
797
  let loadOp = "load";
721
- if (i === 0) loadOp = "clear";
722
- let renderTarget;
723
- let resolveTarget;
724
- const canvasTexture = this.ctx.getCurrentTexture();
798
+ const isFirst = i === 0;
725
799
  const isLastPass = i === enabledPasses.length - 1;
726
- if (pass.renderToCanvas || isLastPass && !pass.view) renderTarget = canvasTexture.createView();
727
- else if (pass.view) renderTarget = pass.view;
728
- else {
800
+ if (isFirst) loadOp = "clear";
801
+ let renderTargetView;
802
+ let isRenderingToCanvas = false;
803
+ if (pass.view) renderTargetView = pass.view;
804
+ else if (this.renderMode === RenderMode.EXPORT && (pass.renderToCanvas || isLastPass)) {
805
+ renderTargetView = this.textureManager.getOrCreateOutputTexture(canvasWidth, canvasHeight, this.format).createView();
806
+ if (this.hasClearedCanvasThisFrame) {
807
+ if (loadOp === "clear") loadOp = "load";
808
+ } else {
809
+ if (loadOp === "load") loadOp = "clear";
810
+ this.hasClearedCanvasThisFrame = true;
811
+ }
812
+ } else if (pass.renderToCanvas || isLastPass) {
813
+ renderTargetView = this.ctx.getCurrentTexture().createView();
814
+ isRenderingToCanvas = true;
815
+ } else {
729
816
  const textureName = `pass_${i}_output`;
730
817
  let texture = this.textureManager.getTexture(textureName);
731
818
  if (!texture) texture = this.textureManager.createTexture(textureName, pass.format || this.format);
732
- renderTarget = texture.createView({
733
- baseMipLevel: 0,
734
- mipLevelCount: 1
735
- });
819
+ renderTargetView = texture.createView();
820
+ }
821
+ if (isRenderingToCanvas) if (this.hasClearedCanvasThisFrame) {
822
+ if (loadOp === "clear") loadOp = "load";
823
+ } else {
824
+ if (loadOp === "load") loadOp = "clear";
825
+ this.hasClearedCanvasThisFrame = true;
736
826
  }
737
827
  const renderPass = commandEncoder.beginRenderPass({ colorAttachments: [{
738
- view: renderTarget,
739
- resolveTarget,
828
+ view: renderTargetView,
740
829
  loadOp,
741
830
  storeOp: "store",
742
831
  clearValue: pass.clearColor
@@ -767,6 +856,44 @@ var WGSLRenderer = class {
767
856
  this.stopLoop();
768
857
  this.passes = [];
769
858
  this.textureManager.destroy();
859
+ this.readBuffer?.destroy();
860
+ this.readBuffer = null;
861
+ }
862
+ /**
863
+ * 快速捕获当前帧的像素数据
864
+ * 从outputTexture读取,不经过canvas,用于视频导出
865
+ * @returns Uint8Array格式的RGBA像素数据
866
+ */
867
+ async captureFrameFast() {
868
+ const outputTexture = this.textureManager.getOutputTexture();
869
+ if (!outputTexture) throw new Error("Output texture not available. Please render a frame first.");
870
+ const width = outputTexture.width;
871
+ const height = outputTexture.height;
872
+ const bytesPerRow = Math.ceil(width * 4 / 256) * 256;
873
+ const bufferSize = bytesPerRow * height;
874
+ if (!this.readBuffer || this.readBuffer.size !== bufferSize) {
875
+ this.readBuffer?.destroy();
876
+ this.readBuffer = this.device.createBuffer({
877
+ size: bufferSize,
878
+ usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ
879
+ });
880
+ }
881
+ const commandEncoder = this.device.createCommandEncoder();
882
+ commandEncoder.copyTextureToBuffer({ texture: outputTexture }, {
883
+ buffer: this.readBuffer,
884
+ bytesPerRow
885
+ }, [width, height]);
886
+ this.device.queue.submit([commandEncoder.finish()]);
887
+ await this.readBuffer.mapAsync(GPUMapMode.READ);
888
+ const mappedBuffer = new Uint8Array(this.readBuffer.getMappedRange().slice(0));
889
+ this.readBuffer.unmap();
890
+ const pixelData = new Uint8Array(width * height * 4);
891
+ for (let row = 0; row < height; row++) {
892
+ const srcOffset = row * bytesPerRow;
893
+ const dstOffset = row * width * 4;
894
+ pixelData.set(mappedBuffer.subarray(srcOffset, srcOffset + width * 4), dstOffset);
895
+ }
896
+ return pixelData;
770
897
  }
771
898
  };
772
899
  async function createWGSLRenderer(cvs, options) {
@@ -776,4 +903,4 @@ async function createWGSLRenderer(cvs, options) {
776
903
  }
777
904
 
778
905
  //#endregion
779
- export { PassTextureRef, WGSLRenderer, createSamplingView, createWGSLRenderer };
906
+ export { PassTextureRef, RenderMode, WGSLRenderer, createSamplingView, createWGSLRenderer };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wgsl-renderer",
3
- "version": "0.3.3",
3
+ "version": "0.4.0",
4
4
  "description": "A multi-pass renderer based on WebGPU and WGSL.",
5
5
  "type": "module",
6
6
  "main": "./dist/cjs/index.js",