@driftengine/core 4.8.6 → 4.9.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/CHANGELOG.json +132 -0
- package/README.md +1 -1
- package/dist/index.d.ts +7 -2
- package/dist/index.js +8 -1
- package/dist/render/anisotropicLobe.d.ts +10 -0
- package/dist/render/anisotropicLobe.js +19 -0
- package/dist/render/areaLights.d.ts +35 -0
- package/dist/render/areaLights.js +35 -0
- package/dist/render/atmosphere.d.ts +13 -0
- package/dist/render/atmosphere.js +9 -1
- package/dist/render/backend/api.d.ts +41 -3
- package/dist/render/backend/createRenderer.d.ts +6 -0
- package/dist/render/backend/createRenderer.js +4 -1
- package/dist/render/backend/webgl2/renderer.d.ts +140 -16
- package/dist/render/backend/webgl2/renderer.js +436 -44
- package/dist/render/backend/webgl2/sceneCaptureTarget.d.ts +29 -0
- package/dist/render/backend/webgl2/sceneCaptureTarget.js +96 -0
- package/dist/render/backend/webgpu/device.d.ts +7 -1
- package/dist/render/backend/webgpu/device.js +31 -3
- package/dist/render/backend/webgpu/flatPass.d.ts +16 -1
- package/dist/render/backend/webgpu/flatPass.js +58 -26
- package/dist/render/backend/webgpu/particlePass.js +2 -1
- package/dist/render/backend/webgpu/postPass.d.ts +51 -9
- package/dist/render/backend/webgpu/postPass.js +16 -2
- package/dist/render/backend/webgpu/reflectionSurfacePass.d.ts +30 -0
- package/dist/render/backend/webgpu/reflectionSurfacePass.js +100 -0
- package/dist/render/backend/webgpu/renderer.d.ts +166 -3
- package/dist/render/backend/webgpu/renderer.js +738 -83
- package/dist/render/backend/webgpu/sceneCaptureTarget.d.ts +24 -0
- package/dist/render/backend/webgpu/sceneCaptureTarget.js +59 -0
- package/dist/render/backend/webgpu/shaders/clusterBin.wgsl.d.ts +1 -1
- package/dist/render/backend/webgpu/surfaceTexturePass.d.ts +4 -1
- package/dist/render/backend/webgpu/surfaceTexturePass.js +34 -8
- package/dist/render/backend/webgpu/waterPass.d.ts +1 -1
- package/dist/render/clusteredLights.d.ts +23 -5
- package/dist/render/clusteredLights.js +35 -8
- package/dist/render/displayRange.d.ts +31 -0
- package/dist/render/displayRange.js +47 -0
- package/dist/render/driftLight/bake.d.ts +2 -0
- package/dist/render/driftLight/bake.js +1 -1
- package/dist/render/driftLight/falloff.d.ts +4 -2
- package/dist/render/driftLight/falloff.js +8 -2
- package/dist/render/filmicCurve.d.ts +74 -0
- package/dist/render/filmicCurve.js +203 -0
- package/dist/render/fog.d.ts +4 -1
- package/dist/render/fog.js +12 -2
- package/dist/render/frame/resources.d.ts +2 -2
- package/dist/render/frame/resources.js +6 -0
- package/dist/render/frameReflections.d.ts +40 -0
- package/dist/render/frameReflections.js +48 -0
- package/dist/render/gi/bounceLights.d.ts +3 -2
- package/dist/render/gi/bounceLights.js +8 -3
- package/dist/render/instanceCull.js +5 -0
- package/dist/render/instances.d.ts +19 -5
- package/dist/render/instances.js +9 -3
- package/dist/render/lightBudget.d.ts +12 -1
- package/dist/render/lightBudget.js +23 -3
- package/dist/render/lightmap.d.ts +23 -9
- package/dist/render/lightmap.js +40 -18
- package/dist/render/particlePool.d.ts +21 -0
- package/dist/render/particlePool.js +7 -0
- package/dist/render/pass.d.ts +49 -0
- package/dist/render/pointLightSelection.d.ts +13 -0
- package/dist/render/pointLightSelection.js +3 -1
- package/dist/render/reflectionSurfaceTargets.d.ts +28 -0
- package/dist/render/reflectionSurfaceTargets.js +99 -0
- package/dist/render/renderQuality.d.ts +47 -15
- package/dist/render/renderQuality.js +4 -0
- package/dist/render/rendererHarness.js +3 -0
- package/dist/render/rgb9e5.d.ts +22 -0
- package/dist/render/rgb9e5.js +47 -0
- package/dist/render/sceneCapture.d.ts +29 -0
- package/dist/render/sceneCapture.js +36 -0
- package/dist/render/sceneTarget.d.ts +5 -0
- package/dist/render/sceneTarget.js +24 -1
- package/dist/render/screenSpaceReflection.d.ts +32 -1
- package/dist/render/screenSpaceReflection.js +14 -0
- package/dist/render/shaders/arcane.d.ts +1 -1
- package/dist/render/shaders/bolt.d.ts +1 -1
- package/dist/render/shaders/bolt.js +7 -1
- package/dist/render/shaders/caustics.d.ts +1 -1
- package/dist/render/shaders/depth.js +11 -0
- package/dist/render/shaders/film.d.ts +1 -1
- package/dist/render/shaders/filmLook.js +2 -1
- package/dist/render/shaders/fire.d.ts +1 -1
- package/dist/render/shaders/flat/anisotropic.d.ts +1 -1
- package/dist/render/shaders/flat/anisotropic.js +37 -3
- package/dist/render/shaders/flat/areaLight.d.ts +1 -1
- package/dist/render/shaders/flat/areaLight.js +46 -0
- package/dist/render/shaders/flat/glassTint.d.ts +3 -2
- package/dist/render/shaders/flat/glassTint.js +5 -1
- package/dist/render/shaders/flat/index.d.ts +15 -0
- package/dist/render/shaders/flat/index.js +22 -2
- package/dist/render/shaders/flat/lobes.d.ts +6 -0
- package/dist/render/shaders/flat/lobes.js +49 -43
- package/dist/render/shaders/flat/main.d.ts +1 -1
- package/dist/render/shaders/flat/main.js +85 -22
- package/dist/render/shaders/flat/models.d.ts +5 -2
- package/dist/render/shaders/flat/models.js +100 -16
- package/dist/render/shaders/flat/overlay.d.ts +16 -0
- package/dist/render/shaders/flat/overlay.js +117 -0
- package/dist/render/shaders/flat/pointShadow.js +1 -1
- package/dist/render/shaders/flat/preamble.js +10 -3
- package/dist/render/shaders/flat/reflectionSurface.d.ts +21 -0
- package/dist/render/shaders/flat/reflectionSurface.js +61 -0
- package/dist/render/shaders/fog.d.ts +1 -1
- package/dist/render/shaders/fog.js +9 -2
- package/dist/render/shaders/fringe.d.ts +29 -0
- package/dist/render/shaders/fringe.js +55 -0
- package/dist/render/shaders/generated/arcane.wgsl.d.ts +1 -1
- package/dist/render/shaders/generated/arcane.wgsl.js +1 -1
- package/dist/render/shaders/generated/bolt.wgsl.d.ts +1 -1
- package/dist/render/shaders/generated/bolt.wgsl.js +1 -1
- package/dist/render/shaders/generated/caustics.wgsl.d.ts +1 -1
- package/dist/render/shaders/generated/caustics.wgsl.js +1 -1
- package/dist/render/shaders/generated/depth.wgsl.d.ts +4 -4
- package/dist/render/shaders/generated/depth.wgsl.js +4 -4
- package/dist/render/shaders/generated/film.wgsl.d.ts +1 -1
- package/dist/render/shaders/generated/film.wgsl.js +1 -1
- package/dist/render/shaders/generated/fire.wgsl.d.ts +1 -1
- package/dist/render/shaders/generated/fire.wgsl.js +1 -1
- package/dist/render/shaders/generated/flat.wgsl.d.ts +192 -16
- package/dist/render/shaders/generated/flat.wgsl.js +230 -54
- package/dist/render/shaders/generated/line.wgsl.d.ts +1 -1
- package/dist/render/shaders/generated/line.wgsl.js +1 -1
- package/dist/render/shaders/generated/particle.wgsl.d.ts +4 -4
- package/dist/render/shaders/generated/particle.wgsl.js +4 -4
- package/dist/render/shaders/generated/rush.wgsl.d.ts +56 -26
- package/dist/render/shaders/generated/rush.wgsl.js +56 -26
- package/dist/render/shaders/generated/scatter.wgsl.d.ts +1 -1
- package/dist/render/shaders/generated/scatter.wgsl.js +1 -1
- package/dist/render/shaders/generated/smoke.wgsl.d.ts +1 -1
- package/dist/render/shaders/generated/smoke.wgsl.js +1 -1
- package/dist/render/shaders/generated/ssrResolve.wgsl.d.ts +29 -0
- package/dist/render/shaders/generated/ssrResolve.wgsl.js +29 -0
- package/dist/render/shaders/generated/ssrTrace.wgsl.d.ts +32 -7
- package/dist/render/shaders/generated/ssrTrace.wgsl.js +32 -7
- package/dist/render/shaders/generated/water.wgsl.d.ts +23 -16
- package/dist/render/shaders/generated/water.wgsl.js +23 -16
- package/dist/render/shaders/gi/probeBake.wgsl.js +9 -5
- package/dist/render/shaders/gpudriven/fog.wgsl.d.ts +2 -2
- package/dist/render/shaders/gpudriven/fog.wgsl.js +11 -3
- package/dist/render/shaders/line.d.ts +1 -1
- package/dist/render/shaders/particle.d.ts +3 -3
- package/dist/render/shaders/recon/motion.wgsl.d.ts +1 -1
- package/dist/render/shaders/recon/motion.wgsl.js +13 -2
- package/dist/render/shaders/rush.js +54 -11
- package/dist/render/shaders/scatter.d.ts +1 -1
- package/dist/render/shaders/smoke.d.ts +1 -1
- package/dist/render/shaders/ssrResolve.d.ts +17 -0
- package/dist/render/shaders/ssrResolve.js +51 -0
- package/dist/render/shaders/ssrTrace.js +46 -1
- package/dist/render/shaders/water.d.ts +1 -1
- package/dist/render/shaders/water.js +12 -3
- package/dist/render/skinScatter.d.ts +3 -1
- package/dist/render/skinScatter.js +3 -1
- package/dist/render/ssrPass.d.ts +16 -0
- package/dist/render/ssrPass.js +107 -1
- package/dist/render/surfaceModel.d.ts +29 -8
- package/dist/render/surfaceModel.js +33 -9
- package/dist/render/surfaceOverlay.d.ts +132 -0
- package/dist/render/surfaceOverlay.js +85 -0
- package/dist/render/surfaceTexture.d.ts +44 -1
- package/dist/render/surfaceTexture.js +96 -5
- package/dist/render/temporalAa.d.ts +14 -0
- package/dist/render/temporalAa.js +20 -0
- package/dist/render/vertexDefaults.d.ts +17 -0
- package/dist/render/vertexDefaults.js +24 -0
- package/package.json +6 -6
- package/src/index.ts +22 -2
- package/src/render/anisotropicLobe.ts +29 -0
- package/src/render/areaLights.ts +58 -0
- package/src/render/atmosphere.ts +22 -1
- package/src/render/backend/api.ts +58 -2
- package/src/render/backend/createRenderer.ts +18 -1
- package/src/render/backend/webgl2/renderer.ts +548 -50
- package/src/render/backend/webgl2/sceneCaptureTarget.ts +130 -0
- package/src/render/backend/webgpu/device.ts +42 -3
- package/src/render/backend/webgpu/flatPass.ts +63 -26
- package/src/render/backend/webgpu/particlePass.ts +2 -1
- package/src/render/backend/webgpu/postPass.ts +18 -0
- package/src/render/backend/webgpu/reflectionSurfacePass.ts +114 -0
- package/src/render/backend/webgpu/renderer.ts +927 -100
- package/src/render/backend/webgpu/sceneCaptureTarget.ts +63 -0
- package/src/render/backend/webgpu/surfaceTexturePass.ts +44 -14
- package/src/render/clusteredLights.ts +41 -8
- package/src/render/displayRange.ts +61 -0
- package/src/render/driftLight/bake.ts +3 -1
- package/src/render/driftLight/falloff.ts +13 -2
- package/src/render/filmicCurve.ts +229 -0
- package/src/render/fog.ts +19 -3
- package/src/render/frame/resources.ts +6 -0
- package/src/render/frameReflections.ts +72 -0
- package/src/render/gi/bounceLights.ts +12 -3
- package/src/render/instanceCull.ts +4 -0
- package/src/render/instances.ts +26 -6
- package/src/render/lightBudget.ts +40 -2
- package/src/render/lightmap.ts +64 -23
- package/src/render/particlePool.ts +23 -0
- package/src/render/pass.ts +53 -1
- package/src/render/pointLightSelection.ts +20 -1
- package/src/render/reflectionSurfaceTargets.ts +105 -0
- package/src/render/renderQuality.ts +52 -15
- package/src/render/rendererHarness.ts +3 -0
- package/src/render/rgb9e5.ts +52 -0
- package/src/render/sceneCapture.ts +56 -0
- package/src/render/sceneTarget.ts +30 -2
- package/src/render/screenSpaceReflection.ts +38 -1
- package/src/render/shaders/bolt.ts +7 -1
- package/src/render/shaders/depth.ts +11 -0
- package/src/render/shaders/filmLook.ts +2 -1
- package/src/render/shaders/flat/anisotropic.ts +37 -3
- package/src/render/shaders/flat/areaLight.ts +46 -0
- package/src/render/shaders/flat/glassTint.ts +13 -2
- package/src/render/shaders/flat/index.ts +37 -2
- package/src/render/shaders/flat/lobes.ts +50 -43
- package/src/render/shaders/flat/main.ts +85 -22
- package/src/render/shaders/flat/models.ts +100 -16
- package/src/render/shaders/flat/overlay.ts +117 -0
- package/src/render/shaders/flat/pointShadow.ts +1 -1
- package/src/render/shaders/flat/preamble.ts +10 -3
- package/src/render/shaders/flat/reflectionSurface.ts +61 -0
- package/src/render/shaders/fog.ts +9 -2
- package/src/render/shaders/fringe.ts +59 -0
- package/src/render/shaders/generated/arcane.wgsl.ts +1 -1
- package/src/render/shaders/generated/bolt.wgsl.ts +1 -1
- package/src/render/shaders/generated/caustics.wgsl.ts +1 -1
- package/src/render/shaders/generated/depth.wgsl.ts +4 -4
- package/src/render/shaders/generated/film.wgsl.ts +1 -1
- package/src/render/shaders/generated/fire.wgsl.ts +1 -1
- package/src/render/shaders/generated/flat.wgsl.ts +230 -54
- package/src/render/shaders/generated/line.wgsl.ts +1 -1
- package/src/render/shaders/generated/particle.wgsl.ts +4 -4
- package/src/render/shaders/generated/rush.wgsl.ts +56 -26
- package/src/render/shaders/generated/scatter.wgsl.ts +1 -1
- package/src/render/shaders/generated/smoke.wgsl.ts +1 -1
- package/src/render/shaders/generated/ssrResolve.wgsl.ts +30 -0
- package/src/render/shaders/generated/ssrTrace.wgsl.ts +32 -7
- package/src/render/shaders/generated/water.wgsl.ts +23 -16
- package/src/render/shaders/gi/probeBake.wgsl.ts +9 -5
- package/src/render/shaders/gpudriven/fog.wgsl.ts +11 -3
- package/src/render/shaders/recon/motion.wgsl.ts +13 -2
- package/src/render/shaders/rush.ts +54 -11
- package/src/render/shaders/ssrResolve.ts +52 -0
- package/src/render/shaders/ssrTrace.ts +46 -1
- package/src/render/shaders/water.ts +12 -3
- package/src/render/skinScatter.ts +3 -1
- package/src/render/ssrPass.ts +139 -1
- package/src/render/surfaceModel.ts +47 -10
- package/src/render/surfaceOverlay.ts +195 -0
- package/src/render/surfaceTexture.ts +136 -9
- package/src/render/temporalAa.ts +20 -0
- package/src/render/vertexDefaults.ts +28 -0
|
@@ -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
|
+
}
|
|
@@ -3,6 +3,8 @@ import { SURFACE_EFFECT_TEXELS, packSurfaceEffects } from '../../surfaceEffects.
|
|
|
3
3
|
import { toHalfFloats } from '../../halfFloat.ts';
|
|
4
4
|
import { isLightmapTexels } from '../../lightmap.ts';
|
|
5
5
|
import type { LightmapTexels } from '../../lightmap.ts';
|
|
6
|
+
import { isSceneCaptureTexels } from '../../sceneCapture.ts';
|
|
7
|
+
import type { SceneCaptureTexels } from '../../sceneCapture.ts';
|
|
6
8
|
import { layerSize, refuseArrayUpdate, sourceSize } from '../../textureSource.ts';
|
|
7
9
|
import { compressedLayers, planBlocks, refuseBlockUpdate } from '../../compressedSource.ts';
|
|
8
10
|
import type {
|
|
@@ -195,35 +197,54 @@ export class GpuSurfaceTexture {
|
|
|
195
197
|
constructor(
|
|
196
198
|
private readonly device: GPUDevice,
|
|
197
199
|
private readonly pipelines: PipelineCache,
|
|
198
|
-
source: SurfaceSource | readonly SurfaceSource[] | LightmapTexels,
|
|
200
|
+
source: SurfaceSource | readonly SurfaceSource[] | LightmapTexels | SceneCaptureTexels,
|
|
199
201
|
options: SurfaceTextureOptions = {},
|
|
200
202
|
compressed: readonly CompressedTextureFormat[] = [],
|
|
201
203
|
) {
|
|
202
|
-
/* A
|
|
204
|
+
/* A scene capture's target: one empty layer the world is drawn into. See `sceneCapture.ts`. */
|
|
205
|
+
const capture = isSceneCaptureTexels(source) ? source : null;
|
|
206
|
+
/* A lightmap's page: two layers of bytes, uploaded as they are. See `lightmap.ts`. */
|
|
203
207
|
const baked = isLightmapTexels(source) ? source : null;
|
|
204
208
|
const listed = (
|
|
205
|
-
baked !== null ? [] : Array.isArray(source) ? source : [source]
|
|
209
|
+
baked !== null || capture !== null ? [] : Array.isArray(source) ? source : [source]
|
|
206
210
|
) as readonly SurfaceSource[];
|
|
207
211
|
const blocks = compressedLayers(listed);
|
|
208
212
|
const srgb = (options.colorSpace ?? 'linear') === 'srgb';
|
|
209
213
|
/* `SRGB8_ALPHA8`'s equivalent. Decoded in the sampler, before filtering, which is the only
|
|
210
|
-
place it is correct — `surfaceTexture.ts` makes the argument in full.
|
|
211
|
-
|
|
212
|
-
|
|
214
|
+
place it is correct — `surfaceTexture.ts` makes the argument in full. A capture takes the
|
|
215
|
+
world's own format, which its pass resolves into. */
|
|
216
|
+
this.format =
|
|
217
|
+
capture !== null
|
|
218
|
+
? pipelines.format
|
|
219
|
+
: srgb && baked === null
|
|
220
|
+
? 'rgba8unorm-srgb'
|
|
221
|
+
: 'rgba8unorm';
|
|
222
|
+
if (capture !== null) {
|
|
223
|
+
this.layers = 1;
|
|
224
|
+
this.blockFormat = null;
|
|
225
|
+
this.mipmapped = false;
|
|
226
|
+
this.width = capture.width;
|
|
227
|
+
this.height = capture.height;
|
|
228
|
+
this.levels = 1;
|
|
229
|
+
this.texture = this.allocate();
|
|
230
|
+
} else if (baked !== null) {
|
|
213
231
|
/* No chain: a page is read at its own resolution, and a level of it would blend regions. */
|
|
214
|
-
this.layers = baked.layers;
|
|
232
|
+
this.layers = baked.layers.length;
|
|
215
233
|
this.blockFormat = null;
|
|
216
234
|
this.mipmapped = false;
|
|
217
235
|
this.width = baked.width;
|
|
218
236
|
this.height = baked.height;
|
|
219
237
|
this.levels = 1;
|
|
220
238
|
this.texture = this.allocate();
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
239
|
+
/* A layer at a time, so a page's arrays are uploaded where they lie rather than joined. */
|
|
240
|
+
for (let layer = 0; layer < this.layers; layer++) {
|
|
241
|
+
device.queue.writeTexture(
|
|
242
|
+
{ texture: this.texture, origin: [0, 0, layer] },
|
|
243
|
+
baked.layers[layer] as Uint8Array<ArrayBuffer>,
|
|
244
|
+
{ bytesPerRow: baked.width * 4, rowsPerImage: baked.height },
|
|
245
|
+
{ width: baked.width, height: baked.height, depthOrArrayLayers: 1 },
|
|
246
|
+
);
|
|
247
|
+
}
|
|
227
248
|
} else if (blocks !== null) {
|
|
228
249
|
/* Refused before anything is allocated; the stored chain, or level 0 alone where no chain
|
|
229
250
|
was asked for. See `compressedUpload.ts`. */
|
|
@@ -282,7 +303,9 @@ export class GpuSurfaceTexture {
|
|
|
282
303
|
: 1,
|
|
283
304
|
});
|
|
284
305
|
|
|
285
|
-
if (blocks === null && baked === null
|
|
306
|
+
if (blocks === null && baked === null && capture === null) {
|
|
307
|
+
this.upload(listed as readonly TexImageSource[]);
|
|
308
|
+
}
|
|
286
309
|
|
|
287
310
|
/* Half floats, as `renderer.ts` uploads it: filterable, so the ordinary float layout takes it,
|
|
288
311
|
and read by `textureLoad` alone, so the filter never runs. */
|
|
@@ -315,6 +338,13 @@ export class GpuSurfaceTexture {
|
|
|
315
338
|
return this.current;
|
|
316
339
|
}
|
|
317
340
|
|
|
341
|
+
/** The only layer as a plain 2D view, which a scene capture's pass resolves into. */
|
|
342
|
+
layerView(): GPUTextureView | null {
|
|
343
|
+
return (
|
|
344
|
+
this.texture?.createView({ dimension: '2d', baseArrayLayer: 0, arrayLayerCount: 1 }) ?? null
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
|
|
318
348
|
/**
|
|
319
349
|
* Replace the pixels, keeping the handle and its sampler.
|
|
320
350
|
*
|
|
@@ -92,7 +92,7 @@ export const MAX_CLUSTERED_LIGHTS = 320;
|
|
|
92
92
|
* it cannot. That is what lets a light index the shadow uniforms by a number read from a texture,
|
|
93
93
|
* and it is the same fact the octahedral array texture was built on.
|
|
94
94
|
*/
|
|
95
|
-
export const LIGHT_TEXELS =
|
|
95
|
+
export const LIGHT_TEXELS = 6;
|
|
96
96
|
|
|
97
97
|
/**
|
|
98
98
|
* Where each field of a light's record sits, as an offset in `uint`s from `lightBase`.
|
|
@@ -123,7 +123,11 @@ export const LIGHT_TEXELS = 5;
|
|
|
123
123
|
* **Reordered 2026-10-02 so a plain light's fields are the first eight**, which is what lets the
|
|
124
124
|
* shader stop after two texels. Nine values are needed by every light and eight fit in two
|
|
125
125
|
* texels, so the emitter size and the weight share slot 7 as two halves; see `packSizeAndWeight`.
|
|
126
|
-
*
|
|
126
|
+
*
|
|
127
|
+
* **A sixth texel arrived in 4.8.7 for a light's own falloff exponent**, the fifth's last slot
|
|
128
|
+
* having gone to the channels: 6 KB more of a table near 300, read only for a light whose record
|
|
129
|
+
* carries a fixture, which a light with an exponent does. Slots 21 and 22 are a rectangle's barn
|
|
130
|
+
* doors and spare on a lamp; slot 23 is spare.
|
|
127
131
|
*/
|
|
128
132
|
export const LIGHT_RECORD = {
|
|
129
133
|
positionX: 0,
|
|
@@ -154,6 +158,11 @@ export const LIGHT_RECORD = {
|
|
|
154
158
|
* last texels are fetched; a plain light's are not, and the shader takes the default for it.
|
|
155
159
|
*/
|
|
156
160
|
channels: 19,
|
|
161
|
+
/** The light's own falloff exponent, or 0 for the frame's falloff. `PointLightSource.falloffExponent`. */
|
|
162
|
+
falloffExponent: 20,
|
|
163
|
+
/** A rectangle's barn doors: the cosine of their angle from its normal, then their length in metres. */
|
|
164
|
+
barnDoorCos: 21,
|
|
165
|
+
barnDoorLength: 22,
|
|
157
166
|
} as const;
|
|
158
167
|
|
|
159
168
|
/** The lighting channel every light and every surface is on unless it says otherwise. */
|
|
@@ -171,11 +180,20 @@ export function lightChannelsOf(value: number | undefined): number {
|
|
|
171
180
|
return value;
|
|
172
181
|
}
|
|
173
182
|
|
|
183
|
+
/**
|
|
184
|
+
* A light's own falloff exponent as the shader reads it: a positive finite number, or 0 for the
|
|
185
|
+
* frame's falloff where none was named or what was named is not one. The record and the uniform
|
|
186
|
+
* path both write this, so a light named badly falls off by the frame's rule in both.
|
|
187
|
+
*/
|
|
188
|
+
export function lightFalloffExponentOf(value: number | undefined): number {
|
|
189
|
+
return value !== undefined && Number.isFinite(value) && value > 0 ? value : 0;
|
|
190
|
+
}
|
|
191
|
+
|
|
174
192
|
/** Texels the lit pass reads for a light whose record carries no fixture. */
|
|
175
193
|
export const PLAIN_LIGHT_TEXELS = 2;
|
|
176
194
|
|
|
177
195
|
/**
|
|
178
|
-
* The bit of `sizeAndWeight` that says the record's last
|
|
196
|
+
* The bit of `sizeAndWeight` that says the record's last four texels hold something: the sign of
|
|
179
197
|
* the emitter size's half, which a size never needs.
|
|
180
198
|
*/
|
|
181
199
|
export const LIGHT_FIXTURE_FLAG = 0x8000;
|
|
@@ -426,6 +444,8 @@ export interface ClusterLightSet {
|
|
|
426
444
|
readonly iesProfiles?: Float32Array;
|
|
427
445
|
/** One per light: its lighting channels. See `PointLightSet.lightChannels`. */
|
|
428
446
|
readonly channels?: Float32Array;
|
|
447
|
+
/** One per light: its own falloff exponent, or 0. See `PointLightSource.falloffExponent`. */
|
|
448
|
+
readonly falloffExponents?: Float32Array;
|
|
429
449
|
/**
|
|
430
450
|
* The rectangles shaded through the table, after the `count` point lights: those of `areas` from
|
|
431
451
|
* `areaFrom` on, `areaCount` of them. The ones before `areaFrom` are the fixed arm's, which is
|
|
@@ -450,6 +470,7 @@ export interface ClusterLightArrays {
|
|
|
450
470
|
readonly lightIesAxes?: Float32Array;
|
|
451
471
|
readonly lightCookies?: Float32Array;
|
|
452
472
|
readonly lightChannels?: Float32Array;
|
|
473
|
+
readonly lightFalloffExponents?: Float32Array;
|
|
453
474
|
readonly areaLights?: AreaLightBuffer | null;
|
|
454
475
|
}
|
|
455
476
|
|
|
@@ -510,6 +531,7 @@ export function fillClusterLightSet(
|
|
|
510
531
|
out.iesAxes = env.lightIesAxes;
|
|
511
532
|
out.cookies = env.lightCookies;
|
|
512
533
|
out.channels = env.lightChannels;
|
|
534
|
+
out.falloffExponents = env.lightFalloffExponents;
|
|
513
535
|
return out;
|
|
514
536
|
}
|
|
515
537
|
|
|
@@ -559,8 +581,8 @@ function distanceSqToBounds(x: number, y: number, z: number, b: Float32Array): n
|
|
|
559
581
|
}
|
|
560
582
|
|
|
561
583
|
/**
|
|
562
|
-
* Whether a light's record needs its last
|
|
563
|
-
* or a
|
|
584
|
+
* Whether a light's record needs its last four texels: it holds a shadow slot, a cone, a profile,
|
|
585
|
+
* a cookie, channels of its own or a falloff exponent of its own. Everything else is a plain light, read in `PLAIN_LIGHT_TEXELS`.
|
|
564
586
|
*
|
|
565
587
|
* **`shadowSlots` is zero where the lit shader has no point shadows**, and both binners are handed
|
|
566
588
|
* it that way: a slot is only read under `POINT_SHADOWS`, so a light holding one in a build without
|
|
@@ -580,7 +602,8 @@ export function lightHasFixture(
|
|
|
580
602
|
(cone?.[light * 2 + 1] ?? POINT_LIGHT_COS_OUTER) !== POINT_LIGHT_COS_OUTER ||
|
|
581
603
|
(lights.iesProfiles?.[light] ?? NO_IES_PROFILE) >= 0 ||
|
|
582
604
|
(lights.cookies?.[light] ?? NO_IES_PROFILE) >= 0 ||
|
|
583
|
-
lightChannelsOf(lights.channels?.[light]) !== DEFAULT_LIGHT_CHANNELS
|
|
605
|
+
lightChannelsOf(lights.channels?.[light]) !== DEFAULT_LIGHT_CHANNELS ||
|
|
606
|
+
lightFalloffExponentOf(lights.falloffExponents?.[light]) > 0
|
|
584
607
|
);
|
|
585
608
|
}
|
|
586
609
|
|
|
@@ -665,6 +688,12 @@ export function writeLightRecord(
|
|
|
665
688
|
out[at + LIGHT_RECORD.iesAxisY] = floatBits(axes?.[light * 3 + 1] ?? 0);
|
|
666
689
|
out[at + LIGHT_RECORD.iesAxisZ] = floatBits(axes?.[light * 3 + 2] ?? 0);
|
|
667
690
|
out[at + LIGHT_RECORD.channels] = floatBits(lightChannelsOf(lights.channels?.[light]));
|
|
691
|
+
out[at + LIGHT_RECORD.falloffExponent] = floatBits(
|
|
692
|
+
lightFalloffExponentOf(lights.falloffExponents?.[light]),
|
|
693
|
+
);
|
|
694
|
+
out[at + LIGHT_RECORD.barnDoorCos] = 0;
|
|
695
|
+
out[at + LIGHT_RECORD.barnDoorLength] = 0;
|
|
696
|
+
out[at + LIGHT_RECORD.barnDoorLength + 1] = 0;
|
|
668
697
|
}
|
|
669
698
|
|
|
670
699
|
/**
|
|
@@ -677,9 +706,9 @@ function areaSlot(lights: ClusterLightSet, light: number): number {
|
|
|
677
706
|
|
|
678
707
|
/**
|
|
679
708
|
* A rectangle's record, which the shader tells from a lamp's by its **negative radius** — its
|
|
680
|
-
* reach, negated — and reads to its
|
|
709
|
+
* reach, negated — and reads to its sixth texel: sidedness where a lamp keeps its shadow slot,
|
|
681
710
|
* the right axis where a spot keeps its direction, the half extents where its cone is, the up
|
|
682
|
-
* axis where the azimuth is, and the
|
|
711
|
+
* axis where the azimuth is, the channels, and the barn doors in the sixth. Never shadowed: a rectangle's occlusion is
|
|
683
712
|
* the fixed arm's, which is why the first `maxAreaLights` stay there.
|
|
684
713
|
*/
|
|
685
714
|
function writeAreaRecord(
|
|
@@ -715,6 +744,10 @@ function writeAreaRecord(
|
|
|
715
744
|
out[at + LIGHT_RECORD.iesAxisY] = floatBits(up?.[a * 3 + 1] ?? 1);
|
|
716
745
|
out[at + LIGHT_RECORD.iesAxisZ] = floatBits(up?.[a * 3 + 2] ?? 0);
|
|
717
746
|
out[at + LIGHT_RECORD.channels] = floatBits(DEFAULT_LIGHT_CHANNELS);
|
|
747
|
+
out[at + LIGHT_RECORD.falloffExponent] = 0;
|
|
748
|
+
out[at + LIGHT_RECORD.barnDoorCos] = floatBits(areas?.barnDoors?.[a * 2] ?? 0);
|
|
749
|
+
out[at + LIGHT_RECORD.barnDoorLength] = floatBits(areas?.barnDoors?.[a * 2 + 1] ?? 0);
|
|
750
|
+
out[at + LIGHT_RECORD.barnDoorLength + 1] = 0;
|
|
718
751
|
}
|
|
719
752
|
|
|
720
753
|
/** A clustered rectangle's reach, which its record carries negated. See `AreaLightSource.range`. */
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which dynamic range the frame goes out in, and why: the one decision behind
|
|
3
|
+
* `RenderQuality.highDynamicRange`, made here so both backends answer it in the same words.
|
|
4
|
+
*
|
|
5
|
+
* **A high range is asked for, found, and held, or it is not had.** Three things must all be true:
|
|
6
|
+
* the profile asked; the frame has a composite holding light past white (`screenEffects` and
|
|
7
|
+
* `hdrScene`), since every other pass writes display values already clipped; and the display says
|
|
8
|
+
* it can show more than paper white. The backend then has to be able to configure such a canvas,
|
|
9
|
+
* which only it knows, and it may still answer standard after this says high.
|
|
10
|
+
*
|
|
11
|
+
* **What a high range gives up** is nothing a standard frame had: paper white stays where it is,
|
|
12
|
+
* and only what was above it — a lamp, a flash, a sunlit edge — goes on rising to the display's
|
|
13
|
+
* peak instead of rolling off at white. **What would make it wrong** is a display that reports a
|
|
14
|
+
* high range it cannot show, which a page cannot detect and the platform owns.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
export type DisplayRange = 'high' | 'standard';
|
|
18
|
+
|
|
19
|
+
export interface DisplayRangeChoice {
|
|
20
|
+
readonly range: DisplayRange;
|
|
21
|
+
/** A sentence saying why, for `RendererApi.displayRangeReason`. */
|
|
22
|
+
readonly reason: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** The display's own answer, as the browser gives it. A host without a DOM supplies its own. */
|
|
26
|
+
export function browserHighDynamicRange(): boolean {
|
|
27
|
+
// platform: browser default — `CreateRendererOptions.highDynamicRangeDisplay` replaces it
|
|
28
|
+
return typeof matchMedia === 'function' && matchMedia('(dynamic-range: high)').matches;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** The range a profile gets on a display, before the backend has tried to configure it. */
|
|
32
|
+
export function chooseDisplayRange(
|
|
33
|
+
asked: boolean,
|
|
34
|
+
composite: boolean,
|
|
35
|
+
displayIsHigh: boolean,
|
|
36
|
+
): DisplayRangeChoice {
|
|
37
|
+
if (!asked) return { range: 'standard', reason: 'highDynamicRange was not asked for' };
|
|
38
|
+
if (!composite) {
|
|
39
|
+
return {
|
|
40
|
+
range: 'standard',
|
|
41
|
+
reason:
|
|
42
|
+
'highDynamicRange needs screenEffects and hdrScene: without a composite holding light ' +
|
|
43
|
+
'past white, every pass writes values already clipped to it',
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
if (!displayIsHigh) {
|
|
47
|
+
return { range: 'standard', reason: 'the display reports a standard dynamic range' };
|
|
48
|
+
}
|
|
49
|
+
return { range: 'high', reason: 'an extended-range canvas on a high dynamic range display' };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The display's peak over the frame's paper white, as `setDisplayLuminance` is handed it: at
|
|
54
|
+
* least 1, and 1 where the range is standard or a number is not one. Capped at 64, a thousand nits
|
|
55
|
+
* over a sixteen-nit white, which no display reaches and no curve should be asked to.
|
|
56
|
+
*/
|
|
57
|
+
export function displayHeadroom(range: DisplayRange, paperWhite: number, peak: number): number {
|
|
58
|
+
if (range !== 'high') return 1;
|
|
59
|
+
const ratio = peak / paperWhite;
|
|
60
|
+
return Number.isFinite(ratio) ? Math.min(Math.max(ratio, 1), 64) : 1;
|
|
61
|
+
}
|
|
@@ -32,6 +32,8 @@ export interface FieldLight extends LightReach {
|
|
|
32
32
|
readonly b: number;
|
|
33
33
|
/** The emitter's physical radius, which widens its penumbra. */
|
|
34
34
|
readonly sourceRadius: number;
|
|
35
|
+
/** Its own falloff exponent, overriding the frame's above zero. See `pointLightShape`. */
|
|
36
|
+
readonly falloffExponent?: number;
|
|
35
37
|
}
|
|
36
38
|
|
|
37
39
|
/**
|
|
@@ -115,7 +117,7 @@ export function sampleFieldLight(
|
|
|
115
117
|
const dz = source.z - pz;
|
|
116
118
|
const dist = Math.hypot(dx, dy, dz);
|
|
117
119
|
if (dist >= source.radius) continue;
|
|
118
|
-
const shape = pointLightShape(dist, source.radius, falloff);
|
|
120
|
+
const shape = pointLightShape(dist, source.radius, falloff, source.falloffExponent);
|
|
119
121
|
if (shape <= 0) continue;
|
|
120
122
|
const seen =
|
|
121
123
|
distance === null
|
|
@@ -9,13 +9,24 @@
|
|
|
9
9
|
*
|
|
10
10
|
* Both of the renderer's falloffs: `smooth`, the default, is a linear ramp squared, reaching zero at
|
|
11
11
|
* the radius; `inverseSquare` is physical, windowed so it still reaches zero at the radius, and
|
|
12
|
-
* floored at ten centimetres so a surface touching a light is not infinitely bright.
|
|
12
|
+
* floored at ten centimetres so a surface touching a light is not infinitely bright. And a light's
|
|
13
|
+
* own, which overrides both: `(1 - (d/R)^2)^n` with no distance term, for an exponent `n` above zero
|
|
14
|
+
* (`PointLightSource.falloffExponent`).
|
|
13
15
|
*/
|
|
14
16
|
|
|
15
17
|
export type PointLightFalloff = 'smooth' | 'inverseSquare';
|
|
16
18
|
|
|
17
19
|
/** The shader's `shape` for an unshaped point light: no cone, no photometric profile. */
|
|
18
|
-
export function pointLightShape(
|
|
20
|
+
export function pointLightShape(
|
|
21
|
+
dist: number,
|
|
22
|
+
radius: number,
|
|
23
|
+
falloff: PointLightFalloff,
|
|
24
|
+
exponent = 0,
|
|
25
|
+
): number {
|
|
26
|
+
if (exponent > 0) {
|
|
27
|
+
const reach = dist / Math.max(radius, 1e-4);
|
|
28
|
+
return Math.min(1, Math.max(0, 1 - reach * reach)) ** exponent;
|
|
29
|
+
}
|
|
19
30
|
if (falloff === 'inverseSquare') {
|
|
20
31
|
const ratio = dist / Math.max(radius, 1e-4);
|
|
21
32
|
const window = Math.min(1, Math.max(0, 1 - ratio ** 4));
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `filmic` output transform: a parametric film curve, its five numbers, and the eight
|
|
3
|
+
* constants the resolve reads, decided once here for both backends.
|
|
4
|
+
*
|
|
5
|
+
* **Why a second filmic curve beside `aces`.** `aces` is the reference RRT and ODT fit, which
|
|
6
|
+
* holds a mid grey of 0.18 at about 0.11 of display linear: graded for a dark cinema and kept dark.
|
|
7
|
+
* The curve here holds 0.18 at 0.18 by construction, and is shaped by five numbers a grading
|
|
8
|
+
* volume carries — a slope through mid grey, a toe and a shoulder, and how far black and white are
|
|
9
|
+
* clipped past the ends — so a look authored against such a curve is matched by passing its numbers
|
|
10
|
+
* rather than by baking the difference into a colour lookup table, which has the fewest steps
|
|
11
|
+
* exactly where two curves differ most: in the shadows.
|
|
12
|
+
*
|
|
13
|
+
* **What the curve is.** In the ACEScg working space, after the ACES reference's glow and red
|
|
14
|
+
* modifier and a 4% desaturation: a straight line of `slope` through mid grey in log10, joined to a
|
|
15
|
+
* logistic toe below `toeMatch` and a logistic shoulder above `shoulderMatch`, blended by a smooth
|
|
16
|
+
* step between the two, then desaturated by 7% and taken back to linear sRGB. The toe's position is
|
|
17
|
+
* solved so that 0.18 in is 0.18 out.
|
|
18
|
+
*
|
|
19
|
+
* **What it gives up.** It needs the composite to grade: `screenEffects` and `hdrScene`, since the
|
|
20
|
+
* forward passes that grade a frame without one carry two uniforms between them and not eight.
|
|
21
|
+
* Without them every pass grades with `aces` and the renderer says so once at construction.
|
|
22
|
+
* **What would make it wrong** is a profile wanting this curve without a scene target, which would
|
|
23
|
+
* then pay six floats in every forward program to have it.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/** A film curve's five numbers, as a grading volume states them. */
|
|
27
|
+
export interface FilmicCurve {
|
|
28
|
+
/** The straight segment's slope in log10, through mid grey. Positive. */
|
|
29
|
+
readonly slope: number;
|
|
30
|
+
/** How much of the curve below mid grey the toe takes, 0 to 1. Larger is a longer, softer toe. */
|
|
31
|
+
readonly toe: number;
|
|
32
|
+
/** How much of the curve above mid grey the shoulder takes, 0 to 1. Larger rolls off sooner. */
|
|
33
|
+
readonly shoulder: number;
|
|
34
|
+
/** How far below 0 the toe's asymptote sits: 0 holds black at black, more crushes it. */
|
|
35
|
+
readonly blackClip: number;
|
|
36
|
+
/** How far above 1 the shoulder's asymptote sits: 0 never reaches white, more lets it clip. */
|
|
37
|
+
readonly whiteClip: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** The defaults the curve ships with where a volume states nothing. */
|
|
41
|
+
export const DEFAULT_FILMIC_CURVE: FilmicCurve = {
|
|
42
|
+
slope: 0.88,
|
|
43
|
+
toe: 0.55,
|
|
44
|
+
shoulder: 0.26,
|
|
45
|
+
blackClip: 0,
|
|
46
|
+
whiteClip: 0.04,
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/** How many floats `resolveFilmicCurve` fills: two vectors of the resolve's uniforms. */
|
|
50
|
+
export const FILMIC_CONSTANTS = 8;
|
|
51
|
+
|
|
52
|
+
/** Mid grey, which the curve takes to itself. */
|
|
53
|
+
const MID_GREY = 0.18;
|
|
54
|
+
|
|
55
|
+
/** The largest toe or shoulder kept: at 1 the toe's or the shoulder's scale is zero. */
|
|
56
|
+
const MOST = 0.999;
|
|
57
|
+
/** The shallowest slope kept, which is still a curve rather than a flat line. */
|
|
58
|
+
const LEAST_SLOPE = 0.01;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The eight constants the resolve reads, into `out`: `uFilmA` = (slope, black clip, white clip,
|
|
62
|
+
* toe scale) and `uFilmB` = (shoulder scale, toe match, straight match, shoulder match). Returns
|
|
63
|
+
* whether any of the five numbers had to be brought into range.
|
|
64
|
+
*
|
|
65
|
+
* Here rather than in the shader, because they are one solve a curve and would be one per pixel
|
|
66
|
+
* there. **Clamped rather than refused**, because a grading volume is set from inside a frame and
|
|
67
|
+
* the frame loop never throws: a slope below 0.01, a toe, shoulder or clip outside 0 to 1, and a
|
|
68
|
+
* toe or shoulder of exactly 1 — whose scale is then zero and divides the curve by it — are each
|
|
69
|
+
* a picture made of NaN otherwise. A number that is not finite takes the default's. The renderer
|
|
70
|
+
* says once when anything was clamped.
|
|
71
|
+
*/
|
|
72
|
+
export function resolveFilmicCurve(
|
|
73
|
+
curve: FilmicCurve,
|
|
74
|
+
out: Float32Array,
|
|
75
|
+
/**
|
|
76
|
+
* The display's peak over paper white (`displayHeadroom`), 1 for a standard range. Above 1 the
|
|
77
|
+
* white clip is raised by the difference, so the shoulder rolls off toward the display's peak
|
|
78
|
+
* rather than toward white, and the toe and mid grey stay exactly where they were.
|
|
79
|
+
*/
|
|
80
|
+
headroom = 1,
|
|
81
|
+
): boolean {
|
|
82
|
+
let clamped = false;
|
|
83
|
+
const within = (value: number, low: number, high: number, fallback: number): number => {
|
|
84
|
+
const finite = Number.isFinite(value) ? value : fallback;
|
|
85
|
+
const kept = Math.min(Math.max(finite, low), high);
|
|
86
|
+
if (kept !== value) clamped = true;
|
|
87
|
+
return kept;
|
|
88
|
+
};
|
|
89
|
+
const slope = within(curve.slope, LEAST_SLOPE, Number.MAX_VALUE, DEFAULT_FILMIC_CURVE.slope);
|
|
90
|
+
const toe = within(curve.toe, 0, MOST, DEFAULT_FILMIC_CURVE.toe);
|
|
91
|
+
const shoulder = within(curve.shoulder, 0, MOST, DEFAULT_FILMIC_CURVE.shoulder);
|
|
92
|
+
const blackClip = within(curve.blackClip, 0, 1, DEFAULT_FILMIC_CURVE.blackClip);
|
|
93
|
+
const whiteClip =
|
|
94
|
+
within(curve.whiteClip, 0, 1, DEFAULT_FILMIC_CURVE.whiteClip) + Math.max(headroom, 1) - 1;
|
|
95
|
+
const toeScale = 1 + blackClip - toe;
|
|
96
|
+
const shoulderScale = 1 + whiteClip - shoulder;
|
|
97
|
+
let toeMatch: number;
|
|
98
|
+
if (toe > 0.8) {
|
|
99
|
+
/* Mid grey falls on the straight segment. */
|
|
100
|
+
toeMatch = (1 - toe - MID_GREY) / slope + Math.log10(MID_GREY);
|
|
101
|
+
} else {
|
|
102
|
+
/* Mid grey falls on the toe: solve the toe's logistic for 0.18 in, 0.18 out. */
|
|
103
|
+
const bt = (MID_GREY + blackClip) / toeScale - 1;
|
|
104
|
+
toeMatch = Math.log10(MID_GREY) - 0.5 * Math.log((1 + bt) / (1 - bt)) * (toeScale / slope);
|
|
105
|
+
}
|
|
106
|
+
const straightMatch = (1 - toe) / slope - toeMatch;
|
|
107
|
+
const shoulderMatch = shoulder / slope - straightMatch;
|
|
108
|
+
out[0] = slope;
|
|
109
|
+
out[1] = blackClip;
|
|
110
|
+
out[2] = whiteClip;
|
|
111
|
+
out[3] = toeScale;
|
|
112
|
+
out[4] = shoulderScale;
|
|
113
|
+
out[5] = toeMatch;
|
|
114
|
+
out[6] = straightMatch;
|
|
115
|
+
out[7] = shoulderMatch;
|
|
116
|
+
return clamped;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Said once a renderer, the first time `setFilmicCurve` was handed a number out of range. */
|
|
120
|
+
export const FILMIC_CLAMPED =
|
|
121
|
+
'[driftengine] setFilmicCurve was handed a number outside its range — a slope under 0.01, a ' +
|
|
122
|
+
'toe, shoulder or clip outside 0 to 1, or one that is not a number — and drew with it clamped.';
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Said once at construction where `filmic` was asked for and the frame has no composite that
|
|
126
|
+
* grades: the passes grade with `aces`, which is the nearest curve they carry.
|
|
127
|
+
*/
|
|
128
|
+
export const FILMIC_WITHOUT_COMPOSITE =
|
|
129
|
+
"[driftengine] outputTransform 'filmic' is applied by the composite, which grades only with " +
|
|
130
|
+
"`screenEffects` and `hdrScene` both on: without them every pass grades itself, with 'aces'.";
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* `filmicCurve(c)`: linear sRGB in, display-linear sRGB out, clamped to 0 to 1 — or, on a high
|
|
134
|
+
* range display, to the shoulder's own asymptote. Reads `uFilmA`, `uFilmB` and `uDisplayHeadroom`,
|
|
135
|
+
* which the including stage declares. The ACES helpers are the reference's own:
|
|
136
|
+
* saturation, `yc`, the glow's sigmoid, and hue in degrees.
|
|
137
|
+
*/
|
|
138
|
+
export const FILMIC_GLSL = `
|
|
139
|
+
/* sRGB to ACEScg (through D60), ACEScg back, and ACEScg to and from ACES2065-1. Columns. */
|
|
140
|
+
const mat3 FILM_SRGB_TO_AP1 = mat3(
|
|
141
|
+
0.61319, 0.07021, 0.02062,
|
|
142
|
+
0.33951, 0.91634, 0.10957,
|
|
143
|
+
0.04737, 0.01345, 0.86961
|
|
144
|
+
);
|
|
145
|
+
/* The inverse of the one above, computed rather than copied, so a round trip is the identity. */
|
|
146
|
+
const mat3 FILM_AP1_TO_SRGB = mat3(
|
|
147
|
+
1.70480, -0.13027, -0.02401,
|
|
148
|
+
-0.62168, 1.14082, -0.12900,
|
|
149
|
+
-0.08325, -0.01055, 1.15324
|
|
150
|
+
);
|
|
151
|
+
const mat3 FILM_AP1_TO_AP0 = mat3(
|
|
152
|
+
0.6954522414, 0.0447945634, -0.0055258826,
|
|
153
|
+
0.1406786965, 0.8596711185, 0.0040252103,
|
|
154
|
+
0.1638690622, 0.0955343182, 1.0015006723
|
|
155
|
+
);
|
|
156
|
+
const mat3 FILM_AP0_TO_AP1 = mat3(
|
|
157
|
+
1.4514393161, -0.0765537734, 0.0083161484,
|
|
158
|
+
-0.2365107469, 1.1762296998, -0.0060324498,
|
|
159
|
+
-0.2149285693, -0.0996759264, 0.9977163014
|
|
160
|
+
);
|
|
161
|
+
const vec3 FILM_AP1_LUMA = vec3(0.2722287168, 0.6740817658, 0.0536895174);
|
|
162
|
+
|
|
163
|
+
float filmSaturation(vec3 c) {
|
|
164
|
+
float hi = max(c.r, max(c.g, c.b));
|
|
165
|
+
float lo = min(c.r, min(c.g, c.b));
|
|
166
|
+
return (max(hi, 1e-10) - max(lo, 1e-10)) / max(hi, 1e-2);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
float filmYc(vec3 c) {
|
|
170
|
+
float chroma = sqrt(max(c.b * (c.b - c.g) + c.g * (c.g - c.r) + c.r * (c.r - c.b), 0.0));
|
|
171
|
+
return (c.b + c.g + c.r + 1.75 * chroma) / 3.0;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
float filmSigmoid(float x) {
|
|
175
|
+
float t = max(1.0 - abs(x / 2.0), 0.0);
|
|
176
|
+
return (1.0 + sign(x) * (1.0 - t * t)) / 2.0;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
float filmGlow(float yc, float gain, float mid) {
|
|
180
|
+
if (yc <= 2.0 / 3.0 * mid) return gain;
|
|
181
|
+
if (yc >= 2.0 * mid) return 0.0;
|
|
182
|
+
return gain * (mid / yc - 0.5);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/* Hue in degrees, -180 to 180 about red; 0 for a grey, which then takes no red modifier. */
|
|
186
|
+
float filmCentredHue(vec3 c) {
|
|
187
|
+
float y = sqrt(3.0) * (c.g - c.b);
|
|
188
|
+
float x = 2.0 * c.r - c.g - c.b;
|
|
189
|
+
if (abs(x) < 1e-10 && abs(y) < 1e-10) return 0.0;
|
|
190
|
+
return degrees(atan(y, x));
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
vec3 filmicCurve(vec3 linear) {
|
|
194
|
+
vec3 ap0 = FILM_AP1_TO_AP0 * (FILM_SRGB_TO_AP1 * linear);
|
|
195
|
+
/* The reference's glow, lifting dark saturated colours a little. */
|
|
196
|
+
float saturation = filmSaturation(ap0);
|
|
197
|
+
ap0 *= 1.0 + filmGlow(filmYc(ap0), 0.05 * filmSigmoid((saturation - 0.4) / 0.2), 0.08);
|
|
198
|
+
/* And its red modifier, pulling saturated reds toward a pivot so they do not run to orange. */
|
|
199
|
+
float weight = smoothstep(0.0, 1.0, 1.0 - abs(2.0 * filmCentredHue(ap0) / 135.0));
|
|
200
|
+
ap0.r += weight * weight * saturation * (0.03 - ap0.r) * (1.0 - 0.82);
|
|
201
|
+
vec3 work = max(FILM_AP0_TO_AP1 * ap0, vec3(0.0));
|
|
202
|
+
work = mix(vec3(dot(work, FILM_AP1_LUMA)), work, 0.96);
|
|
203
|
+
|
|
204
|
+
float slope = uFilmA.x;
|
|
205
|
+
float black = uFilmA.y;
|
|
206
|
+
float white = uFilmA.z;
|
|
207
|
+
float toeScale = uFilmA.w;
|
|
208
|
+
float shoulderScale = uFilmB.x;
|
|
209
|
+
float toeMatch = uFilmB.y;
|
|
210
|
+
float straightMatch = uFilmB.z;
|
|
211
|
+
float shoulderMatch = uFilmB.w;
|
|
212
|
+
/* log10, through log2: GLSL ES has no log10. */
|
|
213
|
+
vec3 logc = log2(max(work, vec3(1e-10))) * 0.30102999566;
|
|
214
|
+
vec3 straight = slope * (logc + straightMatch);
|
|
215
|
+
vec3 toeC = -black + (2.0 * toeScale) / (1.0 + exp((-2.0 * slope / toeScale) * (logc - toeMatch)));
|
|
216
|
+
vec3 shoulderC =
|
|
217
|
+
(1.0 + white) - (2.0 * shoulderScale) / (1.0 + exp((2.0 * slope / shoulderScale) * (logc - shoulderMatch)));
|
|
218
|
+
toeC = mix(straight, toeC, vec3(lessThan(logc, vec3(toeMatch))));
|
|
219
|
+
shoulderC = mix(straight, shoulderC, vec3(greaterThan(logc, vec3(shoulderMatch))));
|
|
220
|
+
vec3 t = clamp((logc - toeMatch) / (shoulderMatch - toeMatch), 0.0, 1.0);
|
|
221
|
+
if (shoulderMatch < toeMatch) t = 1.0 - t;
|
|
222
|
+
t = (3.0 - 2.0 * t) * t * t;
|
|
223
|
+
vec3 tone = mix(toeC, shoulderC, t);
|
|
224
|
+
tone = mix(vec3(dot(tone, FILM_AP1_LUMA)), tone, 0.93);
|
|
225
|
+
/* White is the ceiling on a standard display; on a high range one the shoulder's asymptote is. */
|
|
226
|
+
float ceiling = uDisplayHeadroom > 1.0 ? 1.0 + white : 1.0;
|
|
227
|
+
return clamp(FILM_AP1_TO_SRGB * max(tone, vec3(0.0)), 0.0, ceiling);
|
|
228
|
+
}
|
|
229
|
+
`;
|