@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
package/src/index.ts CHANGED
@@ -1065,8 +1065,12 @@ export {
1065
1065
  INSTANCE_FLOATS,
1066
1066
  INSTANCE_STRIDE,
1067
1067
  } from './render/instances.ts';
1068
- export { ParticlePool } from './render/particlePool.ts';
1069
- export type { ParticlePoolOptions, ParticleInstances } from './render/particlePool.ts';
1068
+ export { ParticlePool, DEVICE_PARTICLE_FLOATS } from './render/particlePool.ts';
1069
+ export type {
1070
+ DeviceParticles,
1071
+ ParticlePoolOptions,
1072
+ ParticleInstances,
1073
+ } from './render/particlePool.ts';
1070
1074
  export { ParticleBatch } from './render/particleBatch.ts';
1071
1075
  export type {
1072
1076
  ParticleBatchOptions,
@@ -39,3 +39,32 @@ export function anisotropicLobe(
39
39
  const d = x * x + y * y + nh * nh;
40
40
  return 1 / Math.max(d * d, 1e-8);
41
41
  }
42
+
43
+ /** Three components in the lobe's frame: along the direction, across it, along the normal. */
44
+ export type FrameVector = readonly [number, number, number];
45
+
46
+ /**
47
+ * The physical form of the same lobe, `π · D · Vis · N·L`, for a surface whose highlight is GGX's
48
+ * own (`SurfaceMaterial.physicalSpecular`): the stretched distribution normalised as a BRDF's, and
49
+ * Smith's height-correlated masking stretched with it (Heitz 2014, as Filament writes it), Fresnel
50
+ * left to the caller. The half vector, the light and the view are each in the lobe's frame. At
51
+ * strength 0 it is isotropic GGX, the term skin's and the eye's highlights are.
52
+ */
53
+ export function anisotropicPhysicalLobe(
54
+ half: FrameVector,
55
+ light: FrameVector,
56
+ view: FrameVector,
57
+ alphaT: number,
58
+ alphaB: number,
59
+ ): number {
60
+ const x = half[0] / alphaT;
61
+ const y = half[1] / alphaB;
62
+ const d = x * x + y * y + half[2] * half[2];
63
+ const distribution = 1 / (Math.PI * alphaT * alphaB * d * d);
64
+ const ndl = light[2];
65
+ const ndv = view[2];
66
+ const lambdaView = ndl * Math.hypot(alphaT * view[0], alphaB * view[1], ndv);
67
+ const lambdaLight = ndv * Math.hypot(alphaT * light[0], alphaB * light[1], ndl);
68
+ const visibility = 0.5 / Math.max(lambdaView + lambdaLight, 1e-5);
69
+ return Math.PI * distribution * visibility * ndl;
70
+ }
@@ -97,6 +97,22 @@ export interface AreaLightSource {
97
97
  * of a scene unit, and never inside the rectangle itself. Unreal's `AttenuationRadius`, in metres.
98
98
  */
99
99
  range?: number;
100
+ /**
101
+ * Barn doors: a flap hinged at each of the rectangle's four edges, standing this many degrees from
102
+ * its normal. 90, or absent, folds them flat and they hide nothing; smaller closes them, so the
103
+ * light narrows to the opening they leave — a rectangle at 50° throws a beam rather than filling
104
+ * its hemisphere. Unreal's `BarnDoorAngle`, whose default of 88° is nearly flat.
105
+ *
106
+ * What a fragment sees past the doors is the part of the rectangle no door's tip hides, one axis
107
+ * at a time, and the rectangle then shades as that smaller rectangle, its highlight included.
108
+ * Its shadow map is still baked from the whole rectangle's centre. **What it gives up**: each
109
+ * door is taken as endless along its hinge, so the two axes clip apart and the corner where two
110
+ * doors meet hides a square rather than the mitre two real flaps make. One-sided: a two-sided
111
+ * rectangle's back face has none.
112
+ */
113
+ barnDoorAngle?: number;
114
+ /** The barn doors' length in metres. Absent, 0.2: Unreal's default of 20 cm. */
115
+ barnDoorLength?: number;
100
116
  }
101
117
 
102
118
  /** The irradiance a derived `range` ends at, in scene units. See `AreaLightSource.range`. */
@@ -113,6 +129,11 @@ export interface AreaLightBuffer {
113
129
  readonly twoSided: Float32Array;
114
130
  /** One a rectangle: how far its light reaches. See `AreaLightSource.range`. */
115
131
  readonly ranges?: Float32Array;
132
+ /**
133
+ * Two a rectangle: the cosine of its barn doors' angle from its normal, then their length in
134
+ * metres — 0 and 0 for none. See `AreaLightSource.barnDoorAngle`.
135
+ */
136
+ readonly barnDoors?: Float32Array;
116
137
  }
117
138
 
118
139
  export function createAreaLightBuffer(capacity: number = MAX_AREA_LIGHTS): AreaLightBuffer {
@@ -125,9 +146,45 @@ export function createAreaLightBuffer(capacity: number = MAX_AREA_LIGHTS): AreaL
125
146
  sizes: new Float32Array(capacity * 2),
126
147
  twoSided: new Float32Array(capacity),
127
148
  ranges: new Float32Array(capacity),
149
+ barnDoors: new Float32Array(capacity * 2),
128
150
  };
129
151
  }
