@driftengine/core 4.8.6 → 4.8.7

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.
Files changed (139) hide show
  1. package/CHANGELOG.json +71 -0
  2. package/README.md +1 -1
  3. package/dist/index.d.ts +2 -2
  4. package/dist/index.js +1 -1
  5. package/dist/render/anisotropicLobe.d.ts +10 -0
  6. package/dist/render/anisotropicLobe.js +19 -0
  7. package/dist/render/areaLights.d.ts +35 -0
  8. package/dist/render/areaLights.js +35 -0
  9. package/dist/render/backend/api.d.ts +33 -3
  10. package/dist/render/backend/webgl2/renderer.d.ts +50 -14
  11. package/dist/render/backend/webgl2/renderer.js +141 -30
  12. package/dist/render/backend/webgl2/sceneCaptureTarget.d.ts +29 -0
  13. package/dist/render/backend/webgl2/sceneCaptureTarget.js +96 -0
  14. package/dist/render/backend/webgpu/particlePass.js +2 -1
  15. package/dist/render/backend/webgpu/renderer.d.ts +35 -1
  16. package/dist/render/backend/webgpu/renderer.js +155 -16
  17. package/dist/render/backend/webgpu/sceneCaptureTarget.d.ts +24 -0
  18. package/dist/render/backend/webgpu/sceneCaptureTarget.js +59 -0
  19. package/dist/render/backend/webgpu/shaders/clusterBin.wgsl.d.ts +1 -1
  20. package/dist/render/backend/webgpu/surfaceTexturePass.d.ts +4 -1
  21. package/dist/render/backend/webgpu/surfaceTexturePass.js +34 -8
  22. package/dist/render/backend/webgpu/waterPass.d.ts +1 -1
  23. package/dist/render/clusteredLights.d.ts +23 -5
  24. package/dist/render/clusteredLights.js +35 -8
  25. package/dist/render/driftLight/bake.d.ts +2 -0
  26. package/dist/render/driftLight/bake.js +1 -1
  27. package/dist/render/driftLight/falloff.d.ts +4 -2
  28. package/dist/render/driftLight/falloff.js +8 -2
  29. package/dist/render/gi/bounceLights.d.ts +3 -2
  30. package/dist/render/gi/bounceLights.js +8 -3
  31. package/dist/render/instanceCull.js +5 -0
  32. package/dist/render/instances.d.ts +19 -5
  33. package/dist/render/instances.js +9 -3
  34. package/dist/render/lightBudget.d.ts +12 -1
  35. package/dist/render/lightBudget.js +23 -3
  36. package/dist/render/lightmap.d.ts +23 -9
  37. package/dist/render/lightmap.js +40 -18
  38. package/dist/render/particlePool.d.ts +21 -0
  39. package/dist/render/particlePool.js +7 -0
  40. package/dist/render/pointLightSelection.d.ts +13 -0
  41. package/dist/render/pointLightSelection.js +3 -1
  42. package/dist/render/renderQuality.d.ts +8 -7
  43. package/dist/render/rendererHarness.js +3 -0
  44. package/dist/render/rgb9e5.d.ts +22 -0
  45. package/dist/render/rgb9e5.js +47 -0
  46. package/dist/render/sceneCapture.d.ts +29 -0
  47. package/dist/render/sceneCapture.js +36 -0
  48. package/dist/render/screenSpaceReflection.d.ts +27 -1
  49. package/dist/render/screenSpaceReflection.js +9 -0
  50. package/dist/render/shaders/bolt.d.ts +1 -1
  51. package/dist/render/shaders/bolt.js +7 -1
  52. package/dist/render/shaders/depth.js +11 -0
  53. package/dist/render/shaders/flat/anisotropic.d.ts +1 -1
  54. package/dist/render/shaders/flat/anisotropic.js +37 -3
  55. package/dist/render/shaders/flat/areaLight.d.ts +1 -1
  56. package/dist/render/shaders/flat/areaLight.js +46 -0
  57. package/dist/render/shaders/flat/glassTint.d.ts +3 -2
  58. package/dist/render/shaders/flat/glassTint.js +3 -1
  59. package/dist/render/shaders/flat/index.d.ts +5 -0
  60. package/dist/render/shaders/flat/index.js +16 -2
  61. package/dist/render/shaders/flat/lobes.d.ts +6 -0
  62. package/dist/render/shaders/flat/lobes.js +49 -43
  63. package/dist/render/shaders/flat/main.d.ts +1 -1
  64. package/dist/render/shaders/flat/main.js +57 -22
  65. package/dist/render/shaders/flat/models.d.ts +5 -2
  66. package/dist/render/shaders/flat/models.js +100 -16
  67. package/dist/render/shaders/flat/preamble.js +10 -3
  68. package/dist/render/shaders/generated/bolt.wgsl.d.ts +1 -1
  69. package/dist/render/shaders/generated/bolt.wgsl.js +1 -1
  70. package/dist/render/shaders/generated/depth.wgsl.d.ts +4 -4
  71. package/dist/render/shaders/generated/depth.wgsl.js +4 -4
  72. package/dist/render/shaders/generated/flat.wgsl.d.ts +16 -0
  73. package/dist/render/shaders/generated/flat.wgsl.js +54 -38
  74. package/dist/render/shaders/generated/ssrTrace.wgsl.d.ts +11 -6
  75. package/dist/render/shaders/generated/ssrTrace.wgsl.js +11 -6
  76. package/dist/render/shaders/generated/water.wgsl.d.ts +23 -16
  77. package/dist/render/shaders/generated/water.wgsl.js +23 -16
  78. package/dist/render/shaders/gi/probeBake.wgsl.js +9 -5
  79. package/dist/render/shaders/recon/motion.wgsl.d.ts +1 -1
  80. package/dist/render/shaders/recon/motion.wgsl.js +13 -2
  81. package/dist/render/shaders/ssrTrace.js +20 -1
  82. package/dist/render/shaders/water.d.ts +1 -1
  83. package/dist/render/shaders/water.js +12 -3
  84. package/dist/render/skinScatter.d.ts +3 -1
  85. package/dist/render/skinScatter.js +3 -1
  86. package/dist/render/ssrPass.js +1 -0
  87. package/dist/render/surfaceModel.d.ts +29 -8
  88. package/dist/render/surfaceModel.js +33 -9
  89. package/dist/render/surfaceTexture.d.ts +25 -1
  90. package/dist/render/surfaceTexture.js +27 -5
  91. package/package.json +6 -6
  92. package/src/index.ts +6 -2
  93. package/src/render/anisotropicLobe.ts +29 -0
  94. package/src/render/areaLights.ts +58 -0
  95. package/src/render/backend/api.ts +49 -2
  96. package/src/render/backend/webgl2/renderer.ts +192 -38
  97. package/src/render/backend/webgl2/sceneCaptureTarget.ts +130 -0
  98. package/src/render/backend/webgpu/particlePass.ts +2 -1
  99. package/src/render/backend/webgpu/renderer.ts +194 -19
  100. package/src/render/backend/webgpu/sceneCaptureTarget.ts +63 -0
  101. package/src/render/backend/webgpu/surfaceTexturePass.ts +44 -14
  102. package/src/render/clusteredLights.ts +41 -8
  103. package/src/render/driftLight/bake.ts +3 -1
  104. package/src/render/driftLight/falloff.ts +13 -2
  105. package/src/render/gi/bounceLights.ts +12 -3
  106. package/src/render/instanceCull.ts +4 -0
  107. package/src/render/instances.ts +26 -6
  108. package/src/render/lightBudget.ts +40 -2
  109. package/src/render/lightmap.ts +64 -23
  110. package/src/render/particlePool.ts +23 -0
  111. package/src/render/pointLightSelection.ts +20 -1
  112. package/src/render/renderQuality.ts +8 -7
  113. package/src/render/rendererHarness.ts +3 -0
  114. package/src/render/rgb9e5.ts +52 -0
  115. package/src/render/sceneCapture.ts +56 -0
  116. package/src/render/screenSpaceReflection.ts +33 -1
  117. package/src/render/shaders/bolt.ts +7 -1
  118. package/src/render/shaders/depth.ts +11 -0
  119. package/src/render/shaders/flat/anisotropic.ts +37 -3
  120. package/src/render/shaders/flat/areaLight.ts +46 -0
  121. package/src/render/shaders/flat/glassTint.ts +9 -2
  122. package/src/render/shaders/flat/index.ts +21 -2
  123. package/src/render/shaders/flat/lobes.ts +50 -43
  124. package/src/render/shaders/flat/main.ts +57 -22
  125. package/src/render/shaders/flat/models.ts +100 -16
  126. package/src/render/shaders/flat/preamble.ts +10 -3
  127. package/src/render/shaders/generated/bolt.wgsl.ts +1 -1
  128. package/src/render/shaders/generated/depth.wgsl.ts +4 -4
  129. package/src/render/shaders/generated/flat.wgsl.ts +54 -38
  130. package/src/render/shaders/generated/ssrTrace.wgsl.ts +11 -6
  131. package/src/render/shaders/generated/water.wgsl.ts +23 -16
  132. package/src/render/shaders/gi/probeBake.wgsl.ts +9 -5
  133. package/src/render/shaders/recon/motion.wgsl.ts +13 -2
  134. package/src/render/shaders/ssrTrace.ts +20 -1
  135. package/src/render/shaders/water.ts +12 -3
  136. package/src/render/skinScatter.ts +3 -1
  137. package/src/render/ssrPass.ts +1 -0
  138. package/src/render/surfaceModel.ts +47 -10
  139. package/src/render/surfaceTexture.ts +69 -9
