@driftengine/core 4.2.1 → 4.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.json +272 -0
- package/README.md +1 -1
- package/dist/cinematic/cameraPath.d.ts +53 -0
- package/dist/cinematic/cameraPath.js +118 -0
- package/dist/environment/daylight.d.ts +77 -0
- package/dist/environment/daylight.js +77 -0
- package/dist/geometry/meshConcat.d.ts +9 -0
- package/dist/geometry/meshConcat.js +101 -0
- package/dist/index.d.ts +12 -2
- package/dist/index.js +14 -1
- package/dist/render/backend/createRenderer.d.ts +13 -1
- package/dist/render/backend/createRenderer.js +6 -5
- package/dist/render/backend/webgl2/renderer.d.ts +166 -1
- package/dist/render/backend/webgl2/renderer.js +424 -23
- package/dist/render/backend/webgpu/buffers.js +46 -8
- package/dist/render/backend/webgpu/depthPass.d.ts +81 -3
- package/dist/render/backend/webgpu/depthPass.js +75 -14
- package/dist/render/backend/webgpu/exposurePass.d.ts +39 -0
- package/dist/render/backend/webgpu/exposurePass.js +190 -0
- package/dist/render/backend/webgpu/fieldCompose.d.ts +11 -0
- package/dist/render/backend/webgpu/fieldCompose.js +13 -0
- package/dist/render/backend/webgpu/flatPass.d.ts +3 -1
- package/dist/render/backend/webgpu/flatPass.js +12 -5
- package/dist/render/backend/webgpu/particlePass.d.ts +8 -0
- package/dist/render/backend/webgpu/particlePass.js +1 -0
- package/dist/render/backend/webgpu/postPass.d.ts +12 -2
- package/dist/render/backend/webgpu/probeBake.d.ts +59 -3
- package/dist/render/backend/webgpu/probeBake.js +97 -7
- package/dist/render/backend/webgpu/probeTargets.d.ts +65 -0
- package/dist/render/backend/webgpu/probeTargets.js +180 -0
- package/dist/render/backend/webgpu/renderer.d.ts +176 -11
- package/dist/render/backend/webgpu/renderer.js +680 -250
- package/dist/render/backend/webgpu/scatterPass.js +7 -7
- package/dist/render/backend/webgpu/shaders/clusterBin.wgsl.d.ts +1 -1
- package/dist/render/backend/webgpu/skinPaletteRing.d.ts +4 -0
- package/dist/render/backend/webgpu/skinPaletteRing.js +9 -0
- package/dist/render/backend/webgpu/skyPass.js +5 -1
- package/dist/render/backend/webgpu/uniformRing.d.ts +31 -1
- package/dist/render/backend/webgpu/uniformRing.js +53 -3
- package/dist/render/bloomChain.d.ts +6 -0
- package/dist/render/bloomChain.js +8 -0
- package/dist/render/clusteredLights.d.ts +19 -11
- package/dist/render/clusteredLights.js +96 -45
- package/dist/render/cutoutCaster.d.ts +23 -0
- package/dist/render/cutoutCaster.js +20 -0
- package/dist/render/driftLight/bake.d.ts +34 -0
- package/dist/render/driftLight/bake.js +95 -0
- package/dist/render/driftLight/falloff.d.ts +16 -0
- package/dist/render/driftLight/falloff.js +23 -0
- package/dist/render/driftLight/layout.d.ts +59 -0
- package/dist/render/driftLight/layout.js +138 -0
- package/dist/render/driftLight/lightField.d.ts +108 -0
- package/dist/render/driftLight/lightField.js +179 -0
- package/dist/render/driftLight/uniforms.d.ts +28 -0
- package/dist/render/driftLight/uniforms.js +41 -0
- package/dist/render/driftLight/visibility.d.ts +18 -0
- package/dist/render/driftLight/visibility.js +47 -0
- package/dist/render/emptyTexture.d.ts +6 -0
- package/dist/render/emptyTexture.js +19 -0
- package/dist/render/exposurePass.d.ts +48 -0
- package/dist/render/exposurePass.js +172 -0
- package/dist/render/flameLight.d.ts +71 -0
- package/dist/render/flameLight.js +63 -0
- package/dist/render/gi/bounceLights.d.ts +34 -0
- package/dist/render/gi/bounceLights.js +66 -0
- package/dist/render/gi/fieldReach.d.ts +12 -0
- package/dist/render/gi/fieldReach.js +62 -0
- package/dist/render/gi/globalField.d.ts +8 -0
- package/dist/render/gi/globalField.js +31 -0
- package/dist/render/gi/probeTrace.d.ts +3 -1
- package/dist/render/gi/probeTrace.js +3 -1
- package/dist/render/gpuCapability.js +19 -4
- package/dist/render/lightBudget.d.ts +11 -0
- package/dist/render/lightBudget.js +11 -0
- package/dist/render/pointLightSelection.d.ts +16 -0
- package/dist/render/pointLightSelection.js +178 -83
- package/dist/render/probeGrid.d.ts +11 -1
- package/dist/render/probeGrid.js +5 -2
- package/dist/render/probeSweeps.d.ts +45 -0
- package/dist/render/probeSweeps.js +77 -0
- package/dist/render/reflectionProbe.d.ts +43 -1
- package/dist/render/reflectionProbe.js +17 -3
- package/dist/render/renderQuality.d.ts +3 -1
- package/dist/render/rendererHarness.d.ts +9 -0
- package/dist/render/rendererHarness.js +18 -3
- package/dist/render/sceneTarget.d.ts +20 -0
- package/dist/render/sceneTarget.js +62 -1
- package/dist/render/shaders/agx.d.ts +31 -0
- package/dist/render/shaders/agx.js +81 -0
- package/dist/render/shaders/ambientOcclusion.d.ts +24 -5
- package/dist/render/shaders/ambientOcclusion.js +120 -63
- package/dist/render/shaders/cutoutCoverage.d.ts +31 -0
- package/dist/render/shaders/cutoutCoverage.js +40 -0
- package/dist/render/shaders/depth.d.ts +25 -0
- package/dist/render/shaders/depth.js +69 -3
- package/dist/render/shaders/exposure.d.ts +52 -0
- package/dist/render/shaders/exposure.js +124 -0
- package/dist/render/shaders/filmLook.d.ts +47 -0
- package/dist/render/shaders/filmLook.js +98 -0
- package/dist/render/shaders/flat/directionalShadow.js +35 -4
- package/dist/render/shaders/flat/driftLight.d.ts +29 -0
- package/dist/render/shaders/flat/driftLight.js +75 -0
- package/dist/render/shaders/flat/index.js +5 -0
- package/dist/render/shaders/flat/lobes.js +13 -0
- package/dist/render/shaders/flat/main.d.ts +1 -1
- package/dist/render/shaders/flat/main.js +68 -9
- package/dist/render/shaders/flat/preamble.js +23 -5
- package/dist/render/shaders/flat/probeGrid.js +46 -3
- package/dist/render/shaders/generated/ambientOcclusion.wgsl.d.ts +1 -1
- package/dist/render/shaders/generated/ambientOcclusion.wgsl.js +1 -1
- package/dist/render/shaders/generated/depth.wgsl.d.ts +100 -0
- package/dist/render/shaders/generated/depth.wgsl.js +100 -0
- package/dist/render/shaders/generated/exposure.wgsl.d.ts +42 -0
- package/dist/render/shaders/generated/exposure.wgsl.js +48 -0
- package/dist/render/shaders/generated/flat.wgsl.d.ts +1800 -1344
- package/dist/render/shaders/generated/flat.wgsl.js +1816 -1360
- package/dist/render/shaders/generated/localExposure.wgsl.d.ts +18 -0
- package/dist/render/shaders/generated/localExposure.wgsl.js +24 -0
- package/dist/render/shaders/generated/rush.wgsl.d.ts +40 -10
- package/dist/render/shaders/generated/rush.wgsl.js +40 -10
- package/dist/render/shaders/generated/temporalResolve.wgsl.d.ts +1 -1
- package/dist/render/shaders/generated/temporalResolve.wgsl.js +1 -1
- package/dist/render/shaders/gi/driftLight.wgsl.d.ts +16 -0
- package/dist/render/shaders/gi/driftLight.wgsl.js +49 -0
- package/dist/render/shaders/gi/probeBake.wgsl.d.ts +29 -4
- package/dist/render/shaders/gi/probeBake.wgsl.js +190 -20
- package/dist/render/shaders/globalMedium.js +4 -0
- package/dist/render/shaders/localExposure.d.ts +35 -0
- package/dist/render/shaders/localExposure.js +138 -0
- package/dist/render/shaders/prefilter.d.ts +1 -1
- package/dist/render/shaders/prefilter.js +6 -0
- package/dist/render/shaders/rush.js +33 -5
- package/dist/render/shaders/tangentFrame.d.ts +1 -1
- package/dist/render/shaders/tangentFrame.js +15 -3
- package/dist/render/shaders/temporalResolve.js +39 -1
- package/dist/render/shadowCasters.d.ts +5 -3
- package/dist/render/shadowMap.js +13 -0
- package/dist/render/surfaceTexture.d.ts +5 -0
- package/package.json +6 -6
- package/scripts/gpuCompute.mjs +24 -3
- package/scripts/png.mjs +23 -12
- package/src/cinematic/cameraPath.ts +178 -0
- package/src/environment/daylight.ts +152 -0
- package/src/geometry/meshConcat.ts +100 -0
- package/src/index.ts +32 -0
- package/src/render/backend/createRenderer.ts +22 -6
- package/src/render/backend/webgl2/renderer.ts +462 -20
- package/src/render/backend/webgpu/buffers.ts +51 -8
- package/src/render/backend/webgpu/depthPass.ts +78 -10
- package/src/render/backend/webgpu/exposurePass.ts +234 -0
- package/src/render/backend/webgpu/fieldCompose.ts +15 -0
- package/src/render/backend/webgpu/flatPass.ts +10 -2
- package/src/render/backend/webgpu/particlePass.ts +9 -0
- package/src/render/backend/webgpu/probeBake.ts +143 -9
- package/src/render/backend/webgpu/probeTargets.ts +211 -0
- package/src/render/backend/webgpu/renderer.ts +807 -271
- package/src/render/backend/webgpu/scatterPass.ts +7 -7
- package/src/render/backend/webgpu/skinPaletteRing.ts +10 -0
- package/src/render/backend/webgpu/skyPass.ts +5 -1
- package/src/render/backend/webgpu/uniformRing.ts +49 -3
- package/src/render/bloomChain.ts +9 -0
- package/src/render/clusteredLights.ts +108 -63
- package/src/render/cutoutCaster.ts +44 -0
- package/src/render/driftLight/bake.ts +125 -0
- package/src/render/driftLight/falloff.ts +26 -0
- package/src/render/driftLight/layout.ts +177 -0
- package/src/render/driftLight/lightField.ts +227 -0
- package/src/render/driftLight/uniforms.ts +61 -0
- package/src/render/driftLight/visibility.ts +58 -0
- package/src/render/emptyTexture.ts +41 -0
- package/src/render/exposurePass.ts +186 -0
- package/src/render/flameLight.ts +135 -0
- package/src/render/gi/bounceLights.ts +68 -0
- package/src/render/gi/fieldReach.ts +74 -0
- package/src/render/gi/globalField.ts +39 -0
- package/src/render/gi/probeTrace.ts +3 -1
- package/src/render/gpuCapability.ts +18 -5
- package/src/render/lightBudget.ts +12 -0
- package/src/render/pointLightSelection.ts +191 -79
- package/src/render/probeGrid.ts +13 -2
- package/src/render/probeSweeps.ts +80 -0
- package/src/render/reflectionProbe.ts +54 -2
- package/src/render/renderQuality.ts +3 -1
- package/src/render/rendererHarness.ts +25 -3
- package/src/render/sceneTarget.ts +75 -1
- package/src/render/shaders/ambientOcclusion.ts +125 -63
- package/src/render/shaders/cutoutCoverage.ts +43 -0
- package/src/render/shaders/depth.ts +80 -3
- package/src/render/shaders/exposure.ts +128 -0
- package/src/render/shaders/filmLook.ts +111 -0
- package/src/render/shaders/flat/directionalShadow.ts +35 -4
- package/src/render/shaders/flat/driftLight.ts +75 -0
- package/src/render/shaders/flat/index.ts +5 -0
- package/src/render/shaders/flat/lobes.ts +13 -0
- package/src/render/shaders/flat/main.ts +68 -9
- package/src/render/shaders/flat/preamble.ts +23 -5
- package/src/render/shaders/flat/probeGrid.ts +46 -3
- package/src/render/shaders/generated/ambientOcclusion.wgsl.ts +1 -1
- package/src/render/shaders/generated/depth.wgsl.ts +103 -0
- package/src/render/shaders/generated/exposure.wgsl.ts +51 -0
- package/src/render/shaders/generated/flat.wgsl.ts +1816 -1360
- package/src/render/shaders/generated/localExposure.wgsl.ts +26 -0
- package/src/render/shaders/generated/rush.wgsl.ts +40 -10
- package/src/render/shaders/generated/temporalResolve.wgsl.ts +1 -1
- package/src/render/shaders/gi/driftLight.wgsl.ts +49 -0
- package/src/render/shaders/gi/probeBake.wgsl.ts +191 -20
- package/src/render/shaders/globalMedium.ts +4 -0
- package/src/render/shaders/localExposure.ts +154 -0
- package/src/render/shaders/prefilter.ts +6 -0
- package/src/render/shaders/rush.ts +33 -5
- package/src/render/shaders/tangentFrame.ts +15 -3
- package/src/render/shaders/temporalResolve.ts +39 -1
- package/src/render/shadowCasters.ts +5 -3
- package/src/render/shadowMap.ts +14 -0
- package/src/render/surfaceTexture.ts +5 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Eye adaptation: the frame's own brightness, measured on the GPU and followed over time.
|
|
3
|
+
*
|
|
4
|
+
* **A fixed exposure is right for one view and wrong for the next.** A courtyard with the sun on
|
|
5
|
+
* its upper walls and its floor in the shade of a 19 m wall is several stops darker at ground level
|
|
6
|
+
* than from the gallery, and an exposure set for one draws the other either clipped or black. A
|
|
7
|
+
* camera meters and an eye adapts; this does both, from the finished scene, with no readback.
|
|
8
|
+
*
|
|
9
|
+
* **Two passes, both tiny.** The meter writes a 32×32 grid, each texel the mean of 8×8 taps of log
|
|
10
|
+
* luminance over its share of the frame. The adaptation reads all 1,024 of them in one texel,
|
|
11
|
+
* averages them into the frame's log-mean luminance, and moves the held value toward it by a blend
|
|
12
|
+
* the caller's frame time decides. The composite reads the held value and scales the scene toward
|
|
13
|
+
* middle grey. Log luminance, because an eye's response is to ratios: one sunlit pixel beside a
|
|
14
|
+
* dark frame should not set the exposure the way an arithmetic mean lets it.
|
|
15
|
+
*
|
|
16
|
+
* What it gives up is metering by region: every pixel counts the same, so a small bright window in
|
|
17
|
+
* a dark room pulls less than a photographer's spot meter would. What would change it is a scene
|
|
18
|
+
* that needs a subject held at a brightness, and the answer then is a centre weight in the meter.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/** Texels a side in the meter's grid, and taps a side within each. */
|
|
22
|
+
export const METER_GRID = 32;
|
|
23
|
+
export const METER_TAPS = 8;
|
|
24
|
+
/** The luminance the meter clamps to, in stops: a black pixel is not minus infinity. */
|
|
25
|
+
export const METER_MIN_LOG2 = -12;
|
|
26
|
+
export const METER_MAX_LOG2 = 8;
|
|
27
|
+
/** What full adaptation brings the frame's log-mean luminance to: middle grey. */
|
|
28
|
+
export const ADAPTED_KEY = 0.18;
|
|
29
|
+
/** The furthest adaptation moves the exposure either way, in stops. A black loading screen is not a
|
|
30
|
+
* reason to multiply the next frame by a thousand. */
|
|
31
|
+
export const ADAPT_RANGE_STOPS = 6;
|
|
32
|
+
/**
|
|
33
|
+
* How fast the held brightness follows the measured one, per second: 1.5 closes about 78% of the
|
|
34
|
+
* way in a second. An eye takes longer to open than to close, and a camera's meter is faster than
|
|
35
|
+
* either; one rate is what a scene that cuts between shots can predict.
|
|
36
|
+
*/
|
|
37
|
+
export const ADAPT_RATE = 1.5;
|
|
38
|
+
|
|
39
|
+
export const EXPOSURE_METER_FRAG = `#version 300 es
|
|
40
|
+
precision highp float;
|
|
41
|
+
|
|
42
|
+
in vec2 vUv;
|
|
43
|
+
|
|
44
|
+
/** The finished scene, in scene light, before any exposure or curve. */
|
|
45
|
+
uniform highp sampler2D uScene;
|
|
46
|
+
|
|
47
|
+
out float fragColor;
|
|
48
|
+
|
|
49
|
+
void main() {
|
|
50
|
+
/* This texel's corner, and taps spread evenly over its share of the frame. */
|
|
51
|
+
vec2 corner = vUv - vec2(0.5 / ${METER_GRID.toFixed(1)});
|
|
52
|
+
float sum = 0.0;
|
|
53
|
+
for (int y = 0; y < ${METER_TAPS}; y++) {
|
|
54
|
+
for (int x = 0; x < ${METER_TAPS}; x++) {
|
|
55
|
+
vec2 uv = corner + (vec2(float(x), float(y)) + 0.5) / ${(METER_GRID * METER_TAPS).toFixed(1)};
|
|
56
|
+
vec3 c = textureLod(uScene, uv, 0.0).rgb;
|
|
57
|
+
float luma = dot(c, vec3(0.2126, 0.7152, 0.0722));
|
|
58
|
+
sum += clamp(log2(max(luma, 1e-8)), ${METER_MIN_LOG2.toFixed(1)}, ${METER_MAX_LOG2.toFixed(1)});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
fragColor = sum / ${(METER_TAPS * METER_TAPS).toFixed(1)};
|
|
62
|
+
}
|
|
63
|
+
`;
|
|
64
|
+
|
|
65
|
+
export const EXPOSURE_ADAPT_FRAG = `#version 300 es
|
|
66
|
+
precision highp float;
|
|
67
|
+
|
|
68
|
+
/** The meter's grid of log luminances. */
|
|
69
|
+
uniform highp sampler2D uMeter;
|
|
70
|
+
/** The brightness held from the frames before, in stops, one texel. */
|
|
71
|
+
uniform highp sampler2D uHeld;
|
|
72
|
+
/** How far toward this frame's measurement to move: 1 snaps, 0 holds. */
|
|
73
|
+
uniform float uBlend;
|
|
74
|
+
|
|
75
|
+
out float fragColor;
|
|
76
|
+
|
|
77
|
+
void main() {
|
|
78
|
+
float sum = 0.0;
|
|
79
|
+
for (int y = 0; y < ${METER_GRID}; y++) {
|
|
80
|
+
for (int x = 0; x < ${METER_GRID}; x++) {
|
|
81
|
+
sum += texelFetch(uMeter, ivec2(x, y), 0).r;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
float measured = sum / ${(METER_GRID * METER_GRID).toFixed(1)};
|
|
85
|
+
float held = texelFetch(uHeld, ivec2(0, 0), 0).r;
|
|
86
|
+
fragColor = mix(held, measured, uBlend);
|
|
87
|
+
}
|
|
88
|
+
`;
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* `uAutoExposure`, `uExposureHeld` and `autoExposureGain`, for the composite. The gain scales scene
|
|
92
|
+
* light before the tone curve, toward the key by the fraction the caller asked for.
|
|
93
|
+
*/
|
|
94
|
+
export const AUTO_EXPOSURE_GLSL = `
|
|
95
|
+
/** How far the frame adapts toward middle grey, 0 to 1. 0 is off and the whole of the off path. */
|
|
96
|
+
uniform float uAutoExposure;
|
|
97
|
+
/** The held log-mean luminance, in stops, one texel. */
|
|
98
|
+
uniform highp sampler2D uExposureHeld;
|
|
99
|
+
|
|
100
|
+
float autoExposureGain() {
|
|
101
|
+
if (uAutoExposure <= 0.0) return 1.0;
|
|
102
|
+
float held = texelFetch(uExposureHeld, ivec2(0, 0), 0).r;
|
|
103
|
+
float stops = uAutoExposure * (${Math.log2(ADAPTED_KEY).toFixed(6)} - held);
|
|
104
|
+
return exp2(clamp(stops, -${ADAPT_RANGE_STOPS.toFixed(1)}, ${ADAPT_RANGE_STOPS.toFixed(1)}));
|
|
105
|
+
}
|
|
106
|
+
`;
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* The fraction of the way toward this frame's measurement to move, after `dtSec` seconds. A cut,
|
|
110
|
+
* or a first frame with nothing held, snaps.
|
|
111
|
+
*/
|
|
112
|
+
export function adaptBlend(dtSec: number, snap: boolean): number {
|
|
113
|
+
if (snap) return 1;
|
|
114
|
+
if (!(dtSec > 0)) return 0;
|
|
115
|
+
return 1 - Math.exp(-ADAPT_RATE * dtSec);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** The scene's gain for a held log-mean `heldLog2` at `strength`, as the composite computes it. */
|
|
119
|
+
export function autoExposureGain(heldLog2: number, strength: number): number {
|
|
120
|
+
if (!(strength > 0)) return 1;
|
|
121
|
+
const stops = strength * (Math.log2(ADAPTED_KEY) - heldLog2);
|
|
122
|
+
return 2 ** Math.min(Math.max(stops, -ADAPT_RANGE_STOPS), ADAPT_RANGE_STOPS);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** An adaptation strength as the shader takes it: 0 to 1, anything else off. */
|
|
126
|
+
export function clampAutoExposure(strength: number): number {
|
|
127
|
+
return Number.isFinite(strength) ? Math.min(Math.max(strength, 0), 1) : 0;
|
|
128
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The lens and the print: a vignette in scene light, and grain in display values.
|
|
3
|
+
*
|
|
4
|
+
* **The vignette is the lens's, so it acts on light rather than on the picture.** A lens loses light
|
|
5
|
+
* toward its corners by roughly the fourth power of the cosine of the angle off axis, which for a
|
|
6
|
+
* point `r` from the centre is `1 / (1 + k r^2)^2`. Applied to scene light before the tone curve,
|
|
7
|
+
* a bright corner rolls off through the curve's shoulder the way a photographed one does, where a
|
|
8
|
+
* vignette painted over the finished picture greys it. `r` is measured to the frame's corner on a
|
|
9
|
+
* circle, not an ellipse, so a wide window is not darker at its sides than at its top.
|
|
10
|
+
*
|
|
11
|
+
* **The grain is the print's, so it acts on display values after the grade and the veil.** It is
|
|
12
|
+
* one value a pixel, the same on all three channels, weighted toward the midtones where film shows
|
|
13
|
+
* it and kept faint in deep shadow and highlight, where adding it would lift black and grey white.
|
|
14
|
+
* It also dithers: a dark gradient quantised to eight bits bands, and a noise of a few levels breaks
|
|
15
|
+
* the bands up. **The seed is the caller's**, because the engine takes time from its caller: a held
|
|
16
|
+
* capture passes the same seed and is identical run to run, and a playing scene passes a new one
|
|
17
|
+
* each frame and the grain moves as film does.
|
|
18
|
+
*
|
|
19
|
+
* Zero strength for each is the whole of its off path: a branch on a uniform, and the frame is what
|
|
20
|
+
* it was byte for byte.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* `uVignette`, `uGrain`, `withVignette` and `withGrain`. The rush composite includes it once.
|
|
25
|
+
*
|
|
26
|
+
* `withVignette` takes the pixel's coordinate and the scene's size, since a distance to the corner
|
|
27
|
+
* has to know the aspect. `withGrain` takes the fragment's integer position, so the grain is one
|
|
28
|
+
* value a pixel whatever the resolution.
|
|
29
|
+
*/
|
|
30
|
+
export const FILM_LOOK_GLSL = `
|
|
31
|
+
/** How strongly the lens darkens its corners: 0 is none, 0.5 about 1.2 stops at the corner. */
|
|
32
|
+
uniform float uVignette;
|
|
33
|
+
/** x: the grain's amplitude in display values, 0 for none. y: this frame's seed, the caller's. */
|
|
34
|
+
uniform vec2 uGrain;
|
|
35
|
+
|
|
36
|
+
vec3 withVignette(vec3 c, vec2 uv, vec2 size) {
|
|
37
|
+
if (uVignette <= 0.0) return c;
|
|
38
|
+
vec2 d = (uv - 0.5) * size;
|
|
39
|
+
vec2 corner = 0.5 * size;
|
|
40
|
+
float r2 = dot(d, d) / dot(corner, corner);
|
|
41
|
+
float falloff = 1.0 + uVignette * r2;
|
|
42
|
+
return c / (falloff * falloff);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
float grainNoise(uvec2 p, uint seed) {
|
|
46
|
+
uint h = p.x * 1664525u + p.y * 1013904223u + seed * 2654435769u;
|
|
47
|
+
h ^= h >> 16u;
|
|
48
|
+
h *= 2246822519u;
|
|
49
|
+
h ^= h >> 13u;
|
|
50
|
+
h *= 3266489917u;
|
|
51
|
+
h ^= h >> 16u;
|
|
52
|
+
return float(h) * (1.0 / 4294967295.0);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
vec3 withGrain(vec3 c, vec2 fragCoord) {
|
|
56
|
+
if (uGrain.x <= 0.0) return c;
|
|
57
|
+
float luma = dot(clamp(c, 0.0, 1.0), vec3(0.2126, 0.7152, 0.0722));
|
|
58
|
+
float weight = 0.25 + 3.0 * luma * (1.0 - luma);
|
|
59
|
+
float n = grainNoise(uvec2(fragCoord), uint(uGrain.y)) - 0.5;
|
|
60
|
+
return c + vec3(2.0 * n * uGrain.x * weight);
|
|
61
|
+
}
|
|
62
|
+
`;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The vignette's factor at `uv` on a `width` by `height` frame, as the shader computes it.
|
|
66
|
+
* The shader's arithmetic written out once more, so a test can pin it to hand-derived values.
|
|
67
|
+
*/
|
|
68
|
+
export function vignetteFactor(
|
|
69
|
+
u: number,
|
|
70
|
+
v: number,
|
|
71
|
+
width: number,
|
|
72
|
+
height: number,
|
|
73
|
+
strength: number,
|
|
74
|
+
): number {
|
|
75
|
+
if (strength <= 0) return 1;
|
|
76
|
+
const dx = (u - 0.5) * width;
|
|
77
|
+
const dy = (v - 0.5) * height;
|
|
78
|
+
const r2 = (dx * dx + dy * dy) / (0.25 * (width * width + height * height));
|
|
79
|
+
const falloff = 1 + strength * r2;
|
|
80
|
+
return 1 / (falloff * falloff);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** How much of the grain a display luminance takes: full in the midtones, a quarter at the ends. */
|
|
84
|
+
export function grainWeight(luma: number): number {
|
|
85
|
+
const l = Math.min(Math.max(luma, 0), 1);
|
|
86
|
+
return 0.25 + 3 * l * (1 - l);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Both backends' words when there is no composite to apply the look in. */
|
|
90
|
+
export const FILM_LOOK_WITHOUT_COMPOSITE =
|
|
91
|
+
'[driftengine] setVignette and setFilmGrain need `screenEffects`: without a composite every ' +
|
|
92
|
+
'pass is the last thing to touch the frame, so there is no one place to apply a lens or a ' +
|
|
93
|
+
'print. The look was not applied.';
|
|
94
|
+
|
|
95
|
+
/** A vignette strength as the shader takes it: 0 to 4, anything else off. */
|
|
96
|
+
export function clampVignette(strength: number): number {
|
|
97
|
+
return Number.isFinite(strength) ? Math.min(Math.max(strength, 0), 4) : 0;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** A grain amplitude in display values: 0 to 0.5, anything else off. */
|
|
101
|
+
export function clampGrain(strength: number): number {
|
|
102
|
+
return Number.isFinite(strength) ? Math.min(Math.max(strength, 0), 0.5) : 0;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* A seed as the shader reads it: a whole number below 2^24, which a 32-bit float carries exactly,
|
|
107
|
+
* so the same seed is the same grain on both backends. Any number a caller has is accepted.
|
|
108
|
+
*/
|
|
109
|
+
export function grainSeed(seed: number): number {
|
|
110
|
+
return Number.isFinite(seed) ? Math.floor(Math.abs(seed)) % 16777216 : 0;
|
|
111
|
+
}
|
|
@@ -68,10 +68,13 @@ float shadowFactor(float directionalNdl) {
|
|
|
68
68
|
// Near a surface's own light terminator its direct-light term is already
|
|
69
69
|
// approaching zero, while the orthographic projection of that surface
|
|
70
70
|
// collapses toward a line. No finite contact-safe bias can represent that
|
|
71
|
-
// slope reliably
|
|
72
|
-
//
|
|
71
|
+
// slope reliably, so through this band the fine filter below hands over to the
|
|
72
|
+
// coarse test after it. \`directionalNdl\` is the surface's own, before any normal
|
|
73
|
+
// map: the band is about the receiving plane, and a map's tilt says nothing about it.
|
|
73
74
|
float receiverFade = smoothstep(0.08, 0.20, directionalNdl);
|
|
74
|
-
|
|
75
|
+
/* A surface turned from the light takes none of it, so there is nothing to shadow; what reads
|
|
76
|
+
this there is the emissive share, which has always seen such a surface as unshadowed. */
|
|
77
|
+
if (directionalNdl <= 0.0) return 1.0;
|
|
75
78
|
|
|
76
79
|
// The receiver stays at its real position, computed above. A normal offset changes
|
|
77
80
|
// direction at a hard mesh edge and tears one continuous shadow where it crosses from
|
|
@@ -104,6 +107,34 @@ float shadowFactor(float directionalNdl) {
|
|
|
104
107
|
);
|
|
105
108
|
if (lowElevationFade <= 0.0) return 1.0;
|
|
106
109
|
|
|
110
|
+
/*
|
|
111
|
+
* **The coarse test: is there anything a metre or more between this point and the sun.**
|
|
112
|
+
*
|
|
113
|
+
* One tap at the receiver's own texel, with a metre of tolerance where the filter below allows
|
|
114
|
+
* fourteen centimetres. It cannot see a contact shadow, and it does not need the plane's slope:
|
|
115
|
+
* a grazing plane's own depth moves about a centimetre times the tangent of its angle to the
|
|
116
|
+
* light across a texel, which stays under a metre down to a hundredth of n·l on a map with
|
|
117
|
+
* centimetre texels. What it does see is a roof.
|
|
118
|
+
*
|
|
119
|
+
* **This band used to return fully lit**, on the argument that the Lambert term was already
|
|
120
|
+
* near zero there. Under a high sun that is every vertical wall: at 70° of elevation a wall's
|
|
121
|
+
* n·l is at most 0.34, and one turned from the sun's bearing sits inside the band. A gallery's
|
|
122
|
+
* back wall under its vault took the sun through the vault, and so did the faces of its
|
|
123
|
+
* columns, which read as lit stone in shade; with a normal map carrying that n·l per texel it
|
|
124
|
+
* was a field of white specks. What would make this wrong is a map whose texels are tens of
|
|
125
|
+
* centimetres, where a grazing plane's own depth moves more than the tolerance and the band
|
|
126
|
+
* draws the acne it exists to avoid.
|
|
127
|
+
*/
|
|
128
|
+
float coarseCompare = p.z - 1.0 / uShadowDepthSpan;
|
|
129
|
+
float coarse =
|
|
130
|
+
directionalVisibility(p.z, coarseCompare, textureLod(uStaticShadowMap, p.xy, 0.0).r) *
|
|
131
|
+
directionalVisibility(p.z, coarseCompare, textureLod(uDynamicShadowMap, p.xy, 0.0).r);
|
|
132
|
+
if (uPeeledShadowEnabled != 0) {
|
|
133
|
+
coarse *= directionalVisibility(p.z, coarseCompare, textureLod(uPeeledShadowMap, p.xy, 0.0).r);
|
|
134
|
+
}
|
|
135
|
+
float strength = uShadowStrength * edgeFade * lowElevationFade;
|
|
136
|
+
if (receiverFade <= 0.0) return mix(1.0, coarse, strength);
|
|
137
|
+
|
|
107
138
|
float texel = 1.35 / uShadowMapSize;
|
|
108
139
|
/*
|
|
109
140
|
* How far the plane compensation below may carry a tap's depth, in the map's own units.
|
|
@@ -182,7 +213,7 @@ float shadowFactor(float directionalNdl) {
|
|
|
182
213
|
// conventional one-depth map would discard; movers remain independent too.
|
|
183
214
|
float lit = staticLit * peeledLit * dynamicLit;
|
|
184
215
|
|
|
185
|
-
return mix(1.0, lit,
|
|
216
|
+
return mix(1.0, mix(coarse, lit, receiverFade), strength);
|
|
186
217
|
}
|
|
187
218
|
#endif
|
|
188
219
|
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DriftLight in the lit shader: how much of a pixel the summed light stands in for, and the summed
|
|
3
|
+
* light itself, read from the field's sparse volume.
|
|
4
|
+
*
|
|
5
|
+
* **The split is per pixel.** Inside the frame's completeness radius (`uDriftLight.y`, measured from
|
|
6
|
+
* the eye) every light that reaches the pixel was chosen, so it is shaded exactly; past it the
|
|
7
|
+
* volume stands in, and across the band before the radius the two crossfade. The choice was made
|
|
8
|
+
* about some centre, which need not be the eye; `uniforms.ts` takes the distance between the two
|
|
9
|
+
* off the radius, so a pixel inside it is inside the choice's radius too. A field light's
|
|
10
|
+
* exact contribution is scaled by the share left over (`main.ts`, where its weight's sign is read),
|
|
11
|
+
* so a light is counted once however the pixel is split.
|
|
12
|
+
*
|
|
13
|
+
* **One index fetch and two filtered ones.** The index finds the brick; the brick's four samples a
|
|
14
|
+
* side share their faces with the next brick's, so the hardware's trilinear lookup inside one is
|
|
15
|
+
* seamless across the join. The light texel carries in its fourth channel whether each sample is a
|
|
16
|
+
* sample at all, and dividing by the filtered value drops the samples buried in walls rather than
|
|
17
|
+
* blending their darkness into the surface. The lookup is nudged half a spacing along the normal,
|
|
18
|
+
* which keeps a surface reading the air in front of it rather than the wall it is part of.
|
|
19
|
+
*
|
|
20
|
+
* **Two vectors of uniforms, and the sizes read off the textures.** The lit stage sits two vectors
|
|
21
|
+
* under the 256 a reported part offers at eight lights, and the first cut of this, at five, took that
|
|
22
|
+
* part down a rung to four. So the grid's extent and the atlas's are what `textureSize` answers, and
|
|
23
|
+
* the radius is measured from `uCameraPos`, which the stage already has, rather than from a centre
|
|
24
|
+
* of its own. What it gives up is two integer queries a pixel past the radius.
|
|
25
|
+
*
|
|
26
|
+
* `textureLod` throughout, by the 2026-08-07 rule: this is reached through branches no compiler can
|
|
27
|
+
* prove uniform. Both samplers are declared `highp` because GLSL ES gives 3D samplers no default.
|
|
28
|
+
*/
|
|
29
|
+
export const DRIFT_LIGHT_GLSL = /* glsl */ `
|
|
30
|
+
/** x how much of the summed light is in the frame (0 off), y the radius, z the band, w the scale. */
|
|
31
|
+
uniform vec4 uDriftLight;
|
|
32
|
+
/** xyz the first brick's first sample; w metres between samples, a third of a brick. */
|
|
33
|
+
uniform vec4 uDriftLightOrigin;
|
|
34
|
+
uniform highp usampler3D uDriftLightIndex; // wgsl:share shadow
|
|
35
|
+
uniform highp sampler3D uDriftLightAtlas;
|
|
36
|
+
|
|
37
|
+
/** The share of this pixel's field light the volume stands in for: 0 inside the radius, 1 past it. */
|
|
38
|
+
float driftLightShare(vec3 world) {
|
|
39
|
+
if (uDriftLight.x <= 0.0) return 0.0;
|
|
40
|
+
float away = distance(world, uCameraPos);
|
|
41
|
+
return smoothstep(uDriftLight.y - uDriftLight.z, uDriftLight.y, away) * uDriftLight.x;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** The summed light arriving on a surface at \`world\` facing \`n\`. */
|
|
45
|
+
vec3 driftLightIrradiance(vec3 world, vec3 n) {
|
|
46
|
+
float spacing = uDriftLightOrigin.w;
|
|
47
|
+
vec3 at = (world + n * (0.5 * spacing) - uDriftLightOrigin.xyz) / (3.0 * spacing);
|
|
48
|
+
ivec3 cell = ivec3(floor(at));
|
|
49
|
+
if (any(lessThan(cell, ivec3(0))) || any(greaterThanEqual(cell, textureSize(uDriftLightIndex, 0)))) {
|
|
50
|
+
return vec3(0.0);
|
|
51
|
+
}
|
|
52
|
+
uint id = texelFetch(uDriftLightIndex, cell, 0).r;
|
|
53
|
+
if (id == 0u) return vec3(0.0);
|
|
54
|
+
/* A brick is eight texels across (its light block, then its direction block) and four up and deep. */
|
|
55
|
+
ivec3 size = textureSize(uDriftLightAtlas, 0);
|
|
56
|
+
int brick = int(id) - 1;
|
|
57
|
+
int across = size.x / 8;
|
|
58
|
+
int down = size.y / 4;
|
|
59
|
+
vec3 corner = vec3(
|
|
60
|
+
float((brick % across) * 8),
|
|
61
|
+
float(((brick / across) % down) * 4),
|
|
62
|
+
float((brick / (across * down)) * 4)
|
|
63
|
+
);
|
|
64
|
+
vec3 texel = 1.0 / vec3(size);
|
|
65
|
+
vec3 inside = (at - vec3(cell)) * 3.0 + 0.5;
|
|
66
|
+
vec4 light = textureLod(uDriftLightAtlas, (corner + inside) * texel, 0.0);
|
|
67
|
+
if (light.a < 1e-3) return vec3(0.0);
|
|
68
|
+
vec4 toward = textureLod(uDriftLightAtlas, (corner + vec3(4.0, 0.0, 0.0) + inside) * texel, 0.0);
|
|
69
|
+
vec3 arriving = light.rgb / light.a;
|
|
70
|
+
vec3 from = toward.xyz / light.a;
|
|
71
|
+
/* Lambert for a single light, a quarter for light arriving from everywhere: see \`bake.ts\`. */
|
|
72
|
+
float facing = max(dot(n, from), 0.0) + (1.0 - min(length(from), 1.0)) * 0.25;
|
|
73
|
+
return arriving * facing;
|
|
74
|
+
}
|
|
75
|
+
`;
|
|
@@ -2,6 +2,7 @@ import { resolveConditionals } from '../conditionals.ts';
|
|
|
2
2
|
import { OCTAHEDRAL_GLSL } from '../octahedral.ts';
|
|
3
3
|
import { PROBEGRID_GLSL } from './probeGrid.ts';
|
|
4
4
|
import { TANGENT_FRAME_GLSL } from '../tangentFrame.ts';
|
|
5
|
+
import { CUTOUT_COVERAGE_GLSL } from '../cutoutCoverage.ts';
|
|
5
6
|
import { preambleGlsl } from './preamble.ts';
|
|
6
7
|
import { LOBES_GLSL } from './lobes.ts';
|
|
7
8
|
import { POINTSHADOW_GLSL } from './pointShadow.ts';
|
|
@@ -10,6 +11,7 @@ import { MORPH_GLSL } from '../morph.ts';
|
|
|
10
11
|
import { SKINNING_GLSL } from '../skinning.ts';
|
|
11
12
|
import { SURFACE_GLSL } from './surface.ts';
|
|
12
13
|
import { MAIN_GLSL } from './main.ts';
|
|
14
|
+
import { DRIFT_LIGHT_GLSL } from './driftLight.ts';
|
|
13
15
|
import { CHANNEL_ATTRIBUTE, CHANNEL_BEND } from '../vertexChannel.ts';
|
|
14
16
|
import { FULL_LIGHT_BUDGET, type LightBudget } from '../../uniformVectorBudget.ts';
|
|
15
17
|
|
|
@@ -489,6 +491,9 @@ export function flatFrag(options: FlatShaderOptions): string {
|
|
|
489
491
|
* and `main` is unconditional by construction.
|
|
490
492
|
*/
|
|
491
493
|
TANGENT_FRAME_GLSL,
|
|
494
|
+
/* Unconditional for the same reason, and called only where a cutoff was asked for. */
|
|
495
|
+
CUTOUT_COVERAGE_GLSL,
|
|
496
|
+
DRIFT_LIGHT_GLSL,
|
|
492
497
|
MAIN_GLSL,
|
|
493
498
|
].join('\n'),
|
|
494
499
|
{
|
|
@@ -74,6 +74,19 @@ const float PENUMBRA_FADE = 1.6;
|
|
|
74
74
|
*/
|
|
75
75
|
const float MAX_FILTER_RADIUS = 0.25;
|
|
76
76
|
|
|
77
|
+
/**
|
|
78
|
+
* The weight below which a point light's shadow is not looked up at all.
|
|
79
|
+
*
|
|
80
|
+
* A light's shadow is blended toward none as the light falls off, \`mix(1, occl, weight)\` with the
|
|
81
|
+
* weight twice the falloff, so a shadow looked up under this floor changes at most three per cent
|
|
82
|
+
* of a light that has already all but faded: with the inverse-square falloff, past about eight
|
|
83
|
+
* metres from it. Measured on a lantern-lit courtyard, the lookups the floor skips were most of a
|
|
84
|
+
* night's frame: 14 ms of 26 at 1440p, a blocker tap and twelve filter taps for every light in every
|
|
85
|
+
* froxel. What it gives up is the last three per cent of a distant light's shadow, which the blend
|
|
86
|
+
* was already taking away; what would make it wrong is a falloff that stays strong far out.
|
|
87
|
+
*/
|
|
88
|
+
const float POINT_SHADOW_MIN_WEIGHT = 0.03;
|
|
89
|
+
|
|
77
90
|
/**
|
|
78
91
|
* How much of a self-lit surface's glow a lamp's shadow may take.
|
|
79
92
|
*
|
|
@@ -37,8 +37,13 @@ export const MAIN_GLSL = `void main() {
|
|
|
37
37
|
if (uAlbedoEnabled != 0) {
|
|
38
38
|
vec4 texel = texture(uAlbedo, vUv);
|
|
39
39
|
// Discarded before anything else is computed: a thrown-away fragment should not pay
|
|
40
|
-
// for the lighting it will never contribute to.
|
|
41
|
-
|
|
40
|
+
// for the lighting it will never contribute to. The test credits alpha for the mip level
|
|
41
|
+
// it samples, so a needle averaged thin down the chain is not lost; see cutoutCoverage.ts.
|
|
42
|
+
// Behind a branch on a uniform, so a material with no cutoff pays nothing for it.
|
|
43
|
+
if (uAlbedoCutout > 0.0) {
|
|
44
|
+
float tested = cutoutAlpha(texel.a, vUv * vec2(textureSize(uAlbedo, 0)));
|
|
45
|
+
if (tested < uAlbedoCutout) discard;
|
|
46
|
+
}
|
|
42
47
|
albedo *= texel.rgb;
|
|
43
48
|
coverage = texel.a;
|
|
44
49
|
}
|
|
@@ -81,6 +86,15 @@ export const MAIN_GLSL = `void main() {
|
|
|
81
86
|
|
|
82
87
|
if (uLightingEnabled != 0) {
|
|
83
88
|
vec3 n = normalize(vNormal);
|
|
89
|
+
/*
|
|
90
|
+
* **Which face of a two-sided surface this is, from the normal and not from the rasteriser.**
|
|
91
|
+
* The rasteriser's own facing flag would say it exactly, and it is one more input to the stage: the flat
|
|
92
|
+
* shader already carries sixteen, WebGPU's ceiling, and the seventeenth made every pipeline
|
|
93
|
+
* invalid and every mesh draw nothing. A face whose own normal points away from the eye is the
|
|
94
|
+
* back of the sheet; what that gives up is a sliver at a curved silhouette, where a front face's
|
|
95
|
+
* interpolated normal already leans away and is lit as the back.
|
|
96
|
+
*/
|
|
97
|
+
bool backFace = uDoubleSided != 0 && dot(n, uCameraPos - vWorldPos) < 0.0;
|
|
84
98
|
/*
|
|
85
99
|
* The authored normal, before either relief perturbs it.
|
|
86
100
|
*
|
|
@@ -260,6 +274,15 @@ export const MAIN_GLSL = `void main() {
|
|
|
260
274
|
);
|
|
261
275
|
}
|
|
262
276
|
|
|
277
|
+
/*
|
|
278
|
+
* **The back face of a two-sided surface is lit as its front**, which is glTF's rule for
|
|
279
|
+
* \`doubleSided\`: the whole shading normal turned toward the viewer, after the map and the
|
|
280
|
+
* relief have perturbed it, as three.js turns its normal, tangent and bitangent together.
|
|
281
|
+
* Turning only the interpolated normal would leave a normal map's tilt pointing the wrong way
|
|
282
|
+
* across the fold of every curtain.
|
|
283
|
+
*/
|
|
284
|
+
if (backFace) n = -n;
|
|
285
|
+
|
|
263
286
|
/*
|
|
264
287
|
* One dot product, used twice: ndl is the lit side and its negative is the night side below.
|
|
265
288
|
* Shared rather than issued twice because it is the same number and one is cheaper, and not
|
|
@@ -273,8 +296,23 @@ export const MAIN_GLSL = `void main() {
|
|
|
273
296
|
* term below needs it too. The sun and the moon are the only sources whose shadow
|
|
274
297
|
* used to stop at a glowing surface.
|
|
275
298
|
*/
|
|
299
|
+
/*
|
|
300
|
+
* **The surface's own n·l, before any map tilted it: the side of the surface the sun is on.**
|
|
301
|
+
*
|
|
302
|
+
* A normal map says which way each texel faces within its surface; it cannot put a texel on the
|
|
303
|
+
* other side of that surface from the sun. Read from the mapped normal alone, the texels of a
|
|
304
|
+
* column's face turned from a high sun that leaned into the light were lit as though the column
|
|
305
|
+
* were not in the way: a field of white specks over stone in shade. So the shadow's band is
|
|
306
|
+
* chosen by this, and the sun is taken away as it reaches zero, over the last two hundredths of
|
|
307
|
+
* n·l, where an unmapped surface's own Lambert term is already under 0.02. Over a tenth it moved
|
|
308
|
+
* every smooth column's terminator in the published scenes. What it gives up is the bump-lit
|
|
309
|
+
* relief of a surface barely facing the sun, where the light was already grazing.
|
|
310
|
+
*/
|
|
311
|
+
vec3 surfaceN = normalize(vNormal);
|
|
312
|
+
if (backFace) surfaceN = -surfaceN;
|
|
313
|
+
float surfaceNdl = dot(surfaceN, uDirectionalDir);
|
|
276
314
|
#if DIRECTIONAL_SHADOWS
|
|
277
|
-
float sunShade = shadowFactor(
|
|
315
|
+
float sunShade = shadowFactor(max(surfaceNdl, 0.0));
|
|
278
316
|
#else
|
|
279
317
|
float sunShade = 1.0;
|
|
280
318
|
#endif
|
|
@@ -298,7 +336,13 @@ export const MAIN_GLSL = `void main() {
|
|
|
298
336
|
* that shipped before it.
|
|
299
337
|
*/
|
|
300
338
|
sunShade *= vSkyDirect;
|
|
301
|
-
|
|
339
|
+
/*
|
|
340
|
+
* And the sun gone from a surface turned from it, whatever its map says. See \`surfaceNdl\`.
|
|
341
|
+
* Its own factor rather than folded into sunShade, which the emissive share reads as the sun's
|
|
342
|
+
* shadow: a lamp's pane turned from the moon is not in shadow, it is only unlit.
|
|
343
|
+
*/
|
|
344
|
+
float sunFacing = smoothstep(0.0, 0.02, surfaceNdl);
|
|
345
|
+
float direct = ndl * sunShade * sunFacing;
|
|
302
346
|
|
|
303
347
|
/*
|
|
304
348
|
* Hemispheric: sky above, ground below, mixed by which way the surface looks. An
|
|
@@ -328,6 +372,7 @@ export const MAIN_GLSL = `void main() {
|
|
|
328
372
|
* set, which is the guarantee that option was added to give.
|
|
329
373
|
*/
|
|
330
374
|
vec3 irradiance = gridIrradiance(n, vWorldPos);
|
|
375
|
+
irradiance = mix(irradiance, ambient, probeGridOutside(surfaceN, vWorldPos));
|
|
331
376
|
ambient = mix(ambient, irradiance, uProbeGridAmbient * uEnvironmentEnabled);
|
|
332
377
|
#endif
|
|
333
378
|
/*
|
|
@@ -406,7 +451,7 @@ export const MAIN_GLSL = `void main() {
|
|
|
406
451
|
* before: \`sunSpec\` is \`vec3(vSpecular)\` at metal 0 and \`vSpecular\` defaults to 0.
|
|
407
452
|
*/
|
|
408
453
|
vec3 sunHighlight =
|
|
409
|
-
uDirectionalColor * specularLobe(max(dot(n, halfway), 0.0), surfaceRoughness) * sunSpec * sunShade;
|
|
454
|
+
uDirectionalColor * specularLobe(max(dot(n, halfway), 0.0), surfaceRoughness) * sunSpec * sunShade * sunFacing;
|
|
410
455
|
/*
|
|
411
456
|
* The dielectric's share goes in here, where it has always gone, so the environment blend
|
|
412
457
|
* below dims it by exactly the reflectance it always dimmed it by. The metal's share is added
|
|
@@ -813,6 +858,7 @@ export const MAIN_GLSL = `void main() {
|
|
|
813
858
|
*/
|
|
814
859
|
vec3 lampOpen = vec3(0.0);
|
|
815
860
|
vec3 lampShadowed = vec3(0.0);
|
|
861
|
+
float driftShare = driftLightShare(vWorldPos);
|
|
816
862
|
|
|
817
863
|
/*
|
|
818
864
|
* Which froxel this fragment is in.
|
|
@@ -912,7 +958,7 @@ export const MAIN_GLSL = `void main() {
|
|
|
912
958
|
cookie = uintBitsToFloat(rec4.w);
|
|
913
959
|
} else {
|
|
914
960
|
i = slot;
|
|
915
|
-
if (i >= uLightCount) break;
|
|
961
|
+
if (i >= uLightCount || i >= MAX_LIGHTS) break;
|
|
916
962
|
lightPos = uLightPos[i];
|
|
917
963
|
lightRadius = uLightRadius[i];
|
|
918
964
|
lightColor = uLightColor[i];
|
|
@@ -925,6 +971,9 @@ export const MAIN_GLSL = `void main() {
|
|
|
925
971
|
iesAxis = uLightIesAxis[i];
|
|
926
972
|
cookie = uLightCookie[i];
|
|
927
973
|
}
|
|
974
|
+
/* A DriftLight field's own light carries a negative weight, and is shaded exactly only for the
|
|
975
|
+
share of this pixel the field does not stand in for. See \`driftLight.ts\`. */
|
|
976
|
+
if (lightWeight < 0.0) lightWeight = -lightWeight * (1.0 - driftShare);
|
|
928
977
|
/*
|
|
929
978
|
* **The cone, and a point light's is one that admits every direction.**
|
|
930
979
|
*
|
|
@@ -987,6 +1036,8 @@ export const MAIN_GLSL = `void main() {
|
|
|
987
1036
|
* grazing the angle is. Without it the cubemap's face seams show as a
|
|
988
1037
|
* diagonal cross and the whole plane bands.
|
|
989
1038
|
*/
|
|
1039
|
+
/* How present this light's shadow is. See \`POINT_SHADOW_MIN_WEIGHT\`. */
|
|
1040
|
+
float shadowWeight = clamp(falloff * 2.0, 0.0, 1.0);
|
|
990
1041
|
#if POINT_SHADOWS
|
|
991
1042
|
float grazing = 1.0 - ndl;
|
|
992
1043
|
float normalOffset = (0.06 + dist * 0.05) * grazing * grazing + 0.02;
|
|
@@ -1025,7 +1076,7 @@ export const MAIN_GLSL = `void main() {
|
|
|
1025
1076
|
*/
|
|
1026
1077
|
int shadowRead = clamp(shadowSlot, 0, MAX_LIGHTS - 1);
|
|
1027
1078
|
int layer = shadowSlot >= 0 && shadowSlot < MAX_LIGHTS ? uPointShadowLayer[shadowRead] : -1;
|
|
1028
|
-
if (layer >= 0) {
|
|
1079
|
+
if (layer >= 0 && shadowWeight > POINT_SHADOW_MIN_WEIGHT) {
|
|
1029
1080
|
occl = mix(
|
|
1030
1081
|
1.0,
|
|
1031
1082
|
pointShadow(
|
|
@@ -1054,7 +1105,7 @@ export const MAIN_GLSL = `void main() {
|
|
|
1054
1105
|
float liveWeight = 0.0;
|
|
1055
1106
|
int liveLayer =
|
|
1056
1107
|
shadowSlot >= 0 && shadowSlot < MAX_LIGHTS ? uLivePointShadowLayer[shadowRead] : -1;
|
|
1057
|
-
if (liveLayer >= 0) {
|
|
1108
|
+
if (liveLayer >= 0 && shadowWeight > POINT_SHADOW_MIN_WEIGHT) {
|
|
1058
1109
|
liveOccl = pointShadow(
|
|
1059
1110
|
uPointShadows,
|
|
1060
1111
|
float(liveLayer),
|
|
@@ -1074,7 +1125,7 @@ export const MAIN_GLSL = `void main() {
|
|
|
1074
1125
|
// Blend occlusion out as the light fades, so a shadow can never be more
|
|
1075
1126
|
// present than the light casting it — and slot changes at the edge of
|
|
1076
1127
|
// range become invisible rather than a pop.
|
|
1077
|
-
float shaded = mix(1.0, occl,
|
|
1128
|
+
float shaded = mix(1.0, occl, shadowWeight);
|
|
1078
1129
|
/*
|
|
1079
1130
|
* The cone multiplies the distance falloff rather than replacing it, which is what makes a
|
|
1080
1131
|
* spot a point light with a direction rather than a second kind of light. It is exactly 1
|
|
@@ -1494,6 +1545,14 @@ export const MAIN_GLSL = `void main() {
|
|
|
1494
1545
|
);
|
|
1495
1546
|
#endif
|
|
1496
1547
|
lit += lampOpenFloor * lampShade;
|
|
1548
|
+
/*
|
|
1549
|
+
* DriftLight: every fixed light past the frame's choice, summed and occluded, for the share of
|
|
1550
|
+
* this pixel the exact lights above no longer cover. Diffuse only, and so no metal term: see
|
|
1551
|
+
* \`driftLight.ts\`.
|
|
1552
|
+
*/
|
|
1553
|
+
if (driftShare > 0.0) {
|
|
1554
|
+
lit += albedo * driftLightIrradiance(vWorldPos, n) * (driftShare * uDriftLight.w) * (1.0 - metal);
|
|
1555
|
+
}
|
|
1497
1556
|
|
|
1498
1557
|
/*
|
|
1499
1558
|
* Self-illuminated geometry, dimmed where a lamp's shadow crosses it.
|
|
@@ -38,6 +38,12 @@ import { FOG_GLSL } from '../fog.ts';
|
|
|
38
38
|
export function preambleGlsl({ maxLights, maxAreaLights }: LightBudget): string {
|
|
39
39
|
return `#version 300 es
|
|
40
40
|
precision highp float;
|
|
41
|
+
/*
|
|
42
|
+
* highp int as well: a clustered light is decoded with uintBitsToFloat from texels that arrive as
|
|
43
|
+
* uvec4, and its texel index passes 32767 in a big world. The fragment default for int and uint is
|
|
44
|
+
* mediump, which a sixteen-bit driver truncates, and every clustered light then lights nothing.
|
|
45
|
+
*/
|
|
46
|
+
precision highp int;
|
|
41
47
|
|
|
42
48
|
in vec3 vNormal;
|
|
43
49
|
in vec3 vColor;
|
|
@@ -140,6 +146,12 @@ uniform int uAlbedoEnabled;
|
|
|
140
146
|
*/
|
|
141
147
|
uniform float uAlbedoCutout;
|
|
142
148
|
|
|
149
|
+
/**
|
|
150
|
+
* 1 for a surface seen from both faces: glTF's doubleSided, a curtain or a leaf card. Its draw culls
|
|
151
|
+
* nothing, and a back face is lit as a front, with the shading normal turned toward the viewer.
|
|
152
|
+
*/
|
|
153
|
+
uniform int uDoubleSided;
|
|
154
|
+
|
|
143
155
|
/**
|
|
144
156
|
* A normal map, in the surface's own space.
|
|
145
157
|
*
|
|
@@ -550,6 +562,12 @@ uniform vec3 uProbeGridCounts;
|
|
|
550
562
|
* exactly as it did before any probe existed.
|
|
551
563
|
*/
|
|
552
564
|
uniform float uProbeGridAmbient;
|
|
565
|
+
/**
|
|
566
|
+
* Which sets of layers the shading reads, and how far between them: the last whole sweep's layer
|
|
567
|
+
* offset, the newest whole sweep's, and the blend from the first toward the second. \`(0, 0, 0)\`
|
|
568
|
+
* for a grid that does not crossfade. See \`probeSweeps.ts\`.
|
|
569
|
+
*/
|
|
570
|
+
uniform vec3 uProbeGridSets;
|
|
553
571
|
#endif
|
|
554
572
|
/** A world-space box that lights up regardless of time of day (run finished). */
|
|
555
573
|
uniform vec3 uHighlightMin;
|
|
@@ -637,12 +655,12 @@ uvec4 clusterTexel(int texel) {
|
|
|
637
655
|
* both numbers sit beside the budgets they come from.
|
|
638
656
|
*/
|
|
639
657
|
/*
|
|
640
|
-
* **
|
|
641
|
-
*
|
|
642
|
-
*
|
|
643
|
-
*
|
|
658
|
+
* **The bound is the froxel's cap, and the fixed arm leaves long before it.** The two were equal
|
|
659
|
+
* until a candlelit interior needed more than sixteen lights in a froxel; see
|
|
660
|
+
* \`MAX_LIGHTS_PER_CLUSTER\`. The fixed arm breaks at \`uLightCount\` and at \`MAX_LIGHTS\`, so it
|
|
661
|
+
* iterates exactly what it did and never indexes its uniform arrays past their end.
|
|
644
662
|
*/
|
|
645
|
-
#define LIGHT_LOOP_MAX
|
|
663
|
+
#define LIGHT_LOOP_MAX MAX_LIGHTS_PER_CLUSTER
|
|
646
664
|
uniform int uLightCount;
|
|
647
665
|
/** 0 keeps the radius-shaped falloff; 1 is physical inverse-square with a soft cutoff. */
|
|
648
666
|
uniform int uLightFalloff;
|