130
152
 
153
+ /** Unreal's default barn door length, in metres. See `AreaLightSource.barnDoorLength`. */
154
+ export const DEFAULT_BARN_DOOR_LENGTH = 0.2;
155
+
156
+ /**
157
+ * A rectangle's barn doors as the shader reads them, into `out[at]` and `out[at + 1]`: the cosine of
158
+ * their angle and their length. An angle not between 0 and 90 degrees, or a length not above zero,
159
+ * is no doors at all, written as 0 and 0 — which the shader reads as "nothing hidden".
160
+ */
161
+ export function barnDoorsOf(light: AreaLightSource, out: Float32Array, at: number): void {
162
+ const angle = light.barnDoorAngle;
163
+ const length = light.barnDoorLength ?? DEFAULT_BARN_DOOR_LENGTH;
164
+ if (angle === undefined || !(angle >= 0 && angle < 90) || !(length > 0)) {
165
+ out[at] = 0;
166
+ out[at + 1] = 0;
167
+ return;
168
+ }
169
+ out[at] = Math.cos((angle * Math.PI) / 180);
170
+ out[at + 1] = length;
171
+ }
172
+
173
+ /**
174
+ * The fixed arm's half extents and barn doors, four a rectangle as `uAreaLightSize` holds them: the
175
+ * two half extents, then the doors' cosine and length. Into a target the binder owns, for the first
176
+ * `MAX_AREA_LIGHTS` alone, which are all the fixed arm reads.
177
+ */
178
+ export function packAreaSizesAndDoors(lights: AreaLightBuffer, out: Float32Array): void {
179
+ const doors = lights.barnDoors;
180
+ for (let a = 0; a < MAX_AREA_LIGHTS; a++) {
181
+ out[a * 4] = lights.sizes[a * 2] ?? 0;
182
+ out[a * 4 + 1] = lights.sizes[a * 2 + 1] ?? 0;
183
+ out[a * 4 + 2] = doors?.[a * 2] ?? 0;
184
+ out[a * 4 + 3] = doors?.[a * 2 + 1] ?? 0;
185
+ }
186
+ }
187
+
131
188
  /**
132
189
  * How far a rectangle's light reaches when it names no range: where its irradiance on its own axis,
133
190
  * `L · A / d²` for a rectangle of radiance `L` and area `A` seen from far, falls to
@@ -227,6 +284,7 @@ export function selectAreaLights(
227
284
  ? light.range
228
285
  : areaRange(light.r, light.g, light.b, light.halfWidth, light.halfHeight);
229
286
  }
287
+ if (out.barnDoors !== undefined) barnDoorsOf(light, out.barnDoors, slot * 2);
230
288
  }
231
289
 
232
290
  return out;
@@ -1,5 +1,6 @@
1
1
  import type { Bounds } from '../../math/bounds.ts';
2
2
  import type { Vec3 } from '../../math/color.ts';
3
+ import type { Camera } from '../camera.ts';
3
4
  import type { ReadonlyMat4 } from 'gl-matrix';
4
5
  import type { MeshData } from '../mesh.ts';
5
6
  import type { ProbeBakeOptions } from '../reflectionProbe.ts';
@@ -8,6 +9,7 @@ import type { InstancedOptions, MeshInstances } from '../instances.ts';
8
9
  import type { SurfaceMaterial } from '../surfaceTexture.ts';
9
10
  import type { SurfaceSource } from '../compressedSource.ts';
10
11
  import type { LightmapPage } from '../lightmap.ts';
12
+ import type { DeviceParticles } from '../particlePool.ts';
11
13
  import type { ClothBindingData } from '../clothBindingData.ts';
12
14
  import type { TextStyle } from '../textLayout.ts';
13
15
  import type { SdfFont } from '../sdfFont.ts';
@@ -276,6 +278,7 @@ export type RendererApi = Omit<
276
278
  | 'disposeCaustics'
277
279
  | 'createParticles'
278
280
  | 'drawParticles'
281
+ | 'drawDeviceParticles'
279
282
  | 'disposeParticles'
280
283
  | 'createWindStreaks'
281
284
  | 'drawWindStreaks'
@@ -302,6 +305,8 @@ export type RendererApi = Omit<
302
305
  | 'createSurfaceTexture'
303
306
  | 'createSurfaceTextureArray'
304
307
  | 'createLightmap'
308
+ | 'createSceneCapture'
309
+ | 'captureScene'
305
310
  | 'updateSurfaceTexture'
306
311
  | 'disposeSurfaceTexture'
307
312
  | 'setSurfaceTexture'
@@ -365,13 +370,41 @@ export type RendererApi = Omit<
365
370
 
366
371
  /**
367
372
  * A baked lightmap page on the device, as the texture a `lightmapModel` material takes for its
368
- * `modelMap`: two layers of half floats, the irradiance and the direction, filtered linearly and
369
- * clamped at its edges, with no mip chain. Disposed as any texture is, with
373
+ * `modelMap`: two layers of four bytes a texel, the irradiance as an rgb9e5 word and the
374
+ * direction, which the lit stage decodes and filters itself, clamped at the page's edges, with no
375
+ * mip chain. A page whose irradiance arrives packed uploads where it lies. Disposed as any texture is, with
370
376
  * `disposeSurfaceTexture`; `updateSurfaceTexture` refuses it, as it refuses any array. See
371
377
  * `lightmap.ts` for what a surface does with it.
372
378
  */