@@ -0,0 +1,130 @@
1
+ import { glSceneDepthFormat } from '../../depthConvention.ts';
2
+ import type { SurfaceTexture } from '../../surfaceTexture.ts';
3
+
4
+ /**
5
+ * A scene capture's framebuffers on WebGL2: the capture's own texture as the colour a pass resolves
6
+ * into, and a depth of its own, at the capture's size. See `sceneCapture.ts` for what a capture is.
7
+ *
8
+ * **At the frame's sample count, as WebGPU's is**, so a capture's edges are antialiased exactly as
9
+ * the frame's are: drawn into multisampled renderbuffers and blitted into the texture at the end,
10
+ * since a texture layer cannot be multisampled and sampled both. A count the driver refuses falls
11
+ * back to one, as the scene target's does, rather than drawing nothing.
12
+ */
13
+ export class GlSceneCaptureTarget {
14
+ /** Where the capture resolves: its texture's layer, and a depth when it is drawn into directly. */
15
+ private readonly resolveFramebuffer: WebGLFramebuffer;
16
+ private readonly resolveDepth: WebGLRenderbuffer;
17
+ /** Where it is drawn above one sample, or null. */
18
+ private multisampleFramebuffer: WebGLFramebuffer | null = null;
19
+ private multisampleColor: WebGLRenderbuffer | null = null;
20
+ private multisampleDepth: WebGLRenderbuffer | null = null;
21
+ /** False where the driver refused the attachment, said once by the renderer. */
22
+ readonly usable: boolean;
23
+
24
+ constructor(
25
+ gl: WebGL2RenderingContext,
26
+ texture: SurfaceTexture,
27
+ readonly width: number,
28
+ readonly height: number,
29
+ samples: number,
30
+ float: boolean,
31
+ ) {
32
+ const depthFormat = glSceneDepthFormat(gl).internalFormat;
33
+ this.resolveFramebuffer = must(gl.createFramebuffer());
34
+ this.resolveDepth = must(gl.createRenderbuffer());
35
+ gl.bindRenderbuffer(gl.RENDERBUFFER, this.resolveDepth);
36
+ gl.renderbufferStorage(gl.RENDERBUFFER, depthFormat, width, height);
37
+ gl.bindFramebuffer(gl.FRAMEBUFFER, this.resolveFramebuffer);
38
+ texture.attachColor(gl);
39
+ gl.framebufferRenderbuffer(
40
+ gl.FRAMEBUFFER,
41
+ gl.DEPTH_ATTACHMENT,
42
+ gl.RENDERBUFFER,
43
+ this.resolveDepth,
44
+ );
45
+ this.usable = gl.checkFramebufferStatus(gl.FRAMEBUFFER) === gl.FRAMEBUFFER_COMPLETE;
46
+
47
+ const maximum = gl.getParameter(gl.MAX_SAMPLES) as number;
48
+ const count = Math.max(
49
+ 1,
50
+ Math.min(Math.round(samples), Number.isFinite(maximum) ? maximum : 1),
51
+ );
52
+ if (this.usable && count > 1) {
53
+ const color = must(gl.createRenderbuffer());
54
+ const depth = must(gl.createRenderbuffer());
55
+ gl.bindRenderbuffer(gl.RENDERBUFFER, color);
56
+ gl.renderbufferStorageMultisample(
57
+ gl.RENDERBUFFER,
58
+ count,
59
+ float ? gl.RGBA16F : gl.RGBA8,
60
+ width,
61
+ height,
62
+ );
63
+ gl.bindRenderbuffer(gl.RENDERBUFFER, depth);
64
+ gl.renderbufferStorageMultisample(gl.RENDERBUFFER, count, depthFormat, width, height);
65
+ const framebuffer = must(gl.createFramebuffer());
66
+ gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
67
+ gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.RENDERBUFFER, color);
68
+ gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, depth);
69
+ if (gl.checkFramebufferStatus(gl.FRAMEBUFFER) === gl.FRAMEBUFFER_COMPLETE) {
70
+ this.multisampleFramebuffer = framebuffer;
71
+ this.multisampleColor = color;
72
+ this.multisampleDepth = depth;
73
+ } else {
74
+ gl.deleteFramebuffer(framebuffer);
75
+ gl.deleteRenderbuffer(color);
76
+ gl.deleteRenderbuffer(depth);
77
+ }
78
+ }
79
+ gl.bindRenderbuffer(gl.RENDERBUFFER, null);
80
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null);
81
+ }
82
+
83
+ /** Bind where the pass draws, size the viewport and clear: colour to `clear`, depth to `depthClear`. */
84
+ begin(
85
+ gl: WebGL2RenderingContext,
86
+ clear: readonly [number, number, number],
87
+ depthClear: number,
88
+ ): void {
89
+ gl.bindFramebuffer(gl.FRAMEBUFFER, this.multisampleFramebuffer ?? this.resolveFramebuffer);
90
+ gl.viewport(0, 0, this.width, this.height);
91
+ gl.clearColor(clear[0], clear[1], clear[2], 1);
92
+ gl.clearDepth(depthClear);
93
+ gl.depthMask(true);
94
+ gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
95
+ }
96
+
97
+ /** Resolve the multisampled colour into the texture, where there is any. */
98
+ end(gl: WebGL2RenderingContext): void {
99
+ if (this.multisampleFramebuffer === null) return;
100
+ gl.bindFramebuffer(gl.READ_FRAMEBUFFER, this.multisampleFramebuffer);
101
+ gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, this.resolveFramebuffer);
102
+ gl.blitFramebuffer(
103
+ 0,
104
+ 0,
105
+ this.width,
106
+ this.height,
107
+ 0,
108
+ 0,
109
+ this.width,
110
+ this.height,
111
+ gl.COLOR_BUFFER_BIT,
112
+ gl.NEAREST,
113
+ );
114
+ gl.bindFramebuffer(gl.READ_FRAMEBUFFER, null);
115
+ gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, null);
116
+ }
117
+
118
+ dispose(gl: WebGL2RenderingContext): void {
119
+ gl.deleteFramebuffer(this.resolveFramebuffer);
120
+ gl.deleteRenderbuffer(this.resolveDepth);
121
+ if (this.multisampleFramebuffer !== null) gl.deleteFramebuffer(this.multisampleFramebuffer);
122
+ if (this.multisampleColor !== null) gl.deleteRenderbuffer(this.multisampleColor);
123
+ if (this.multisampleDepth !== null) gl.deleteRenderbuffer(this.multisampleDepth);
124
+ }
125
+ }
126
+
127
+ function must<T>(made: T | null): T {
128
+ if (made === null) throw new Error('GlSceneCaptureTarget: a GL object could not be created');
129
+ return made;
130
+ }
@@ -3,6 +3,7 @@ import { particleShaders, type ParticleMaterial } from '../../particleMaterial.t
3
3
  import { PARTICLE_BINDINGS } from '../../shaders/generated/particle.wgsl.ts';
4
4
  import { PARTICLE_BLADES, PARTICLE_INDICES, PARTICLE_VERTS } from '../../shaders/particle.ts';
5
5
  import type { ParticleInstances } from '../../particlePool.ts';
6
+ import { DEVICE_PARTICLE_FLOATS } from '../../particlePool.ts';
6
7
  import { packSpriteStream, type ParticleBlend, type ParticleFacing } from '../../particleBatch.ts';
7
8
  import { createParticleSort, type ParticleSort } from '../../particleSort.ts';
8
9
  import { DEPTH_FORMAT } from './flatPass.ts';
@@ -58,7 +59,7 @@ const USAGE_UNIFORM_DST = 0x0040 | 0x0008;
58
59
  * Floats per instance, in the order they are packed: position 3, size 1, spin 1, colour 3,
59
60
  * alpha 1, age 1, seed 1, velocity 3, and a sprite's frame and half-height 2.
60
61
  */
61
- const INSTANCE_FLOATS = 16;
62
+ const INSTANCE_FLOATS = DEVICE_PARTICLE_FLOATS;
62
63
 
63
64
  const INSTANCE_ATTRIBUTES: GPUVertexAttribute[] = [
64
65
  { shaderLocation: 2, offset: 0, format: 'float32x3' },
@@ -129,7 +129,7 @@ import {
129
129
  FACE_COUNT,
130
130
  createResolvedPointShadows,
131
131
  } from '../../pointShadowImage.ts';
132
- import { MAX_AREA_LIGHTS, type AreaLightSource } from '../../areaLights.ts';
132
+ import { MAX_AREA_LIGHTS, packAreaSizesAndDoors, type AreaLightSource } from '../../areaLights.ts';
133
133
  import {
134
134
  areaShadowLayerCount,
135
135
  castingRange,
@@ -175,11 +175,18 @@ import {
175
175
  import type { TextStyle } from '../../textLayout.ts';
176
176
  import { deviceSnappedCellSize, deviceSnappedOrigin } from '../../textLayout.ts';
177
177
  import { GpuSurfaceTexture } from './surfaceTexturePass.ts';
178
+ import { GpuSceneCaptureTarget } from './sceneCaptureTarget.ts';
179
+ import { sceneCaptureTexels } from '../../sceneCapture.ts';
178
180
  import { gpuCompressedFormats } from './compressedUpload.ts';
179
181
  import { GpuClothBinding, GpuClothParticles, clothStandIn } from './clothTextures.ts';
180
182
  import type { ClothViews } from './clothTextures.ts';
181
183
  import type { ComputeHost } from './computeHost.ts';
182
- import { MODEL_PARAM_FLOATS, eyeAxisInWorld, packModel } from '../../surfaceModel.ts';
184
+ import {
185
+ MODEL_PARAM_FLOATS,
186
+ eyeAxisInWorld,
187
+ packModel,
188
+ skinProfileDistance,
189
+ } from '../../surfaceModel.ts';
183
190
  import type { SurfaceModel } from '../../surfaceModel.ts';
184
191
  import { validateClothBinding } from '../../clothBindingData.ts';
185
192
  import type { ClothBindingData } from '../../clothBindingData.ts';
@@ -337,7 +344,7 @@ import type { LineSegments } from '../../linePoints.ts';
337
344
  import { buildPlumeGeometry } from '../../plumeGeometry.ts';
338
345
  import type { ParticleMaterial } from '../../particleMaterial.ts';
339
346
  import { facingCode, type ParticleBatchOptions } from '../../particleBatch.ts';
340
- import type { ParticleInstances } from '../../particlePool.ts';
347
+ import type { DeviceParticles, ParticleInstances } from '../../particlePool.ts';
341
348
  import { PARTICLE_INDICES } from '../../shaders/particle.ts';
342
349
  import type { PlumeOptions, PlumePlacement } from '../../plumeRenderer.ts';
343
350
  import type { InstanceData } from '../../instancedMesh.ts';
@@ -2948,6 +2955,110 @@ export class WebGPURenderer implements RendererApi {
2948
2955
  });
2949
2956
  }
2950
2957
 
2958
+ /**
2959
+ * A texture the scene can be drawn into, for a material to show. See `RendererApi` and
2960
+ * `sceneCapture.ts`. At the world's own format, which under `hdrScene` is half floats.
2961
+ */
2962
+ createSceneCapture(width: number, height: number): GpuSurfaceTexture {
2963
+ const texture = new GpuSurfaceTexture(
2964
+ this.surface.device,
2965
+ this.pipelines,
2966
+ sceneCaptureTexels(width, height, this.quality.hdrScene),
2967
+ { wrap: 'clamp', mipmap: false },
2968
+ );
2969
+ this.captureTargets.set(texture, new GpuSceneCaptureTarget(this.surface.device, width, height));
2970
+ return texture;
2971
+ }
2972
+
2973
+ /** Each capture's attachments, beside the texture a material binds. */
2974
+ private readonly captureTargets = new WeakMap<GpuSurfaceTexture, GpuSceneCaptureTarget>();
2975
+ /**
2976
+ * Whether a scene capture is being drawn: an off-screen pass as a probe bake is, so the frame's
2977
+ * camera, jitter, motion, refraction copy, skin's spread and late routing are left alone, but
2978
+ * lit by the probes and DriftLight as the frame is. See `captureScene`.
2979
+ */
2980
+ private capturePassActive = false;
2981
+
2982
+ /**
2983
+ * Draw the scene into a capture from `camera`. See `RendererApi.captureScene`.
2984
+ *
2985
+ * **Its own encoder, as a probe bake's**: the frame's pass is set aside and restored, the rings
2986
+ * are marked and rewound around the submit, and the frame graph is flushed at both boundaries so
2987
+ * no recorded draw crosses one. **Radiance**: the output transform and exposure are held off.
2988
+ */
2989
+ captureScene(
2990
+ capture: GpuSurfaceTexture,
2991
+ camera: Camera,
2992
+ clearColor: Vec3,
2993
+ draw: (camera: Camera) => void,
2994
+ ): boolean {
2995
+ if (this.surface.lost) return false;
2996
+ const target = this.captureTargets.get(capture);
2997
+ const resolve = capture.layerView();
2998
+ if (target === undefined || resolve === null) return false;
2999
+ camera.updateMatrices(target.width / target.height);
3000
+ const { device } = this.surface;
3001
+ const { depth, colorMsaa } = target.attachments(this.samples, this.pipelines.format);
3002
+
3003
+ const savedPass = this.pass;
3004
+ if (this.quality.frameGraph) this.flushGraph();
3005
+ const savedEncoder = this.encoder;
3006
+ this.markRings();
3007
+ const skinMark = this.skinPalettes.mark();
3008
+ const frameEpoch = this.encoderEpoch;
3009
+ this.encoderEpoch = ++this.encoderEpochs;
3010
+ const encoder = device.createCommandEncoder({ label: 'capture' });
3011
+ this.encoder = encoder;
3012
+ this.capturePassActive = true;
3013
+ const heldGrade = this.perFrameInts[this.materialField('uOutputTransform')] ?? 0;
3014
+ const heldExposure = this.perFrameFloats[this.materialField('uOutputExposure')] ?? 1;
3015
+ this.perFrameInts[this.materialField('uOutputTransform')] = 0;
3016
+ this.perFrameFloats[this.materialField('uOutputExposure')] = 1;
3017
+ this.materials.dirty();
3018
+ const pass = encoder.beginRenderPass({
3019
+ label: 'capture',
3020
+ colorAttachments: [
3021
+ {
3022
+ view: colorMsaa ?? resolve,
3023
+ resolveTarget: colorMsaa === null ? undefined : resolve,
3024
+ loadOp: 'clear',
3025
+ storeOp: resolvedStoreOp(
3026
+ colorMsaa !== null,
3027
+ true,
3028
+ this.quality.discardResolvedAttachments,
3029
+ ),
3030
+ clearValue: { r: clearColor[0], g: clearColor[1], b: clearColor[2], a: 1 },
3031
+ },
3032
+ ],
3033
+ depthStencilAttachment: {
3034
+ view: depth,
3035
+ depthClearValue: DEPTH_CLEAR,
3036
+ depthLoadOp: 'clear',
3037
+ depthStoreOp: 'discard',
3038
+ },
3039
+ });
3040
+ this.pass = pass;
3041
+ try {
3042
+ draw(camera);
3043
+ /* The callback's draws replayed into this pass before it ends: see `bakeProbe`. */
3044
+ if (this.quality.frameGraph) this.flushGraph();
3045
+ } finally {
3046
+ pass.end();
3047
+ this.pass = null;
3048
+ this.flushRings();
3049
+ device.queue.submit([encoder.finish()]);
3050
+ this.rewindRings(skinMark);
3051
+ this.encoderEpoch = frameEpoch;
3052
+ this.encoder = savedEncoder;
3053
+ this.pass = savedPass;
3054
+ this.perFrameInts[this.materialField('uOutputTransform')] = heldGrade;
3055
+ this.perFrameFloats[this.materialField('uOutputExposure')] = heldExposure;
3056
+ this.materials.dirty();
3057
+ this.capturePassActive = false;
3058
+ }
3059
+ return true;
3060
+ }
3061
+
2951
3062
  /**
2952
3063
  * GPU textures an `updateSurfaceTexture` replaced, destroyed when the next frame begins.
2953
3064
  *
@@ -2996,6 +3107,8 @@ export class WebGPURenderer implements RendererApi {
2996
3107
  if (this.modelMap === texture) this.modelMap = null;
2997
3108
  this.forgetBindingsOf(texture);
2998
3109
  texture.dispose();
3110
+ this.captureTargets.get(texture)?.dispose();
3111
+ this.captureTargets.delete(texture);
2999
3112
  }
3000
3113
 
3001
3114
  /**
@@ -3207,9 +3320,9 @@ export class WebGPURenderer implements RendererApi {
3207
3320
  if (model?.kind === 'skin' && this.skinScatter !== null) {
3208
3321
  this.skinScatter.setProfile(
3209
3322
  model.profile,
3210
- model.scatterColor[0] * model.radius,
3211
- model.scatterColor[1] * model.radius,
3212
- model.scatterColor[2] * model.radius,
3323
+ skinProfileDistance(model, 0),
3324
+ skinProfileDistance(model, 1),
3325
+ skinProfileDistance(model, 2),
3213
3326
  );
3214
3327
  }
3215
3328
  this.material((f, i) => {
@@ -3259,7 +3372,14 @@ export class WebGPURenderer implements RendererApi {
3259
3372
  f[emissive + 1] = asked?.[1] ?? 1;
3260
3373
  f[emissive + 2] = asked?.[2] ?? 1;
3261
3374
  /* The model's numbers and whether its map is bound. See `packModel`. */
3262
- packModel(material?.model ?? null, modelMap !== null, this.modelParams);
3375
+ packModel(
3376
+ material?.model ?? null,
3377
+ modelMap !== null,
3378
+ this.modelParams,
3379
+ material?.physicalSpecular === true,
3380
+ );
3381
+ /* The first physical highlight asked for is when the lit pipelines start carrying one. */
3382
+ if (material?.physicalSpecular === true) this.enableLit('PHYSICAL_SPECULAR');
3263
3383
  f.set(this.modelParams, this.materialField('uModelParams'));
3264
3384
  });