373
379
  createLightmap(page: LightmapPage): SurfaceTextureHandle;
374
380
 
381
+ /**
382
+ * A texture the scene can be drawn into from a camera of the caller's (`captureScene`), which a
383
+ * material then shows like any image — most often as its `emissive`, for a screen in the world
384
+ * showing what a camera elsewhere sees. `width` and `height` in pixels, 1 to 4096. It holds
385
+ * radiance, at the colour format a probe takes: half floats under `hdrScene`, bytes otherwise.
386
+ * Disposed as any texture is; `updateSurfaceTexture` refuses it. See `sceneCapture.ts`.
387
+ */
388
+ createSceneCapture(width: number, height: number): SurfaceTextureHandle;
389
+
390
+ /**
391
+ * Draw the scene into a capture from `camera`, at the moment a caller says, usually once a frame
392
+ * before the frame's own world. `draw` is handed the camera, its matrices updated for the
393
+ * capture's shape, and submits the scene exactly as it would to the screen — `bindMeshPass`,
394
+ * `drawMesh` and the rest; `bindMeshPass` the frame's own camera again afterwards. The output
395
+ * transform is held off, so a screen showing it is graded once, with the frame.
396
+ *
397
+ * **The mesh pass alone**, as a probe bake is: what a scene draws after its world is in the
398
+ * capture only if `draw` draws it, and no temporal resolve or effect after the world runs on it.
399
+ * Returns whether it drew: false for a texture that is not a capture.
400
+ */
401
+ captureScene(
402
+ capture: SurfaceTextureHandle,
403
+ camera: Camera,
404
+ clearColor: Vec3,
405
+ draw: (camera: Camera) => void,
406
+ ): boolean;
407
+
375
408
  /**
376
409
  * Replace a texture's pixels, keeping the GPU object and its sampler state.
377
410
  *
@@ -735,6 +768,20 @@ export type RendererApi = Omit<
735
768
  timeSeconds: number,
736
769
  ): void;
737
770
 
771
+ /**
772
+ * Draw a pool's particles from a buffer a caller's own compute wrote (`registerCompute`), where
773
+ * they lie: the pool's material, blend, flipbook and fades, its instance stream the caller's
774
+ * buffer. WebGPU only; WebGL2 says so once and draws nothing. Not sorted. See `DeviceParticles`
775
+ * for the layout.
776
+ */
777
+ drawDeviceParticles(
778
+ batch: ParticleHandle,
779
+ particles: DeviceParticles,
780
+ camera: Parameters<Renderer['drawParticles']>[2],
781
+ env: Parameters<Renderer['drawParticles']>[3],
782
+ timeSeconds: number,
783
+ ): void;
784
+
738
785
  /** Release a particle pool. */