3265
3385
  /*
@@ -4175,7 +4295,43 @@ export class WebGPURenderer implements RendererApi {
4175
4295
 
4176
4296
  const device = this.surface.device;
4177
4297
  device.queue.writeBuffer(batch.instances, 0, batch.staging, 0, live * PARTICLE_INSTANCE_FLOATS);
4298
+ this.drawParticleInstances(batch, batch.instances, live, camera, env, timeSeconds);
4299
+ }
4178
4300
 
4301
+ /**
4302
+ * Draw particles a caller's own compute wrote, where they lie. See `DeviceParticles`.
4303
+ *
4304
+ * **The pool's draw with its instance stream swapped**: the same material, blend, flipbook, soft
4305
+ * edge and fog, reading the caller's buffer in place of the one the pool uploads into — so nothing
4306
+ * crosses back to the CPU. Not sorted, and a count past what the buffer holds is cut to it rather
4307
+ * than handed to a draw the device would refuse with the frame.
4308
+ */
4309
+ drawDeviceParticles(
4310
+ batch: GpuParticles,
4311
+ particles: DeviceParticles,
4312
+ camera: Camera,
4313
+ env: Environment,
4314
+ timeSeconds: number,
4315
+ ): void {
4316
+ const fits = Math.floor(particles.buffer.size / (PARTICLE_INSTANCE_FLOATS * 4));
4317
+ const live = Math.min(Math.max(0, Math.floor(particles.count)), fits);
4318
+ /* Not above zero covers a NaN as well: a count or a size that is no number draws nothing. */
4319
+ if (!this.canDraw() || !(live > 0)) return;
4320
+ if (batch.drawnInEncoder === this.encoderEpoch) return;
4321
+ batch.drawnInEncoder = this.encoderEpoch;
4322
+ this.drawParticleInstances(batch, particles.buffer, live, camera, env, timeSeconds);
4323
+ }
4324
+
4325
+ /** A pool's uniforms for this view, and its draw from `instances`: both entry points' tail. */
4326
+ private drawParticleInstances(
4327
+ batch: GpuParticles,
4328
+ instances: GPUBuffer,
4329
+ live: number,
4330
+ camera: Camera,
4331
+ env: Environment,
4332
+ timeSeconds: number,
4333
+ ): void {
4334
+ const device = this.surface.device;
4179
4335
  const v = batch.vertFloats;
4180
4336
  const atV = (name: string): number => (PARTICLE_VERT_FIELDS[name]?.offset ?? -4) / 4;
4181
4337
  mat4.multiply(this.correctedViewProj, CLIP_CORRECTION, camera.viewProjection);
@@ -4255,7 +4411,7 @@ export class WebGPURenderer implements RendererApi {
4255
4411
  particleCommand.pipeline = particlePipe;
4256
4412
  particleCommand.bindGroup = group;
4257
4413
  particleCommand.vertexBuffers[0] = batch.corners;
4258
- particleCommand.vertexBuffers[1] = batch.instances;
4414
+ particleCommand.vertexBuffers[1] = instances;
4259
4415
  particleCommand.vertexCount = 2;
4260
4416
  particleCommand.indexBuffer = batch.indexBuffer;
4261
4417
  particleCommand.indexed = true;
@@ -4267,7 +4423,7 @@ export class WebGPURenderer implements RendererApi {
4267
4423
  pass.setPipeline(particlePipe);
4268
4424
  pass.setBindGroup(0, group);
4269
4425
  pass.setVertexBuffer(0, batch.corners);
4270
- pass.setVertexBuffer(1, batch.instances);
4426
+ pass.setVertexBuffer(1, instances);
4271
4427
  pass.setIndexBuffer(batch.indexBuffer, 'uint32');
4272
4428
  pass.drawIndexed(PARTICLE_INDICES, live);
4273
4429
  }
@@ -4286,7 +4442,12 @@ export class WebGPURenderer implements RendererApi {
4286
4442
  const fields = batch.fields;
4287
4443
  const atF = (name: string): number => (fields[name]?.offset ?? -4) / 4;
4288
4444
  let depth: GPUTextureView | null = null;
4289
- if (batch.softDepth > 0 && !this.reflectionPassActive && !this.probePassActive) {
4445
+ if (
4446
+ batch.softDepth > 0 &&
4447
+ !this.reflectionPassActive &&
4448
+ !this.probePassActive &&
4449
+ !this.capturePassActive
4450
+ ) {
4290
4451
  if (this.openPass() !== null && this.takeVolumeDepth()) depth = this.resolvedDepthView;
4291
4452
  if (depth === null && !this.warnedSpriteDepth) {
4292
4453
  this.warnedSpriteDepth = true;
@@ -4660,6 +4821,7 @@ export class WebGPURenderer implements RendererApi {
4660
4821
  scatterInto(f, WATER_FRAG_FIELDS['uLightRadius'], lights.radii, 1);
4661
4822
  scatterInto(f, WATER_FRAG_FIELDS['uLightSourceRadius'], lights.sourceRadii, 1);
4662
4823
  scatterInto(f, WATER_FRAG_FIELDS['uLightWeight'], lights.weights, 1);
4824
+ scatterInto(f, WATER_FRAG_FIELDS['uLightCone'], lights.conesAndFalloffs, 4);
4663
4825
 
4664
4826
  const medium = resolveAtmosphere(
4665
4827
  env,
@@ -5617,8 +5779,11 @@ export class WebGPURenderer implements RendererApi {
5617
5779
  cookies: new Float32Array(0),
5618
5780
  heldSourceRadii: new Float32Array(MAX_POINT_LIGHTS),
5619
5781
  axesAndChannels: new Float32Array(MAX_POINT_LIGHTS * 4),
5782
+ conesAndFalloffs: new Float32Array(MAX_POINT_LIGHTS * 4),
5620
5783
  heldWeights: new Float32Array(MAX_POINT_LIGHTS),
5621
5784
  };
5785
+ /** The fixed arm's rectangles as `uAreaLightSize` holds them. See `packAreaSizesAndDoors`. */
5786
+ private readonly areaSizesAndDoors = new Float32Array(MAX_AREA_LIGHTS * 4);
5622
5787
  /** Refilled per frame rather than allocated; see `resolveAtmosphere`. */
5623
5788
  private readonly medium: ResolvedAtmosphere = {
5624
5789
  fogColor: new Float32Array(3),
@@ -8217,7 +8382,7 @@ export class WebGPURenderer implements RendererApi {
8217
8382
  * the viewer sees, and bounding it through a mirrored camera puts it in the wrong half of the
8218
8383
  * screen.
8219
8384
  */
8220
- if (!this.reflectionPassActive) {
8385
+ if (!this.reflectionPassActive && !this.capturePassActive) {
8221
8386
  mat4.copy(this.frameRawViewProj, camera.viewProjection);
8222
8387
  /* The eye in world space, which the resolve's normals are turned to face. */
8223
8388
  this.reconEye.set(camera.position);
@@ -8251,7 +8416,8 @@ export class WebGPURenderer implements RendererApi {
8251
8416
  * second temporal frame stepped the sequence twice a frame, so the world saw every other
8252
8417
  * position and the anti-flicker's period never started.
8253
8418
  */
8254
- if (this.framePresented) this.viewProj = this.correctedViewProj;
8419
+ /* A capture has no resolve to undo a jitter and is no frame of the sequence. */
8420
+ if (this.framePresented || this.capturePassActive) this.viewProj = this.correctedViewProj;
8255
8421
  else this.jitterFrame();
8256
8422
  /*
8257
8423
  * The frustum, once a frame, and from the **uncorrected** matrix.
@@ -8593,7 +8759,8 @@ export class WebGPURenderer implements RendererApi {
8593
8759
  * uniform added on one backend is bound on both in the same change.
8594
8760
  */
8595
8761
  scatterInto(f, this.fragment.fields['uLightDir'], lights.directions, 3);
8596
- scatterInto(f, this.fragment.fields['uLightCone'], lights.coneCos, 2);
8762
+ /* The cone and, in z, the light's own falloff exponent. See `conesAndFalloffs`. */
8763
+ scatterInto(f, this.fragment.fields['uLightCone'], lights.conesAndFalloffs, 4);
8597
8764
  scatterInto(f, this.fragment.fields['uLightIesProfile'], lights.iesProfiles, 1);
8598
8765
  /* The azimuth and, in w, the light's channels. See `axesAndChannels`. */
8599
8766
  scatterInto(f, this.fragment.fields['uLightIesAxis'], lights.axesAndChannels, 4);
@@ -8615,7 +8782,9 @@ export class WebGPURenderer implements RendererApi {
8615
8782
  scatterInto(f, this.fragment.fields['uAreaLightColor'], area.colors, 3);
8616
8783
  scatterInto(f, this.fragment.fields['uAreaLightRight'], area.right, 3);
8617
8784
  scatterInto(f, this.fragment.fields['uAreaLightUp'], area.up, 3);
8618
- scatterInto(f, this.fragment.fields['uAreaLightSize'], area.sizes, 2);
8785
+ /* The half extents and, in zw, the barn doors. See `packAreaSizesAndDoors`. */
8786
+ packAreaSizesAndDoors(area, this.areaSizesAndDoors);
8787
+ scatterInto(f, this.fragment.fields['uAreaLightSize'], this.areaSizesAndDoors, 4);
8619
8788
  scatterInto(f, this.fragment.fields['uAreaLightTwoSided'], area.twoSided, 1);
8620
8789
  }
8621
8790
 
@@ -11723,7 +11892,7 @@ export class WebGPURenderer implements RendererApi {
11723
11892
  routesLate(
11724
11893
  this.reconstructing,
11725
11894
  this.reflectionPassActive,
11726
- this.probePassActive,
11895
+ this.probePassActive || this.capturePassActive,
11727
11896
  this.framePresented,
11728
11897
  )
11729
11898
  );
@@ -11803,7 +11972,7 @@ export class WebGPURenderer implements RendererApi {
11803
11972
  return tracksMotion(
11804
11973
  this.reconstructing,
11805
11974
  this.reflectionPassActive,
11806
- this.probePassActive,
11975
+ this.probePassActive || this.capturePassActive,
11807
11976
  this.framePresented,
11808
11977
  );
11809
11978
  }
@@ -12260,6 +12429,8 @@ export class WebGPURenderer implements RendererApi {
12260
12429
  f.set(surface.axis, base + at('uSsrAxis'));
12261
12430
  f.set(surface.tint, base + at('uSsrTint'));
12262
12431
  f[base + at('uSsrStrength')] = surface.strength;
12432
+ f[base + at('uSsrFresnel')] = surface.fresnel ? 1 : 0;
12433
+ f[base + at('uSsrFresnel') + 1] = surface.roughness;
12263
12434
  f[base + at('uSsrFacingCos')] = surface.facingCos;
12264
12435
  f[base + at('uSsrReach')] = surface.reachM;
12265
12436
  f[base + at('uSsrThickness')] = surface.thicknessM;
@@ -12963,7 +13134,7 @@ export class WebGPURenderer implements RendererApi {
12963
13134
  * glass, and a frame that refracts nothing breaks no pass at all.
12964
13135
  */
12965
13136
  private takeRefractSnapshot(): boolean {
12966
- if (this.reflectionPassActive || this.probePassActive) return false;
13137
+ if (this.reflectionPassActive || this.probePassActive || this.capturePassActive) return false;
12967
13138
  if (this.refractSnapshotTaken) return true;
12968
13139
  /* A late draw refracts the reconstructed picture, which does not exist until the resolve has
12969
13140
  run; `runLate` takes the copy then, once, before any late draw is replayed. */
@@ -13044,6 +13215,7 @@ export class WebGPURenderer implements RendererApi {
13044
13215
  !this.skinScattered &&
13045
13216
  !this.reflectionPassActive &&
13046
13217
  !this.probePassActive &&
13218
+ !this.capturePassActive &&
13047
13219
  !this.framePresented &&
13048
13220
  !this.overlayActive &&
13049
13221
  this.oitMode === 'none'
@@ -13133,6 +13305,7 @@ export class WebGPURenderer implements RendererApi {
13133
13305
  this.skinScattered = true;
13134
13306
  if (!skin.pending) return;
13135
13307
  if (this.reflectionPassActive || this.probePassActive || this.oitReplaying) return;
13308
+ if (this.capturePassActive) return;
13136
13309
  const encoder = this.encoder;
13137
13310
  const depth = this.depthView;
13138
13311
  const resolved = this.resolvedDepthView;
@@ -13201,7 +13374,7 @@ export class WebGPURenderer implements RendererApi {
13201
13374
  * own terms: `resolvedDepthView` holds the *frame's* depth, and clamping a mirrored beam
13202
13375
  * against it would stop it at geometry that is not in front of it.
13203
13376
  */
13204
- if (this.reflectionPassActive) return false;
13377
+ if (this.reflectionPassActive || this.capturePassActive) return false;
13205
13378
  if (this.resolvedDepthView === null) return false;
13206
13379
  if (this.volumeDepthTaken) return true;
13207
13380
  const encoder = this.encoder;
@@ -13761,7 +13934,9 @@ export class WebGPURenderer implements RendererApi {
13761
13934
  * any future branch added to one belongs in the other.
13762
13935
  */
13763
13936
  private targetPipelines(): PipelineCache {
13764
- if (this.probePassActive || this.reflectionPassActive) return this.pipelines;
13937
+ if (this.probePassActive || this.reflectionPassActive || this.capturePassActive) {
13938
+ return this.pipelines;
13939
+ }
13765
13940
  return this.overlayActive || this.framePresented ? this.overlayPipelines : this.pipelines;
13766
13941
  }
13767
13942
 
@@ -0,0 +1,63 @@
1
+ import { DEPTH_FORMAT } from '../../depthConvention.ts';
2
+
3
+ /**
4
+ * A scene capture's attachments on WebGPU: the depth and, above one sample, the multisampled colour
5
+ * its pass draws into and resolves from — the capture's own texture being the resolve, since **a
6
+ * texture cannot be both multisampled and sampled**. At the world's sample count and format, because
7
+ * a capture draws the world with the world's pipelines: a pipeline whose count or format disagrees
8
+ * with its attachment is rejected at `finish` and takes the whole capture with it. Remade only if
9
+ * either has changed since. See `sceneCapture.ts` for what a capture is.
10
+ */
11
+ export class GpuSceneCaptureTarget {
12
+ private depth: GPUTexture | null = null;
13
+ private depthView: GPUTextureView | null = null;
14
+ private colorMsaa: GPUTexture | null = null;
15
+ private colorMsaaView: GPUTextureView | null = null;
16
+ private key = '';
17
+
18
+ constructor(
19
+ private readonly device: GPUDevice,
20
+ readonly width: number,
21
+ readonly height: number,
22
+ ) {}
23
+
24
+ attachments(
25
+ samples: number,
26
+ format: GPUTextureFormat,
27
+ ): { depth: GPUTextureView; colorMsaa: GPUTextureView | null } {
28
+ const key = `${samples}:${format}`;
29
+ if (this.depthView === null || key !== this.key) {
30
+ this.dispose();
31
+ this.key = key;
32
+ this.depth = this.device.createTexture({
33
+ label: 'capture.depth',
34
+ size: [this.width, this.height],
35
+ format: DEPTH_FORMAT,
36
+ sampleCount: samples,
37
+ usage: 0x10, // RENDER_ATTACHMENT
38
+ });
39
+ this.depthView = this.depth.createView();
40
+ this.colorMsaa =
41
+ samples > 1
42
+ ? this.device.createTexture({
43
+ label: 'capture.colorMsaa',
44
+ size: [this.width, this.height],
45
+ format,
46
+ sampleCount: samples,
47
+ usage: 0x10, // RENDER_ATTACHMENT
48
+ })
49
+ : null;
50
+ this.colorMsaaView = this.colorMsaa?.createView() ?? null;
51
+ }
52
+ return { depth: this.depthView as GPUTextureView, colorMsaa: this.colorMsaaView };
53
+ }
54
+
55
+ dispose(): void {
56
+ this.depth?.destroy();
57
+ this.colorMsaa?.destroy();
58
+ this.depth = null;
59
+ this.depthView = null;
60
+ this.colorMsaa = null;
61
+ this.colorMsaaView = null;
62
+ }
63
+ }