739
786
  disposeParticles(batch: ParticleHandle): void;
740
787
 
@@ -136,7 +136,12 @@ import {
136
136
  MORPH_DELTA_TEXTURE_UNIT,
137
137
  SKIN_PALETTE_TEXTURE_UNIT,
138
138
  } from '../../lightBudget.ts';
139
- import { MODEL_PARAM_FLOATS, eyeAxisInWorld, packModel } from '../../surfaceModel.ts';
139
+ import {
140
+ MODEL_PARAM_FLOATS,
141
+ eyeAxisInWorld,
142
+ packModel,
143
+ skinProfileDistance,
144
+ } from '../../surfaceModel.ts';
140
145
  import type { SurfaceModelKind } from '../../surfaceModel.ts';
141
146
  import { lightmapRegionsBound, modelBound, skinHalfBound } from '../../shaders/flat/models.ts';
142
147
  import {
@@ -251,6 +256,8 @@ import type { IncrementalMesh } from '../../mesh.ts';
251
256
  import type { MeshData } from '../../mesh.ts';
252
257
  import type { MeshOptions } from '../api.ts';
253
258
  import { SurfaceTexture } from '../../surfaceTexture.ts';
259
+ import { sceneCaptureTexels } from '../../sceneCapture.ts';
260
+ import { GlSceneCaptureTarget } from './sceneCaptureTarget.ts';
254
261
  import { glCompressedFormats } from '../../glCompressed.ts';
255
262
  import type { CompressedTextureFormat, SurfaceSource } from '../../compressedSource.ts';
256
263
  import type { SurfaceMaterial, SurfaceTextureOptions } from '../../surfaceTexture.ts';
@@ -274,7 +281,7 @@ import { PlanarReflection } from '../../planarReflection.ts';
274
281
  import { InstancedMesh } from '../../instancedMesh.ts';
275
282
  import { ParticleBatch } from '../../particleBatch.ts';
276
283
  import type { ParticleBatchOptions } from '../../particleBatch.ts';
277
- import type { ParticleInstances } from '../../particlePool.ts';
284
+ import type { DeviceParticles, ParticleInstances } from '../../particlePool.ts';
278
285
  import { BoltBatch } from '../../boltBatch.ts';
279
286
  import type { BoltSegments } from '../../boltPool.ts';
280
287
  import { LineBatch } from '../../lineBatch.ts';
@@ -435,6 +442,11 @@ export interface Environment extends Atmosphere {
435
442
  lightDirections: Float32Array;
436
443
  /** Two per active light: the cosine of the inner cone angle, then of the outer. */
437
444
  lightConeCos: Float32Array;
445
+ /**
446
+ * One per active light: its own falloff exponent, or 0 for the frame's. Optional: absent is 0 for
447
+ * every light. See `PointLightSource.falloffExponent`.
448
+ */
449
+ lightFalloffExponents?: Float32Array;
438
450
  /** One per active light: a row of the photometric atlas, or −1 for none. */
439
451
  lightIesProfiles: Float32Array;
440
452
  /**
@@ -1621,6 +1633,15 @@ export class WebGL2Renderer implements RendererApi {
1621
1633
  private warnedEnvironmentAmbient = false;
1622
1634
  /** Whether a probe is being baked, so the passes that would recurse into one can tell. */
1623
1635
  private probePassActive = false;
1636
+ /**
1637
+ * Whether a scene capture is being drawn (`captureScene`): an off-screen pass like a bake, so the
1638
+ * frame's own view, its refraction copy and skin's spread are left alone, but lit as the frame is.
1639
+ */
1640
+ private capturePassActive = false;
1641
+ /** Each capture's framebuffer, beside the texture a material binds. See `createSceneCapture`. */
1642
+ private readonly captureTargets = new WeakMap<SurfaceTexture, GlSceneCaptureTarget>();
1643
+ /** Said once, where a driver refuses to draw into a capture. */
1644
+ private warnedCapture = false;
1624
1645
  /** Whether the bake under way lights its faces by the grid. `ProbeBakeOptions.bounce`. */
1625
1646
  private probeBounce = false;
1626
1647
  /** The clip-control extension, or null where this context does not offer it. */
@@ -1692,12 +1713,14 @@ export class WebGL2Renderer implements RendererApi {
1692
1713
  * the camera's matrix unchanged and multiplies nothing.
1693
1714
  */
1694
1715
  private viewProjFor(camera: { readonly viewProjection: ReadonlyMat4 }): ReadonlyMat4 {
1695
- const scene = this.probePassActive
1696
- ? (cubeFaceProjection(
1697
- this.cubeFaceViewProj,
1698
- this.sceneMatrix(camera.viewProjection) as unknown as ArrayLike<number>,
1699
- ) as unknown as ReadonlyMat4)
1700
- : this.sceneMatrix(camera.viewProjection);
1716
+ /* A capture's rows are flipped as a probe face's are, so it reads as an image does. */
1717
+ const scene =
1718
+ this.probePassActive || this.capturePassActive
1719
+ ? (cubeFaceProjection(
1720
+ this.cubeFaceViewProj,
1721
+ this.sceneMatrix(camera.viewProjection) as unknown as ArrayLike<number>,
1722
+ ) as unknown as ReadonlyMat4)
1723
+ : this.sceneMatrix(camera.viewProjection);
1701
1724
  /*
1702
1725
  * **The jitter goes on here and nowhere else.** This is the one funnel every geometry upload
1703
1726
  * passes through, so one line covers the mesh passes, the sky, the water and the effects
@@ -3499,6 +3522,92 @@ export class WebGL2Renderer implements RendererApi {
3499
3522
  return new SurfaceTexture(this.gl, lightmapTexels(page), { wrap: 'clamp', mipmap: false });
3500
3523
  }
3501
3524
 
3525
+ /**
3526
+ * A texture the scene can be drawn into, for a material to show. See `RendererApi` and
3527
+ * `sceneCapture.ts`. Half floats where the frame keeps range, as a probe does.
3528
+ */
3529
+ createSceneCapture(width: number, height: number): SurfaceTexture {
3530
+ const float = this.quality.hdrScene && this.gl.getExtension('EXT_color_buffer_float') !== null;
3531
+ const texture = new SurfaceTexture(this.gl, sceneCaptureTexels(width, height, float), {
3532
+ wrap: 'clamp',
3533
+ mipmap: false,
3534
+ });
3535
+ this.captureTargets.set(
3536
+ texture,
3537
+ new GlSceneCaptureTarget(
3538
+ this.gl,
3539
+ texture,
3540
+ width,
3541
+ height,
3542
+ this.sceneTarget?.sampleCount ?? 1,
3543
+ float,
3544
+ ),
3545
+ );
3546
+ return texture;
3547
+ }
3548
+
3549
+ /**
3550
+ * Draw the scene into a capture from `camera`: `draw` is handed the camera, its matrices updated
3551
+ * for the capture's shape, and submits the scene as it would to the screen. See `RendererApi`.
3552
+ *
3553
+ * **Radiance, as a probe bake stores it**: the output transform and exposure are held off for the
3554
+ * pass, so the frame grades a screen showing it once. No temporal jitter, which a capture has no
3555
+ * resolve to undo. Returns whether it drew: false for a texture that is not a capture, or one the
3556
+ * driver will not draw into.
3557
+ */
3558
+ captureScene(
3559
+ capture: SurfaceTexture,
3560
+ camera: Camera,
3561
+ clearColor: Vec3,
3562
+ draw: (camera: Camera) => void,
3563
+ ): boolean {
3564
+ if (this.contextLost) return false;
3565
+ const target = this.captureTargets.get(capture);
3566
+ if (target === undefined) return false;
3567
+ if (!target.usable) {
3568
+ if (!this.warnedCapture) {
3569
+ this.warnedCapture = true;
3570
+ console.warn(
3571
+ 'Renderer: this driver will not draw into a scene capture, so it keeps its clear. ' +
3572
+ 'Nothing else is affected.',
3573
+ );
3574
+ }
3575
+ return false;
3576
+ }
3577
+ camera.updateMatrices(target.width / target.height);
3578
+ const { gl } = this;
3579
+ const jittering = this.temporalJittering;
3580
+ this.temporalJittering = false;
3581
+ this.capturePassActive = true;
3582
+ this.useFlatProgram();
3583
+ gl.uniform1i(this.flatUniforms['uOutputTransform'] ?? null, 0);
3584
+ gl.uniform1f(this.flatUniforms['uOutputExposure'] ?? null, 1);
3585
+ this.materials.dirty();
3586
+ target.begin(gl, clearColor, this.depthClear);
3587
+ /*
3588
+ * **Rows flipped, as a probe face's are.** A GL framebuffer's first row is its bottom and an
3589
+ * image's is its top, so a capture drawn upright would be shown upside down by every material
3590
+ * reading it as the image it is; `viewProjFor` negates clip y for the pass, and negating one axis
3591
+ * reverses every triangle's winding, which the front face follows. WebGPU's rows already run
3592
+ * the image's way.
3593
+ */
3594
+ gl.frontFace(gl.CW);
3595
+ try {
3596
+ draw(camera);
3597
+ } finally {
3598
+ target.end(gl);
3599
+ gl.frontFace(gl.CCW);
3600
+ this.useFlatProgram();
3601
+ gl.uniform1i(this.flatUniforms['uOutputTransform'] ?? null, this.gradeCode());
3602
+ gl.uniform1f(this.flatUniforms['uOutputExposure'] ?? null, this.gradeExposure());
3603
+ this.materials.dirty();
3604
+ this.capturePassActive = false;
3605
+ this.temporalJittering = jittering;
3606
+ this.restoreViewport();
3607
+ }
3608
+ return true;
3609
+ }
3610
+
3502
3611
  /**
3503
3612
  * Replace a surface texture's pixels, keeping the GPU object and its sampler state.
3504
3613
  *
@@ -3519,6 +3628,8 @@ export class WebGL2Renderer implements RendererApi {
3519
3628
  disposeSurfaceTexture(texture: SurfaceTexture): void {
3520
3629
  if (this.contextLost) return;
3521
3630
  texture.dispose(this.gl);
3631
+ this.captureTargets.get(texture)?.dispose(this.gl);
3632
+ this.captureTargets.delete(texture);
3522
3633
  }
3523
3634
 
3524
3635
  /**
@@ -3751,9 +3862,9 @@ export class WebGL2Renderer implements RendererApi {
3751
3862
  if (model?.kind === 'skin' && this.skinScatter !== null) {
3752
3863
  this.skinScatter.setProfile(
3753
3864
  model.profile,
3754
- model.scatterColor[0] * model.radius,
3755
- model.scatterColor[1] * model.radius,
3756
- model.scatterColor[2] * model.radius,
3865
+ skinProfileDistance(model, 0),
3866
+ skinProfileDistance(model, 1),
3867
+ skinProfileDistance(model, 2),
3757
3868
  );
3758
3869
  }
3759
3870
  /*
@@ -3901,7 +4012,14 @@ export class WebGL2Renderer implements RendererApi {
3901
4012
  gl.bindTexture(gl.TEXTURE_2D_ARRAY, this.emptyTexture2DArray);
3902
4013
  if (modelMap !== null) modelMap.bind(gl, MODEL_MAP_TEXTURE_UNIT);
3903
4014
  gl.uniform1i(u['uModelMap'] ?? null, MODEL_MAP_TEXTURE_UNIT);
3904
- packModel(material?.model ?? null, modelMap !== null, this.modelParams);
4015
+ packModel(
4016
+ material?.model ?? null,
4017
+ modelMap !== null,
4018
+ this.modelParams,
4019
+ material?.physicalSpecular === true,
4020
+ );
4021
+ /* The first physical highlight asked for is when the lit programs start carrying one. */
4022
+ if (material?.physicalSpecular === true) this.askLit('PHYSICAL_SPECULAR');
3905
4023
  gl.uniform4fv(u['uModelParams'] ?? null, this.modelParams);
3906
4024
 
3907
4025
  /* The albedo's effects table, or the stand-in the shader reads as none. Before the early
@@ -3963,18 +4081,6 @@ export class WebGL2Renderer implements RendererApi {
3963
4081
  gl.bindTexture(gl.TEXTURE_2D_ARRAY, this.emptyTexture2DArray);
3964
4082
  }
3965
4083
 
3966
- /**
3967
- * A pass-level scale over the grain the geometry itself declared, 0 to 1.
3968
- *
3969
- * Which surfaces are mineral is now `MeshData.grain`, stated per vertex through
3970
- * `MeshBuilder.setGrain` — because it is a property of a material, and absent means none.
3971
- * This stays as the override for the case that has no answer to state: an imported model
3972
- * arrives with no grain information at all, and a caller drawing one decides for it.
3973
- *
3974
- * Pass state, like `setSurfaceTexture`, because a material covers many draws. The default
3975
- * is 1 and `bindMeshPass` restores it, so it scales geometry that declared grain and
3976
- * leaves geometry that did not exactly as it is.
3977
- */
3978
4084
  /**
3979
4085
  * The ambient the following draws take, as nine spherical-harmonic coefficients of incoming
3980
4086
  * radiance — red, green and blue of each, twenty-seven numbers — or `null` for the frame's own:
@@ -3986,16 +4092,6 @@ export class WebGL2Renderer implements RendererApi {
3986
4092
  * only — a glossy surface still reflects the probes — and it is pass state like the material,
3987
4093
  * which `bindMeshPass` clears, so a mirror or a probe bake drawn after does not inherit it.
3988
4094
  */
3989
- /**
3990
- * How much of the environment the following draws mirror, 0 to 1.
3991
- *
3992
- * Pass state beside the grain, and the counterpart to it: grain is a surface being uneven,
3993
- * this is a surface being smooth enough to carry an image. Polished paint, glass, chrome
3994
- * and still water want it; plaster and stone want none.
3995
- *
3996
- * Defaults to zero and bindMeshPass restores it, so a scene that never calls this looks
3997
- * exactly as it did.
3998
- */
3999
4095
  setAmbientSH(coefficients: ArrayLike<number> | null): void {
4000
4096
  if (this.contextLost) return;
4001
4097
  packAmbientSH(coefficients, this.ambientSH);
@@ -4007,6 +4103,16 @@ export class WebGL2Renderer implements RendererApi {
4007
4103
  this.useFlatProgram();
4008
4104
  }
4009
4105
 
4106
+ /**
4107
+ * How much of the environment the following draws mirror, 0 to 1.
4108
+ *
4109
+ * Pass state beside the grain, and the counterpart to it: grain is a surface being uneven,
4110
+ * this is a surface being smooth enough to carry an image. Polished paint, glass, chrome
4111
+ * and still water want it; plaster and stone want none.
4112
+ *
4113
+ * Defaults to zero and bindMeshPass restores it, so a scene that never calls this looks
4114
+ * exactly as it did.
4115
+ */
4010
4116
  setSurfaceReflectivity(amount: number): void {
4011
4117
  if (this.contextLost) return;
4012
4118
  this.materials.dirty();
@@ -4056,6 +4162,18 @@ export class WebGL2Renderer implements RendererApi {
4056
4162
  );
4057
4163
  }
4058
4164
 
4165
+ /**
4166
+ * A pass-level scale over the grain the geometry itself declared, 0 to 1.
4167
+ *
4168
+ * Which surfaces are mineral is now `MeshData.grain`, stated per vertex through
4169
+ * `MeshBuilder.setGrain` — because it is a property of a material, and absent means none.
4170
+ * This stays as the override for the case that has no answer to state: an imported model
4171
+ * arrives with no grain information at all, and a caller drawing one decides for it.
4172
+ *
4173
+ * Pass state, like `setSurfaceTexture`, because a material covers many draws. The default
4174
+ * is 1 and `bindMeshPass` restores it, so it scales geometry that declared grain and
4175
+ * leaves geometry that did not exactly as it is.
4176
+ */
4059
4177
  setSurfaceGrain(amount: number): void {
4060
4178
  if (this.contextLost) return;
4061
4179
  this.materials.dirty();
@@ -4485,7 +4603,7 @@ export class WebGL2Renderer implements RendererApi {
4485
4603
  const refraction = options.refraction ?? 0;
4486
4604
  const glassy = resolveGlass(options.glass, this.glassScratch);
4487
4605
  if (refraction <= 0 && !glassy) return false;
4488
- if (this.probePassActive || this.reflectionPassActive) return false;
4606
+ if (this.probePassActive || this.reflectionPassActive || this.capturePassActive) return false;
4489
4607
  const snapshot = this.sceneTarget?.snapshotColor(false, glassy) ?? null;
4490
4608
  if (snapshot === null) return false;
4491
4609
  gl.activeTexture(gl.TEXTURE0 + REFRACT_SCENE_TEXTURE_UNIT);
@@ -4847,6 +4965,29 @@ export class WebGL2Renderer implements RendererApi {
4847
4965
  return new ParticleBatch(this.gl, capacity, options);
4848
4966
  }
4849
4967
 
4968
+ /**
4969
+ * Particles a caller's own compute wrote: WebGPU's alone, since this backend has no compute stage
4970
+ * to have written them (`computeSupported`). Said once, and nothing is drawn.
4971
+ */
4972
+ drawDeviceParticles(
4973
+ _batch: ParticleBatch,
4974
+ _particles: DeviceParticles,
4975
+ _camera: Camera,
4976
+ _env: Environment,
4977
+ _timeSeconds: number,
4978
+ ): void {
4979
+ if (this.warnedDeviceParticles) return;
4980
+ this.warnedDeviceParticles = true;
4981
+ console.warn(
4982
+ 'Renderer: drawDeviceParticles draws particles a compute shader wrote, and WebGL2 has no ' +
4983
+ 'compute stage, so nothing is drawn. Check `computeSupported`, and draw a pool with ' +
4984
+ '`drawParticles` where it is false.',
4985
+ );
4986
+ }
4987
+
4988
+ /** Said once: see `drawDeviceParticles`. */
4989
+ private warnedDeviceParticles = false;
4990
+
4850
4991
  /**
4851
4992
  * Draw a live particle pool.
4852
4993
  *
@@ -4913,7 +5054,12 @@ export class WebGL2Renderer implements RendererApi {
4913
5054
  const u = batch.uniforms;
4914
5055
  const scene = this.sceneTarget;
4915
5056
  let depth: WebGLTexture | null = null;
4916
- if (batch.softDepth > 0 && !this.reflectionPassActive && !this.probePassActive) {
5057
+ if (
5058
+ batch.softDepth > 0 &&
5059
+ !this.reflectionPassActive &&
5060
+ !this.probePassActive &&
5061
+ !this.capturePassActive
5062
+ ) {
4917
5063
  depth = scene?.snapshotDepth(true) ?? null;
4918
5064
  if (depth === null && !this.warnedSpriteDepth) {
4919
5065
  this.warnedSpriteDepth = true;
@@ -5982,6 +6128,7 @@ export class WebGL2Renderer implements RendererApi {
5982
6128
  lightFixtures: this.litOn.LIGHT_FIXTURES,
5983
6129
  surfaceEffects: this.litOn.SURFACE_EFFECTS,
5984
6130
  driftLight: this.litOn.DRIFT_LIGHT,
6131
+ physicalSpecular: this.litOn.PHYSICAL_SPECULAR,
5985
6132
  maxLights: budget.maxLights,
5986
6133
  maxAreaLights: budget.maxAreaLights,
5987
6134
  });
@@ -7877,7 +8024,12 @@ export class WebGL2Renderer implements RendererApi {
7877
8024
  * other position, and the anti-flicker's period never started.
7878
8025
  */
7879
8026
  if (this.framePresented) this.temporalJittering = false;
7880
- if (!this.reflectionPassActive && !this.probePassActive && !this.framePresented) {
8027
+ if (
8028
+ !this.reflectionPassActive &&
8029
+ !this.probePassActive &&
8030
+ !this.capturePassActive &&
8031
+ !this.framePresented
8032
+ ) {
7881
8033
  this.frameViewProj = camera.viewProjection;
7882
8034
  /* Copied rather than referenced: a caller moves its camera in place, and the decal pass
7883
8035
  reads this at the end of the frame. */
@@ -8599,6 +8751,7 @@ export class WebGL2Renderer implements RendererApi {
8599
8751
  !this.skinScattered &&
8600
8752
  !this.reflectionPassActive &&
8601
8753
  !this.probePassActive &&
8754
+ !this.capturePassActive &&
8602
8755
  !this.framePresented &&
8603
8756
  skin.ready(scene)
8604
8757
  );
@@ -8619,7 +8772,8 @@ export class WebGL2Renderer implements RendererApi {
8619
8772
  /* Latched whether or not skin was drawn: a skin drawn after this point is the whole surface. */
8620
8773
  this.skinScattered = true;
8621
8774
  if (!skin.pending) return;
8622
- if (this.reflectionPassActive || this.probePassActive || projection === null) return;
8775
+ if (this.reflectionPassActive || this.probePassActive || this.capturePassActive) return;
8776
+ if (projection === null) return;
8623
8777
  const depth = scene.snapshotDepth(true);
8624
8778
  mat4.invert(this.skinInverseProjection, projection);
8625
8779
  skin.spread(scene, depth, this.emptyTexture2D, projection, this.skinInverseProjection);