@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,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* rgb9e5: three 9-bit mantissas over one shared 5-bit exponent, an unsigned float colour in four
|
|
3
|
+
* bytes. `EXT_texture_shared_exponent`'s encoding, the one a GPU's `rgb9e5ufloat` reads, written
|
|
4
|
+
* out so a lightmap page can hold its irradiance in four bytes a texel against a half float's eight.
|
|
5
|
+
*
|
|
6
|
+
* **What it gives up**: precision in the smaller channels, which share the largest one's exponent —
|
|
7
|
+
* a channel a thousandth of its neighbour keeps about one bit — and anything negative or past
|
|
8
|
+
* 65,408, which it clamps. Light is neither, and a bake's colours rarely span more than a few
|
|
9
|
+
* octaves within one texel, so a lightmap loses nothing visible; a value that needs each channel
|
|
10
|
+
* exact wants a format of its own.
|
|
11
|
+
*
|
|
12
|
+
* The bits, low to high: red in 0–8, green in 9–17, blue in 18–26, the exponent in 27–31, biased by
|
|
13
|
+
* 15, and a value is `mantissa * 2^(exponent - 15 - 9)`.
|
|
14
|
+
*/
|
|
15
|
+
/** The largest value the format holds: 511/512 of 2^16. */
|
|
16
|
+
export declare const RGB9E5_MAX: number;
|
|
17
|
+
/** One colour packed into its 32 bits, rounding to nearest; NaN and negatives are zero. */
|
|
18
|
+
export declare function packRgb9e5(red: number, green: number, blue: number): number;
|
|
19
|
+
/** A packed colour's three channels, into `out[at…at+2]`. */
|
|
20
|
+
export declare function unpackRgb9e5(bits: number, out: {
|
|
21
|
+
[index: number]: number;
|
|
22
|
+
}, at?: number): void;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* rgb9e5: three 9-bit mantissas over one shared 5-bit exponent, an unsigned float colour in four
|
|
3
|
+
* bytes. `EXT_texture_shared_exponent`'s encoding, the one a GPU's `rgb9e5ufloat` reads, written
|
|
4
|
+
* out so a lightmap page can hold its irradiance in four bytes a texel against a half float's eight.
|
|
5
|
+
*
|
|
6
|
+
* **What it gives up**: precision in the smaller channels, which share the largest one's exponent —
|
|
7
|
+
* a channel a thousandth of its neighbour keeps about one bit — and anything negative or past
|
|
8
|
+
* 65,408, which it clamps. Light is neither, and a bake's colours rarely span more than a few
|
|
9
|
+
* octaves within one texel, so a lightmap loses nothing visible; a value that needs each channel
|
|
10
|
+
* exact wants a format of its own.
|
|
11
|
+
*
|
|
12
|
+
* The bits, low to high: red in 0–8, green in 9–17, blue in 18–26, the exponent in 27–31, biased by
|
|
13
|
+
* 15, and a value is `mantissa * 2^(exponent - 15 - 9)`.
|
|
14
|
+
*/
|
|
15
|
+
const BIAS = 15;
|
|
16
|
+
const MANTISSA_BITS = 9;
|
|
17
|
+
const MANTISSA_VALUES = 1 << MANTISSA_BITS;
|
|
18
|
+
const MAX_EXPONENT = 31;
|
|
19
|
+
/** The largest value the format holds: 511/512 of 2^16. */
|
|
20
|
+
export const RGB9E5_MAX = ((MANTISSA_VALUES - 1) / MANTISSA_VALUES) * 2 ** (MAX_EXPONENT - BIAS);
|
|
21
|
+
const clampChannel = (v) => (v > 0 ? Math.min(v, RGB9E5_MAX) : 0);
|
|
22
|
+
/** One colour packed into its 32 bits, rounding to nearest; NaN and negatives are zero. */
|
|
23
|
+
export function packRgb9e5(red, green, blue) {
|
|
24
|
+
const r = clampChannel(red);
|
|
25
|
+
const g = clampChannel(green);
|
|
26
|
+
const b = clampChannel(blue);
|
|
27
|
+
const largest = Math.max(r, g, b);
|
|
28
|
+
let exponent = Math.max(-BIAS - 1, Math.floor(Math.log2(largest))) + 1 + BIAS;
|
|
29
|
+
let scale = 2 ** (exponent - BIAS - MANTISSA_BITS);
|
|
30
|
+
/* Rounding the largest channel up to 512 needs one more exponent, which the spec allows for. */
|
|
31
|
+
if (Math.floor(largest / scale + 0.5) === MANTISSA_VALUES) {
|
|
32
|
+
exponent += 1;
|
|
33
|
+
scale *= 2;
|
|
34
|
+
}
|
|
35
|
+
/* Inline rather than a helper: a page packs millions of texels, and a closure a call is one each. */
|
|
36
|
+
const mr = Math.min(MANTISSA_VALUES - 1, Math.floor(r / scale + 0.5));
|
|
37
|
+
const mg = Math.min(MANTISSA_VALUES - 1, Math.floor(g / scale + 0.5));
|
|
38
|
+
const mb = Math.min(MANTISSA_VALUES - 1, Math.floor(b / scale + 0.5));
|
|
39
|
+
return (mr | (mg << 9) | (mb << 18) | (exponent << 27)) >>> 0;
|
|
40
|
+
}
|
|
41
|
+
/** A packed colour's three channels, into `out[at…at+2]`. */
|
|
42
|
+
export function unpackRgb9e5(bits, out, at = 0) {
|
|
43
|
+
const scale = 2 ** ((bits >>> 27) - BIAS - MANTISSA_BITS);
|
|
44
|
+
out[at] = (bits & 511) * scale;
|
|
45
|
+
out[at + 1] = ((bits >>> 9) & 511) * scale;
|
|
46
|
+
out[at + 2] = ((bits >>> 18) & 511) * scale;
|
|
47
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A scene capture: a texture the world is drawn into from a camera of the caller's, which a material
|
|
3
|
+
* then shows like any image — a screen in the world showing what a camera elsewhere sees, a monitor,
|
|
4
|
+
* a security feed. The planar reflection's pass, for a caller's camera rather than a mirrored one.
|
|
5
|
+
*
|
|
6
|
+
* **It holds radiance, not display pixels**, as a probe does: drawn with the output transform held
|
|
7
|
+
* off, at the colour format a probe takes — half floats under `hdrScene`, bytes otherwise — and
|
|
8
|
+
* sampled linear, so a material showing it as its `emissive` emits the light the capture saw and the
|
|
9
|
+
* frame's grade then applies once, to the screen as to everything around it.
|
|
10
|
+
*
|
|
11
|
+
* **What it gives up**: it is the mesh pass alone, as a probe bake is — the sky, water, particles and
|
|
12
|
+
* anything else a scene draws after its world are in the capture only if the callback draws them;
|
|
13
|
+
* no temporal resolve, bloom or other effect after the world runs on it; and it has no mip chain, so
|
|
14
|
+
* a screen seen small and far shimmers where a mipmapped image would not.
|
|
15
|
+
*/
|
|
16
|
+
/** What a capture's texture is made from, in place of an image: its size and colour format. */
|
|
17
|
+
export interface SceneCaptureTexels {
|
|
18
|
+
readonly kind: 'scene-capture';
|
|
19
|
+
readonly width: number;
|
|
20
|
+
readonly height: number;
|
|
21
|
+
/** Half floats, which hold light past one; bytes otherwise. */
|
|
22
|
+
readonly float: boolean;
|
|
23
|
+
}
|
|
24
|
+
/** The largest side a capture may have, the size every WebGL2 device must take. */
|
|
25
|
+
export declare const MAX_SCENE_CAPTURE_SIZE = 4096;
|
|
26
|
+
/** A capture's description, refused by name where a side is not a whole number of pixels it can be. */
|
|
27
|
+
export declare function sceneCaptureTexels(width: number, height: number, float: boolean): SceneCaptureTexels;
|
|
28
|
+
/** Whether a texture source is a capture's description rather than an image. */
|
|
29
|
+
export declare function isSceneCaptureTexels(source: unknown): source is SceneCaptureTexels;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A scene capture: a texture the world is drawn into from a camera of the caller's, which a material
|
|
3
|
+
* then shows like any image — a screen in the world showing what a camera elsewhere sees, a monitor,
|
|
4
|
+
* a security feed. The planar reflection's pass, for a caller's camera rather than a mirrored one.
|
|
5
|
+
*
|
|
6
|
+
* **It holds radiance, not display pixels**, as a probe does: drawn with the output transform held
|
|
7
|
+
* off, at the colour format a probe takes — half floats under `hdrScene`, bytes otherwise — and
|
|
8
|
+
* sampled linear, so a material showing it as its `emissive` emits the light the capture saw and the
|
|
9
|
+
* frame's grade then applies once, to the screen as to everything around it.
|
|
10
|
+
*
|
|
11
|
+
* **What it gives up**: it is the mesh pass alone, as a probe bake is — the sky, water, particles and
|
|
12
|
+
* anything else a scene draws after its world are in the capture only if the callback draws them;
|
|
13
|
+
* no temporal resolve, bloom or other effect after the world runs on it; and it has no mip chain, so
|
|
14
|
+
* a screen seen small and far shimmers where a mipmapped image would not.
|
|
15
|
+
*/
|
|
16
|
+
/** The largest side a capture may have, the size every WebGL2 device must take. */
|
|
17
|
+
export const MAX_SCENE_CAPTURE_SIZE = 4096;
|
|
18
|
+
/** A capture's description, refused by name where a side is not a whole number of pixels it can be. */
|
|
19
|
+
export function sceneCaptureTexels(width, height, float) {
|
|
20
|
+
for (const [name, side] of [
|
|
21
|
+
['width', width],
|
|
22
|
+
['height', height],
|
|
23
|
+
]) {
|
|
24
|
+
if (!Number.isInteger(side) || side < 1 || side > MAX_SCENE_CAPTURE_SIZE) {
|
|
25
|
+
throw new Error(`createSceneCapture: ${name} must be a whole number from 1 to ${MAX_SCENE_CAPTURE_SIZE}, ` +
|
|
26
|
+
`not ${String(side)}`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return { kind: 'scene-capture', width, height, float };
|
|
30
|
+
}
|
|
31
|
+
/** Whether a texture source is a capture's description rather than an image. */
|
|
32
|
+
export function isSceneCaptureTexels(source) {
|
|
33
|
+
return (typeof source === 'object' &&
|
|
34
|
+
source !== null &&
|
|
35
|
+
source.kind === 'scene-capture');
|
|
36
|
+
}
|
|
@@ -89,6 +89,8 @@ export declare class SceneTarget {
|
|
|
89
89
|
private gradeStrength;
|
|
90
90
|
/** The lens and the print, held like the grade. See `filmLook.ts`. */
|
|
91
91
|
private vignette;
|
|
92
|
+
/** The lens's colour fringe: red's and green's pull and where it begins. See `fringe.ts`. */
|
|
93
|
+
private readonly fringe;
|
|
92
94
|
private grain;
|
|
93
95
|
private grainSeed;
|
|
94
96
|
/** Eye adaptation: how far, and this frame's time step. See `Renderer.setAutoExposure`. */
|
|
@@ -277,6 +279,8 @@ export declare class SceneTarget {
|
|
|
277
279
|
*/
|
|
278
280
|
/** The lens's corner falloff, held until changed. See `Renderer.setVignette`. */
|
|
279
281
|
setVignette(strength: number): void;
|
|
282
|
+
/** The lens's colour fringe, held until changed. See `Renderer.setChromaticAberration`. */
|
|
283
|
+
setChromaticAberration(intensity: number, start: number): void;
|
|
280
284
|
/** The print's grain and this frame's seed, held until changed. See `Renderer.setFilmGrain`. */
|
|
281
285
|
setFilmGrain(strength: number, seed: number): void;
|
|
282
286
|
/** How far the eye adapts, and the time since the last frame. See `Renderer.setAutoExposure`. */
|
|
@@ -298,6 +302,7 @@ export declare class SceneTarget {
|
|
|
298
302
|
resolve(strength: number, reachUv: number, grade?: {
|
|
299
303
|
readonly transform: number;
|
|
300
304
|
readonly exposure: number;
|
|
305
|
+
readonly film?: Float32Array;
|
|
301
306
|
}, motion?: {
|
|
302
307
|
readonly reprojection: Float32Array;
|
|
303
308
|
readonly strength: number;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { glSceneDepthFormat } from './depthConvention.js';
|
|
2
2
|
import { clampGrain, clampVignette, grainSeed } from './shaders/filmLook.js';
|
|
3
|
+
import { FRINGE_FLOATS, resolveFringe } from './shaders/fringe.js';
|
|
3
4
|
import { AmbientOcclusionPass } from './ambientOcclusionPass.js';
|
|
4
5
|
import { ExposurePass } from './exposurePass.js';
|
|
5
6
|
import { clampAutoExposure } from './shaders/exposure.js';
|
|
@@ -93,6 +94,8 @@ export class SceneTarget {
|
|
|
93
94
|
gradeStrength = 0;
|
|
94
95
|
/** The lens and the print, held like the grade. See `filmLook.ts`. */
|
|
95
96
|
vignette = 0;
|
|
97
|
+
/** The lens's colour fringe: red's and green's pull and where it begins. See `fringe.ts`. */
|
|
98
|
+
fringe = new Float32Array(FRINGE_FLOATS);
|
|
96
99
|
grain = 0;
|
|
97
100
|
grainSeed = 0;
|
|
98
101
|
/** Eye adaptation: how far, and this frame's time step. See `Renderer.setAutoExposure`. */
|
|
@@ -605,6 +608,10 @@ export class SceneTarget {
|
|
|
605
608
|
setVignette(strength) {
|
|
606
609
|
this.vignette = clampVignette(strength);
|
|
607
610
|
}
|
|
611
|
+
/** The lens's colour fringe, held until changed. See `Renderer.setChromaticAberration`. */
|
|
612
|
+
setChromaticAberration(intensity, start) {
|
|
613
|
+
resolveFringe(intensity, start, this.fringe);
|
|
614
|
+
}
|
|
608
615
|
/** The print's grain and this frame's seed, held until changed. See `Renderer.setFilmGrain`. */
|
|
609
616
|
setFilmGrain(strength, seed) {
|
|
610
617
|
this.grain = clampGrain(strength);
|
|
@@ -654,7 +661,10 @@ export class SceneTarget {
|
|
|
654
661
|
this.gradeSize = lut.size;
|
|
655
662
|
}
|
|
656
663
|
resolve(strength, reachUv,
|
|
657
|
-
/*
|
|
664
|
+
/*
|
|
665
|
+
* The grade, applied here because this is the last pass. See RUSH_FRAG's own note. `film` is
|
|
666
|
+
* the `filmic` curve's eight constants (`resolveFilmicCurve`), read only under transform 4.
|
|
667
|
+
*/
|
|
658
668
|
grade = {
|
|
659
669
|
transform: 0,
|
|
660
670
|
exposure: 1,
|
|
@@ -809,6 +819,12 @@ export class SceneTarget {
|
|
|
809
819
|
gl.uniform1f(this.uniforms['uReach'] ?? null, reachUv);
|
|
810
820
|
gl.uniform1i(this.uniforms['uOutputTransform'] ?? null, grade.transform);
|
|
811
821
|
gl.uniform1f(this.uniforms['uOutputExposure'] ?? null, grade.exposure);
|
|
822
|
+
if (grade.film !== undefined) {
|
|
823
|
+
gl.uniform4fv(this.uniforms['uFilmA'] ?? null, grade.film, 0, 4);
|
|
824
|
+
gl.uniform4fv(this.uniforms['uFilmB'] ?? null, grade.film, 4, 4);
|
|
825
|
+
}
|
|
826
|
+
/* A standard range always here: see `Renderer.displayRange`. */
|
|
827
|
+
gl.uniform1f(this.uniforms['uDisplayHeadroom'] ?? null, 1);
|
|
812
828
|
/*
|
|
813
829
|
* Depth on its own unit, and the reprojection with it. Bound every frame rather than
|
|
814
830
|
* once, because a texture unit is context state and any pass between two frames is free
|
|
@@ -894,6 +910,12 @@ export class SceneTarget {
|
|
|
894
910
|
gl.bindTexture(gl.TEXTURE_2D, localGrid ?? scene);
|
|
895
911
|
gl.uniform1i(this.uniforms['uExposureLocal'] ?? null, 6);
|
|
896
912
|
gl.uniform1f(this.uniforms['uLocalExposure'] ?? null, localGrid === null ? 0 : this.localExposure);
|
|
913
|
+
/* Each object's own motion is WebGPU's under a reconstruction; here the scene stands in on
|
|
914
|
+
unit 7 and the flag says not to read it, so the camera's motion alone blurs. */
|
|
915
|
+
gl.activeTexture(gl.TEXTURE7);
|
|
916
|
+
gl.bindTexture(gl.TEXTURE_2D, scene);
|
|
917
|
+
gl.uniform1i(this.uniforms['uMotion'] ?? null, 7);
|
|
918
|
+
gl.uniform1f(this.uniforms['uObjectMotion'] ?? null, 0);
|
|
897
919
|
gl.activeTexture(gl.TEXTURE0);
|
|
898
920
|
/* No unit and no texture: a flat colour, not a sample. Uploaded unconditionally — two
|
|
899
921
|
floats and a triplet cost nothing next to the triangle this pass already draws — and it
|
|
@@ -902,6 +924,7 @@ export class SceneTarget {
|
|
|
902
924
|
gl.uniform3fv(this.uniforms['uVeilColor'] ?? null, veilColor);
|
|
903
925
|
gl.uniform1f(this.uniforms['uVeilAlpha'] ?? null, veilAlpha);
|
|
904
926
|
gl.uniform1f(this.uniforms['uVignette'] ?? null, this.vignette);
|
|
927
|
+
gl.uniform3fv(this.uniforms['uFringe'] ?? null, this.fringe);
|
|
905
928
|
gl.uniform2f(this.uniforms['uGrain'] ?? null, this.grain, this.grainSeed);
|
|
906
929
|
gl.bindVertexArray(this.vao);
|
|
907
930
|
gl.drawArrays(gl.TRIANGLES, 0, 3);
|
|
@@ -24,6 +24,11 @@ import type { DecalBox } from './decalProjector.ts';
|
|
|
24
24
|
* which is the same price the temporal row declined for a velocity buffer. A caller declares a
|
|
25
25
|
* `ReflectiveSurface` instead: a box, and a facing rule for which surfaces inside it reflect. It is
|
|
26
26
|
* the shape `DecalProjector` already has, for the same reason.
|
|
27
|
+
*
|
|
28
|
+
* **Or every surface by its own material**, where the renderer was built with
|
|
29
|
+
* `screenSpaceReflections`: the lit stage leaves two maps of what it reflected — not a G-buffer, the
|
|
30
|
+
* environment's share and its tint, which is all a swap needs — and the same trace runs in a
|
|
31
|
+
* material mode over every pixel. See `frameReflections.ts`.
|
|
27
32
|
*/
|
|
28
33
|
/**
|
|
29
34
|
* Clip Y negated, for a backend whose framebuffer rows run downward.
|
|
@@ -159,9 +164,30 @@ export declare function edgeFade(margin: number, band: number): number;
|
|
|
159
164
|
* nothing at one running directly into it.
|
|
160
165
|
*/
|
|
161
166
|
export declare function towardCameraFade(direction: ReadonlyVec3, viewDirection: ReadonlyVec3): number;
|
|
167
|
+
/**
|
|
168
|
+
* What a `ReflectiveSurface` is made with: its box, which way the surfaces inside it face, and how
|
|
169
|
+
* much and how far they reflect. Every number but the box has a default; see each field.
|
|
170
|
+
*/
|
|
162
171
|
export interface ReflectiveSurfaceOptions extends DecalBox {
|
|
163
|
-
/**
|
|
172
|
+
/**
|
|
173
|
+
* How much of what the ray finds lands on the surface, 0 to 1: one share over the box, or with
|
|
174
|
+
* `fresnel` the surface's reflectance head-on.
|
|
175
|
+
*/
|
|
164
176
|
readonly strength?: number;
|
|
177
|
+
/**
|
|
178
|
+
* Whether `strength` is the surface's reflectance head-on, F0, rather than one share over the
|
|
179
|
+
* box. Each pixel then returns `F0 · A + B` of what its ray finds, the split-sum environment BRDF
|
|
180
|
+
* at its own view and `roughness` — Karis's fit, the one the lit stage reflects its environment
|
|
181
|
+
* by — so a floor returns more toward the horizon than underfoot, and the share follows the
|
|
182
|
+
* camera as Unreal weighs its reflections. **False by default**, the one share as before.
|
|
183
|
+
*
|
|
184
|
+
* **What it gives up**: the pass has no material buffer, so the roughness and F0 are the box's,
|
|
185
|
+
* not each pixel's, and nothing occludes the reflection where the lit stage's ambient occlusion
|
|
186
|
+
* would. A floor of one material is what it is for.
|
|
187
|
+
*/
|
|
188
|
+
readonly fresnel?: boolean;
|
|
189
|
+
/** The surface's roughness, 0 to 1, that `fresnel` weighs by. 0, a polished surface, if absent. */
|
|
190
|
+
readonly roughness?: number;
|
|
165
191
|
/** How far a ray may travel, world metres. */
|
|
166
192
|
readonly reachM?: number;
|
|
167
193
|
/** How deep a depth sample is treated as being. See `ScreenSpaceMarch.thicknessM`. */
|
|
@@ -189,6 +215,9 @@ export declare class ReflectiveSurface {
|
|
|
189
215
|
readonly axis: Float32Array<ArrayBuffer>;
|
|
190
216
|
readonly tint: Float32Array<ArrayBuffer>;
|
|
191
217
|
strength: number;
|
|
218
|
+
/** See `ReflectiveSurfaceOptions.fresnel`. */
|
|
219
|
+
fresnel: boolean;
|
|
220
|
+
roughness: number;
|
|
192
221
|
reachM: number;
|
|
193
222
|
thicknessM: number;
|
|
194
223
|
steps: number;
|
|
@@ -226,6 +255,8 @@ export interface RecordedReflection {
|
|
|
226
255
|
readonly axis: Float32Array;
|
|
227
256
|
readonly tint: Float32Array;
|
|
228
257
|
strength: number;
|
|
258
|
+
fresnel: boolean;
|
|
259
|
+
roughness: number;
|
|
229
260
|
reachM: number;
|
|
230
261
|
thicknessM: number;
|
|
231
262
|
steps: number;
|
|
@@ -23,6 +23,11 @@ import { decalToWorldMatrix, worldToDecalMatrix } from './decalProjector.js';
|
|
|
23
23
|
* which is the same price the temporal row declined for a velocity buffer. A caller declares a
|
|
24
24
|
* `ReflectiveSurface` instead: a box, and a facing rule for which surfaces inside it reflect. It is
|
|
25
25
|
* the shape `DecalProjector` already has, for the same reason.
|
|
26
|
+
*
|
|
27
|
+
* **Or every surface by its own material**, where the renderer was built with
|
|
28
|
+
* `screenSpaceReflections`: the lit stage leaves two maps of what it reflected — not a G-buffer, the
|
|
29
|
+
* environment's share and its tint, which is all a swap needs — and the same trace runs in a
|
|
30
|
+
* material mode over every pixel. See `frameReflections.ts`.
|
|
26
31
|
*/
|
|
27
32
|
/**
|
|
28
33
|
* Clip Y negated, for a backend whose framebuffer rows run downward.
|
|
@@ -292,6 +297,9 @@ export class ReflectiveSurface {
|
|
|
292
297
|
axis = new Float32Array(3);
|
|
293
298
|
tint = new Float32Array(3);
|
|
294
299
|
strength;
|
|
300
|
+
/** See `ReflectiveSurfaceOptions.fresnel`. */
|
|
301
|
+
fresnel;
|
|
302
|
+
roughness;
|
|
295
303
|
reachM;
|
|
296
304
|
thicknessM;
|
|
297
305
|
steps;
|
|
@@ -308,6 +316,8 @@ export class ReflectiveSurface {
|
|
|
308
316
|
/* Half, because a mirror is the rare case: a wet floor, polished stone and a car bonnet all
|
|
309
317
|
return a fraction of what lands on them, and a consumer who wants a mirror asks for one. */
|
|
310
318
|
this.strength = options.strength ?? 0.5;
|
|
319
|
+
this.fresnel = options.fresnel ?? false;
|
|
320
|
+
this.roughness = options.roughness ?? 0;
|
|
311
321
|
/* Eight metres covers a room and a street corner. Further costs steps rather than accuracy,
|
|
312
322
|
since the step is the reach divided by them. */
|
|
313
323
|
this.reachM = options.reachM ?? 8;
|
|
@@ -366,6 +376,8 @@ function newRecord() {
|
|
|
366
376
|
axis: new Float32Array(3),
|
|
367
377
|
tint: new Float32Array(3),
|
|
368
378
|
strength: 0.5,
|
|
379
|
+
fresnel: false,
|
|
380
|
+
roughness: 0,
|
|
369
381
|
reachM: 8,
|
|
370
382
|
thicknessM: 0.25,
|
|
371
383
|
steps: 24,
|
|
@@ -414,6 +426,8 @@ export class ReflectionQueue {
|
|
|
414
426
|
record.axis.set(surface.axis);
|
|
415
427
|
record.tint.set(surface.tint);
|
|
416
428
|
record.strength = Math.min(1, Math.max(0, surface.strength));
|
|
429
|
+
record.fresnel = surface.fresnel;
|
|
430
|
+
record.roughness = Math.min(1, Math.max(0, surface.roughness));
|
|
417
431
|
record.reachM = Math.max(0, surface.reachM);
|
|
418
432
|
record.thicknessM = Math.max(0, surface.thicknessM);
|
|
419
433
|
record.steps = Math.min(32, Math.max(1, Math.round(surface.steps)));
|
|
@@ -16,4 +16,4 @@
|
|
|
16
16
|
* gets motion without direction. That is what separates a spell from a lamp on a
|
|
17
17
|
* dimmer.
|
|
18
18
|
*/
|
|
19
|
-
export declare const ARCANE_FRAG = "#version 300 es\nprecision highp float;\n\nin vec2 vUv;\nin float vSeed;\nin float vDistance;\nin vec3 vWorldPos;\n\nuniform float uTime;\n\nuniform vec3 uFogColor;\n/** Extinction per metre **at the camera's own height**, not at the base. */\nuniform float uFogDensity;\n/** Reciprocal scale height of the haze, per metre. 0 is a uniform medium. */\nuniform float uFogHeightFalloff;\n/** The camera's height, so a fragment's own height is one subtraction away. */\nuniform float uFogEyeY;\nuniform vec3 uUnderwaterColor;\nuniform float uUnderwaterFogDensity;\nuniform float uUnderwaterFactor;\n/** 0 integrates a medium (Beer-Lambert); 1 ramps linearly between near and far. */\nuniform int uFogMode;\n/** Linear mode only: where haze begins, and where it is total. */\nuniform float uFogNear;\nuniform float uFogFar;\n\n/** What distance converges toward: the day's haze, or the water column. */\nvec3 mediumColor() {\n return mix(uFogColor, uUnderwaterColor, uUnderwaterFactor);\n}\n\n/**\n * How much of a surface standing at height pointY, dist metres away, is lost to\n * the medium in between. 0 is perfectly clear, 1 is the medium's own colour.\n */\nfloat mediumFog(float dist, float pointY) {\n /*\n * A depth ramp rather than a medium, for worlds authored against one.\n *\n * Beer-Lambert is the honest model and stays the default: it integrates something\n * physically there, so extinction begins at the eye and never stops. A linear near/far\n * fog is not that \u2014 it is a *look*, with a hard start distance before which the air is\n * perfectly clear, and it is what the fixed-function pipeline offered and what several\n * renderers still expose. Approximating one with the other is not a matter of tuning:\n * matched at half occlusion, the exponential curve still lays haze over everything\n * inside near, where the ramp lays none, and near-field colour desaturates toward the\n * fog colour with nothing to show for it.\n *\n * Height falloff has no meaning here and is ignored: a ramp is defined on distance\n * alone.\n */\n if (uFogMode == 1) {\n float span = max(uFogFar - uFogNear, 1e-4);\n float ramp = clamp((dist - uFogNear) / span, 0.0, 1.0);\n if (uUnderwaterFactor <= 0.0) return ramp;\n float wetLinear = uUnderwaterFogDensity * dist;\n return mix(ramp, 1.0 - exp2(-wetLinear * wetLinear * 1.442695), uUnderwaterFactor);\n }\n\n float t = (pointY - uFogEyeY) * uFogHeightFalloff;\n // (1 - e^-t)/t, which tends to 1 as the ray levels out. Guarded rather than\n // branched: at t = 0 the quotient is 0/0, and 1e-4 of a scale height is 8 mm.\n float denom = abs(
|
|
19
|
+
export declare const ARCANE_FRAG = "#version 300 es\nprecision highp float;\n\nin vec2 vUv;\nin float vSeed;\nin float vDistance;\nin vec3 vWorldPos;\n\nuniform float uTime;\n\nuniform vec3 uFogColor;\n/** Extinction per metre **at the camera's own height**, not at the base. */\nuniform float uFogDensity;\n/** Reciprocal scale height of the haze, per metre. 0 is a uniform medium. */\nuniform float uFogHeightFalloff;\n/** The camera's height, so a fragment's own height is one subtraction away. */\nuniform float uFogEyeY;\nuniform vec3 uUnderwaterColor;\nuniform float uUnderwaterFogDensity;\nuniform float uUnderwaterFactor;\n/** 0 integrates a medium (Beer-Lambert); 1 ramps linearly between near and far. */\nuniform int uFogMode;\n/** Linear mode only: where haze begins, and where it is total. */\nuniform float uFogNear;\nuniform float uFogFar;\n\n/** What distance converges toward: the day's haze, or the water column. */\nvec3 mediumColor() {\n return mix(uFogColor, uUnderwaterColor, uUnderwaterFactor);\n}\n\n/**\n * How much of a surface standing at height pointY, dist metres away, is lost to\n * the medium in between. 0 is perfectly clear, 1 is the medium's own colour.\n */\nfloat mediumFog(float dist, float pointY) {\n /*\n * A depth ramp rather than a medium, for worlds authored against one.\n *\n * Beer-Lambert is the honest model and stays the default: it integrates something\n * physically there, so extinction begins at the eye and never stops. A linear near/far\n * fog is not that \u2014 it is a *look*, with a hard start distance before which the air is\n * perfectly clear, and it is what the fixed-function pipeline offered and what several\n * renderers still expose. Approximating one with the other is not a matter of tuning:\n * matched at half occlusion, the exponential curve still lays haze over everything\n * inside near, where the ramp lays none, and near-field colour desaturates toward the\n * fog colour with nothing to show for it.\n *\n * Height falloff has no meaning here and is ignored: a ramp is defined on distance\n * alone.\n */\n if (uFogMode == 1) {\n float span = max(uFogFar - uFogNear, 1e-4);\n float ramp = clamp((dist - uFogNear) / span, 0.0, 1.0);\n if (uUnderwaterFactor <= 0.0) return ramp;\n float wetLinear = uUnderwaterFogDensity * dist;\n return mix(ramp, 1.0 - exp2(-wetLinear * wetLinear * 1.442695), uUnderwaterFactor);\n }\n\n // Where the medium begins along the ray: uFogNear, the linear mode's slot, which this mode\n // never read before Atmosphere.fogStart. The ray is integrated from there on, at the density\n // of that point; at a start of 0 every term is the one it always was. See render/fog.ts.\n float start = min(uFogNear, dist);\n float before = dist > 0.0 ? start / dist : 0.0;\n float t = (pointY - uFogEyeY) * uFogHeightFalloff;\n float rest = t * (1.0 - before);\n // (1 - e^-t)/t, which tends to 1 as the ray levels out. Guarded rather than\n // branched: at t = 0 the quotient is 0/0, and 1e-4 of a scale height is 8 mm.\n float denom = abs(rest) < 1e-4 ? 1e-4 : rest;\n float air =\n 1.0 - exp(-uFogDensity * exp(-t * before) * (dist - start) * (1.0 - exp(-denom)) / denom);\n if (uUnderwaterFactor <= 0.0) return air;\n // Water is its own medium and keeps its own curve, so crossing the surface\n // changes nothing about how the water column reads.\n float wet = uUnderwaterFogDensity * dist;\n return mix(air, 1.0 - exp2(-wet * wet * 1.442695), uUnderwaterFactor);\n}\n\nuniform int uNoiseOctaves;\nuniform vec4 uClipPlane;\nuniform int uClipEnabled;\n/** The caller's colour, so an aura shifts with the world's own palette. */\nuniform vec3 uTint;\n\nout vec4 outColor;\n\nfloat hash21(vec2 p) {\n return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123);\n}\n\nfloat valueNoise(vec2 p) {\n vec2 i = floor(p);\n vec2 f = fract(p);\n vec2 u = f * f * (3.0 - 2.0 * f);\n return mix(\n mix(hash21(i), hash21(i + vec2(1.0, 0.0)), u.x),\n mix(hash21(i + vec2(0.0, 1.0)), hash21(i + vec2(1.0, 1.0)), u.x),\n u.y\n );\n}\n\nvoid main() {\n if (uClipEnabled != 0 && dot(vec4(vWorldPos, 1.0), uClipPlane) < 0.0) discard;\n\n // Polar about the quad centre. This is the line that makes it an orb rather\n // than a column; everything downstream reads radius and angle, never height.\n float radius = length(vUv - 0.5) * 2.0;\n if (radius >= 1.0) discard;\n\n vec2 d = vUv - 0.5;\n float angle = atan(d.y, d.x);\n float t = uTime + vSeed * 37.0;\n\n /*\n * Two bands, turning opposite ways at different rates. Sampled in (angle, radius)\n * space so the noise wraps around the disc instead of sliding across it \u2014 noise in\n * UV space would drift off one edge and read as a texture being scrolled.\n */\n float inner = valueNoise(vec2(angle * 1.9 + t * 0.55, radius * 4.2));\n float outer = valueNoise(vec2(angle * 3.1 - t * 0.34, radius * 2.6));\n float swirl = inner * 0.62 + outer * 0.38;\n if (uNoiseOctaves > 1) {\n swirl = mix(swirl, swirl * valueNoise(vec2(angle * 6.3 + t * 0.9, radius * 8.0)) * 2.0, 0.35);\n }\n\n /*\n * Core and corona as two separate falloffs rather than one curve. A single falloff\n * gives a blob; what makes this read as *contained energy* is a small bright centre\n * with a much wider, much fainter halo, so the object inside stays legible.\n */\n float core = 1.0 - smoothstep(0.0, 0.28, radius);\n float corona = (1.0 - smoothstep(0.18, 1.0, radius)) * (0.35 + swirl * 0.65);\n float body = core * 0.9 + corona;\n if (body <= 0.02) discard;\n\n // Hot centre through the tint to a dark rim, so the core reads as white-hot\n // energy and the edge as the colour it is made of.\n vec3 col = mix(uTint * 0.35, uTint, smoothstep(0.0, 0.55, body));\n col = mix(col, vec3(1.0), smoothstep(0.82, 1.4, body));\n col *= uTint;\n\n float alpha = clamp(body * 1.6, 0.0, 1.0);\n // Additive light must lose alpha as it scatters, or mixing its colour alone\n // still makes a distant aura glow through fog and through water.\n float fog = mediumFog(vDistance, vWorldPos.y);\n col = mix(col, mediumColor(), fog);\n alpha *= 1.0 - fog;\n\n outColor = vec4(col, alpha);\n}\n";
|
|
@@ -14,4 +14,4 @@
|
|
|
14
14
|
* gives an arc its three-dimensionality is the path, and the path is world-space.
|
|
15
15
|
*/
|
|
16
16
|
export declare const BOLT_VERT = "#version 300 es\nlayout(location = 0) in vec3 aFrom;\nlayout(location = 1) in vec3 aTo;\n/** Along the segment (0 or 1), and which side of it (-1 or +1). */\nlayout(location = 2) in vec2 aCorner;\n/** Where this segment sits along its arc, the arc's envelope, its seed, its gain. */\nlayout(location = 3) in vec4 aArc;\n\nuniform mat4 uViewProj;\nuniform vec3 uCameraPos;\n/** Core half-width in metres, and how much of a pixel a far arc must still cover. */\nuniform float uWidth;\nuniform float uMinWidthPerMetre;\n\nout float vSide;\nout float vAlong;\nout float vFade;\nout float vSeed;\nout float vGain;\nout vec3 vWorldPos;\n\nvoid main() {\n vec3 mid = mix(aFrom, aTo, aCorner.x);\n vec3 dir = aTo - aFrom;\n float len = length(dir);\n dir = len > 1e-6 ? dir / len : vec3(0.0, 1.0, 0.0);\n\n vec3 view = mid - uCameraPos;\n float dist = length(view);\n view = dist > 1e-6 ? view / dist : vec3(0.0, 0.0, 1.0);\n\n /*\n * Across the segment and across the line of sight. Degenerate exactly when the\n * segment points at the viewer, where any perpendicular is as good as another \u2014\n * and where the segment covers almost no screen anyway.\n */\n vec3 across = cross(dir, view);\n float span = length(across);\n across = span > 1e-4 ? across / span : normalize(cross(dir, vec3(0.0, 1.0, 0.0)) + vec3(1e-3));\n\n /*\n * A floor on the world-space width that grows with distance, so a filament\n * never thins below a pixel. Under a pixel a bright thin line does not get\n * fainter, it *flickers* \u2014 the rasteriser catches it on some frames and not\n * others \u2014 and a strobing artefact reads as a bug, not as lightning.\n */\n float halfWidth = max(uWidth, dist * uMinWidthPerMetre);\n vec3 world = mid + across * aCorner.y * halfWidth;\n\n vSide = aCorner.y;\n vAlong = aArc.x;\n vFade = aArc.y;\n vSeed = aArc.z;\n vGain = aArc.w;\n vWorldPos = world;\n gl_Position = uViewProj * vec4(world, 1.0);\n}\n";
|
|
17
|
-
export declare const BOLT_FRAG = "#version 300 es\nprecision highp float;\n\nin float vSide;\nin float vAlong;\nin float vFade;\nin float vSeed;\nin float vGain;\nin vec3 vWorldPos;\n\nuniform float uTime;\n/** The saturating centre, and the glow it sits inside. */\nuniform vec3 uCoreColor;\nuniform vec3 uEdgeColor;\n/** How far past white the core is allowed to go. */\nuniform float uCoreGain;\n\nuniform vec3 uFogColor;\n/** Extinction per metre **at the camera's own height**, not at the base. */\nuniform float uFogDensity;\n/** Reciprocal scale height of the haze, per metre. 0 is a uniform medium. */\nuniform float uFogHeightFalloff;\n/** The camera's height, so a fragment's own height is one subtraction away. */\nuniform float uFogEyeY;\nuniform vec3 uUnderwaterColor;\nuniform float uUnderwaterFogDensity;\nuniform float uUnderwaterFactor;\n/** 0 integrates a medium (Beer-Lambert); 1 ramps linearly between near and far. */\nuniform int uFogMode;\n/** Linear mode only: where haze begins, and where it is total. */\nuniform float uFogNear;\nuniform float uFogFar;\n\n/** What distance converges toward: the day's haze, or the water column. */\nvec3 mediumColor() {\n return mix(uFogColor, uUnderwaterColor, uUnderwaterFactor);\n}\n\n/**\n * How much of a surface standing at height pointY, dist metres away, is lost to\n * the medium in between. 0 is perfectly clear, 1 is the medium's own colour.\n */\nfloat mediumFog(float dist, float pointY) {\n /*\n * A depth ramp rather than a medium, for worlds authored against one.\n *\n * Beer-Lambert is the honest model and stays the default: it integrates something\n * physically there, so extinction begins at the eye and never stops. A linear near/far\n * fog is not that \u2014 it is a *look*, with a hard start distance before which the air is\n * perfectly clear, and it is what the fixed-function pipeline offered and what several\n * renderers still expose. Approximating one with the other is not a matter of tuning:\n * matched at half occlusion, the exponential curve still lays haze over everything\n * inside near, where the ramp lays none, and near-field colour desaturates toward the\n * fog colour with nothing to show for it.\n *\n * Height falloff has no meaning here and is ignored: a ramp is defined on distance\n * alone.\n */\n if (uFogMode == 1) {\n float span = max(uFogFar - uFogNear, 1e-4);\n float ramp = clamp((dist - uFogNear) / span, 0.0, 1.0);\n if (uUnderwaterFactor <= 0.0) return ramp;\n float wetLinear = uUnderwaterFogDensity * dist;\n return mix(ramp, 1.0 - exp2(-wetLinear * wetLinear * 1.442695), uUnderwaterFactor);\n }\n\n float t = (pointY - uFogEyeY) * uFogHeightFalloff;\n // (1 - e^-t)/t, which tends to 1 as the ray levels out. Guarded rather than\n // branched: at t = 0 the quotient is 0/0, and 1e-4 of a scale height is 8 mm.\n float denom = abs(
|
|
17
|
+
export declare const BOLT_FRAG = "#version 300 es\nprecision highp float;\n\nin float vSide;\nin float vAlong;\nin float vFade;\nin float vSeed;\nin float vGain;\nin vec3 vWorldPos;\n\nuniform float uTime;\n/** The saturating centre, and the glow it sits inside. */\nuniform vec3 uCoreColor;\nuniform vec3 uEdgeColor;\n/** How far past white the core is allowed to go. */\nuniform float uCoreGain;\n\nuniform vec3 uFogColor;\n/** Extinction per metre **at the camera's own height**, not at the base. */\nuniform float uFogDensity;\n/** Reciprocal scale height of the haze, per metre. 0 is a uniform medium. */\nuniform float uFogHeightFalloff;\n/** The camera's height, so a fragment's own height is one subtraction away. */\nuniform float uFogEyeY;\nuniform vec3 uUnderwaterColor;\nuniform float uUnderwaterFogDensity;\nuniform float uUnderwaterFactor;\n/** 0 integrates a medium (Beer-Lambert); 1 ramps linearly between near and far. */\nuniform int uFogMode;\n/** Linear mode only: where haze begins, and where it is total. */\nuniform float uFogNear;\nuniform float uFogFar;\n\n/** What distance converges toward: the day's haze, or the water column. */\nvec3 mediumColor() {\n return mix(uFogColor, uUnderwaterColor, uUnderwaterFactor);\n}\n\n/**\n * How much of a surface standing at height pointY, dist metres away, is lost to\n * the medium in between. 0 is perfectly clear, 1 is the medium's own colour.\n */\nfloat mediumFog(float dist, float pointY) {\n /*\n * A depth ramp rather than a medium, for worlds authored against one.\n *\n * Beer-Lambert is the honest model and stays the default: it integrates something\n * physically there, so extinction begins at the eye and never stops. A linear near/far\n * fog is not that \u2014 it is a *look*, with a hard start distance before which the air is\n * perfectly clear, and it is what the fixed-function pipeline offered and what several\n * renderers still expose. Approximating one with the other is not a matter of tuning:\n * matched at half occlusion, the exponential curve still lays haze over everything\n * inside near, where the ramp lays none, and near-field colour desaturates toward the\n * fog colour with nothing to show for it.\n *\n * Height falloff has no meaning here and is ignored: a ramp is defined on distance\n * alone.\n */\n if (uFogMode == 1) {\n float span = max(uFogFar - uFogNear, 1e-4);\n float ramp = clamp((dist - uFogNear) / span, 0.0, 1.0);\n if (uUnderwaterFactor <= 0.0) return ramp;\n float wetLinear = uUnderwaterFogDensity * dist;\n return mix(ramp, 1.0 - exp2(-wetLinear * wetLinear * 1.442695), uUnderwaterFactor);\n }\n\n // Where the medium begins along the ray: uFogNear, the linear mode's slot, which this mode\n // never read before Atmosphere.fogStart. The ray is integrated from there on, at the density\n // of that point; at a start of 0 every term is the one it always was. See render/fog.ts.\n float start = min(uFogNear, dist);\n float before = dist > 0.0 ? start / dist : 0.0;\n float t = (pointY - uFogEyeY) * uFogHeightFalloff;\n float rest = t * (1.0 - before);\n // (1 - e^-t)/t, which tends to 1 as the ray levels out. Guarded rather than\n // branched: at t = 0 the quotient is 0/0, and 1e-4 of a scale height is 8 mm.\n float denom = abs(rest) < 1e-4 ? 1e-4 : rest;\n float air =\n 1.0 - exp(-uFogDensity * exp(-t * before) * (dist - start) * (1.0 - exp(-denom)) / denom);\n if (uUnderwaterFactor <= 0.0) return air;\n // Water is its own medium and keeps its own curve, so crossing the surface\n // changes nothing about how the water column reads.\n float wet = uUnderwaterFogDensity * dist;\n return mix(air, 1.0 - exp2(-wet * wet * 1.442695), uUnderwaterFactor);\n}\n\nuniform vec3 uCameraPos;\n\nout vec4 outColor;\n\nfloat hash11(float p) {\n return fract(sin(p * 12.9898) * 43758.5453123);\n}\n\nvoid main() {\n /*\n * Across the filament: 1 at the centre line, 0 at the quad's edge. Two powers\n * of it are the entire look \u2014 a very tight one for the core and a loose one for\n * the glow. A single falloff gives either a hard line with no atmosphere or a\n * soft smudge with no filament.\n */\n /*\n * Clamped, because a multisampled target shades a pixel on the quad's edge at its centre, which\n * may lie outside the quad: there 'vSide' runs past one, 'across' goes negative, and 'pow' of a\n * negative number is NaN. One NaN pixel is a dark speck; bloom then spreads it into a white disc\n * the size of the blur. Found by a fight drawing short arcs at 4 samples.\n */\n float across = clamp(1.0 - abs(vSide), 0.0, 1.0);\n float core = pow(across, 10.0);\n /*\n * A *wide* halo, deliberately. This exponent is the single biggest control over\n * whether the effect reads as lightning or as a drawn line: a tight falloff gives a\n * crisp filament with nothing around it, which is a wire, while a broad one bleeds\n * light into the air the way an over-exposed discharge does on film. It is the\n * closest thing to bloom available without a second pass.\n */\n float glow = pow(across, 1.1);\n\n /*\n * Stutter. An arc is not a steady light: it is re-established many times a\n * second, and the eye reads a *constant* bright line as neon. Two frequencies\n * so the pattern does not read as a sine, and per-arc phase from the seed so\n * several arcs never blink together.\n */\n /*\n * Stutter, but shallow. An arc is re-established many times a second and a perfectly\n * steady bright line reads as neon \u2014 but too *much* flicker reads as a broken effect\n * rather than as a discharge, and at the brief lifetime these now have, deep flicker\n * mostly hides the bolt. The path itself is already being redrawn (see 'BoltPool'),\n * which carries most of the crackle; this only keeps the brightness alive.\n */\n float t = uTime * 47.0 + vSeed * 17.0;\n float stutter = 0.78 + 0.22 * (hash11(floor(t)) * 0.6 + hash11(floor(t * 2.37)) * 0.4);\n\n /*\n * The tip tapers. An arc that ends in a squared-off quad reads as a drawn\n * segment; thinning toward the far end is what makes it read as a discharge\n * dissipating into the air.\n */\n float taper = 1.0 - vAlong * 0.55;\n\n float energy = vFade * stutter * taper * vGain;\n vec3 lit = uEdgeColor * glow + uCoreColor * core * uCoreGain;\n\n /*\n * Distance dims it rather than tinting it toward the fog. A discharge emits;\n * mixing it into the medium's colour is what a *surface* does, and doing that\n * here turned a far bolt grey instead of faint.\n */\n float fog = mediumFog(distance(vWorldPos, uCameraPos), vWorldPos.y);\n /*\n * Alpha is 1, and the energy is already in the colour.\n *\n * The additive blend is 'SRC_ALPHA, ONE', so what reaches the framebuffer is\n * 'rgb * a'. Putting the energy in both \u2014 which the first version did \u2014 squares it,\n * and squaring a number below one is how an effect authored as blinding arrives as\n * faint: at a typical energy of 0.5 it delivered a quarter of what it asked for.\n * Every emissive material here has to pick one channel and mean it.\n */\n outColor = vec4(lit * energy * (1.0 - fog), 1.0);\n}\n";
|
|
@@ -104,7 +104,13 @@ void main() {
|
|
|
104
104
|
* the glow. A single falloff gives either a hard line with no atmosphere or a
|
|
105
105
|
* soft smudge with no filament.
|
|
106
106
|
*/
|
|
107
|
-
|
|
107
|
+
/*
|
|
108
|
+
* Clamped, because a multisampled target shades a pixel on the quad's edge at its centre, which
|
|
109
|
+
* may lie outside the quad: there 'vSide' runs past one, 'across' goes negative, and 'pow' of a
|
|
110
|
+
* negative number is NaN. One NaN pixel is a dark speck; bloom then spreads it into a white disc
|
|
111
|
+
* the size of the blur. Found by a fight drawing short arcs at 4 samples.
|
|
112
|
+
*/
|
|
113
|
+
float across = clamp(1.0 - abs(vSide), 0.0, 1.0);
|
|
108
114
|
float core = pow(across, 10.0);
|
|
109
115
|
/*
|
|
110
116
|
* A *wide* halo, deliberately. This exponent is the single biggest control over
|
|
@@ -35,4 +35,4 @@
|
|
|
35
35
|
* ordinary one, and it wants the sheet laid just clear of the tiles.
|
|
36
36
|
*/
|
|
37
37
|
export declare const CAUSTICS_VERT = "#version 300 es\nlayout(location = 0) in vec3 aRest;\nlayout(location = 1) in vec2 aLocal;\n/** x: resting height of the water below. y: spare. */\nlayout(location = 2) in vec2 aParams;\n\nuniform mat4 uViewProj;\n\nout vec3 vWorldPos;\nout vec2 vLocal;\nout vec2 vParams;\n\nvoid main() {\n vWorldPos = aRest;\n vLocal = aLocal;\n vParams = aParams;\n gl_Position = uViewProj * vec4(aRest, 1.0);\n}\n";
|
|
38
|
-
export declare const CAUSTICS_FRAG = "#version 300 es\nprecision highp float;\n\nin vec3 vWorldPos;\nin vec2 vLocal;\nin vec2 vParams;\n\nuniform vec3 uCameraPos;\nuniform vec2 uWindDir;\nuniform float uWaveGain;\nuniform float uTime;\n/** Toward the dominant source, as everywhere else in the engine. */\nuniform vec3 uLightDir;\n/** Colour of the light the water is reflecting. */\nuniform vec3 uTint;\nuniform float uStrength;\n/** How far from the water, on either side, the effect has died out entirely. */\nuniform float uMaxDrop;\n\nuniform vec3 uFogColor;\n/** Extinction per metre **at the camera's own height**, not at the base. */\nuniform float uFogDensity;\n/** Reciprocal scale height of the haze, per metre. 0 is a uniform medium. */\nuniform float uFogHeightFalloff;\n/** The camera's height, so a fragment's own height is one subtraction away. */\nuniform float uFogEyeY;\nuniform vec3 uUnderwaterColor;\nuniform float uUnderwaterFogDensity;\nuniform float uUnderwaterFactor;\n/** 0 integrates a medium (Beer-Lambert); 1 ramps linearly between near and far. */\nuniform int uFogMode;\n/** Linear mode only: where haze begins, and where it is total. */\nuniform float uFogNear;\nuniform float uFogFar;\n\n/** What distance converges toward: the day's haze, or the water column. */\nvec3 mediumColor() {\n return mix(uFogColor, uUnderwaterColor, uUnderwaterFactor);\n}\n\n/**\n * How much of a surface standing at height pointY, dist metres away, is lost to\n * the medium in between. 0 is perfectly clear, 1 is the medium's own colour.\n */\nfloat mediumFog(float dist, float pointY) {\n /*\n * A depth ramp rather than a medium, for worlds authored against one.\n *\n * Beer-Lambert is the honest model and stays the default: it integrates something\n * physically there, so extinction begins at the eye and never stops. A linear near/far\n * fog is not that \u2014 it is a *look*, with a hard start distance before which the air is\n * perfectly clear, and it is what the fixed-function pipeline offered and what several\n * renderers still expose. Approximating one with the other is not a matter of tuning:\n * matched at half occlusion, the exponential curve still lays haze over everything\n * inside near, where the ramp lays none, and near-field colour desaturates toward the\n * fog colour with nothing to show for it.\n *\n * Height falloff has no meaning here and is ignored: a ramp is defined on distance\n * alone.\n */\n if (uFogMode == 1) {\n float span = max(uFogFar - uFogNear, 1e-4);\n float ramp = clamp((dist - uFogNear) / span, 0.0, 1.0);\n if (uUnderwaterFactor <= 0.0) return ramp;\n float wetLinear = uUnderwaterFogDensity * dist;\n return mix(ramp, 1.0 - exp2(-wetLinear * wetLinear * 1.442695), uUnderwaterFactor);\n }\n\n float t = (pointY - uFogEyeY) * uFogHeightFalloff;\n // (1 - e^-t)/t, which tends to 1 as the ray levels out. Guarded rather than\n // branched: at t = 0 the quotient is 0/0, and 1e-4 of a scale height is 8 mm.\n float denom = abs(t) < 1e-4 ? 1e-4 : t;\n float air = 1.0 - exp(-uFogDensity * dist * (1.0 - exp(-denom)) / denom);\n if (uUnderwaterFactor <= 0.0) return air;\n // Water is its own medium and keeps its own curve, so crossing the surface\n // changes nothing about how the water column reads.\n float wet = uUnderwaterFogDensity * dist;\n return mix(air, 1.0 - exp2(-wet * wet * 1.442695), uUnderwaterFactor);\n}\n\n\nout vec4 outColor;\n\n\nconst int GERSTNER_COUNT = 4;\n/*\n * direction.xy, steepness, wavelength. Amplitude is steepness/k, so these sum to\n * roughly a 1m swell \u2014 enough to read as a living surface, low enough that crests\n * never wash over an island whose deck sits ~1.8m above sea level.\n */\nconst vec4 GERSTNER_WAVES[4] = vec4[4](\n vec4( 1.0, 0.0, 0.115, 34.0),\n vec4( 0.6, 0.8, 0.100, 18.0),\n vec4(-0.7, 0.7, 0.080, 9.0),\n vec4( 0.2, -0.98, 0.060, 5.0)\n);\n\n/*\n * Steer each wave toward the wind, but only partway. A real sea is a dominant\n * swell plus cross-chop; rotating all four onto one bearing gives corduroy,\n * which reads as a material rather than as water.\n */\nvec2 gerstnerDirection(int i, vec2 windDir) {\n return normalize(mix(normalize(GERSTNER_WAVES[i].xy), windDir, 0.62));\n}\n\nfloat gerstnerWavenumber(int i) {\n return 6.28318530718 / GERSTNER_WAVES[i].w;\n}\n\n/** Deep-water phase in radians at a point on the resting plane. */\nfloat gerstnerPhase(float k, vec2 dir, vec2 p, float time) {\n float c = sqrt(9.81 / k); // deep-water phase speed\n return k * (dot(dir, p) - c * time);\n}\n\nstruct GerstnerSurface {\n /** Displacement from the resting plane \u2014 horizontal as well as vertical. */\n vec3 offset;\n vec3 normal;\n /** Height in units of steepness; positive near a crest. */\n float crest;\n};\n\nGerstnerSurface gerstnerSurface(vec2 p, float time, vec2 windDir, float gain) {\n GerstnerSurface s;\n s.offset = vec3(0.0);\n s.normal = vec3(0.0, 1.0, 0.0);\n s.crest = 0.0;\n for (int i = 0; i < GERSTNER_COUNT; i++) {\n vec2 dir = gerstnerDirection(i, windDir);\n float steepness = GERSTNER_WAVES[i].z * gain;\n float k = gerstnerWavenumber(i);\n float f = gerstnerPhase(k, dir, p, time);\n float a = steepness / k; // amplitude from steepness\n\n s.offset.x += dir.x * a * cos(f);\n s.offset.y += a * sin(f);\n s.offset.z += dir.y * a * cos(f);\n\n // Analytic normal: the partial derivatives of the displacement above.\n s.normal.x -= dir.x * steepness * cos(f);\n s.normal.z -= dir.y * steepness * cos(f);\n s.normal.y -= steepness * sin(f);\n\n s.crest += sin(f) * steepness;\n }\n return s;\n}\n\n/**\n * Second derivatives of the height field: (d2h/dx2, d2h/dz2, d2h/dxdz).\n *\n * How the surface *bends*, which is what decides where reflected light\n * converges. Analytic from the same sum, so it cannot disagree with the shape\n * the vertex stage displaced.\n */\nvec3 gerstnerCurvature(vec2 p, float time, vec2 windDir, float gain) {\n vec3 h = vec3(0.0);\n for (int i = 0; i < GERSTNER_COUNT; i++) {\n vec2 dir = gerstnerDirection(i, windDir);\n float steepness = GERSTNER_WAVES[i].z * gain;\n float k = gerstnerWavenumber(i);\n float f = gerstnerPhase(k, dir, p, time);\n // Second derivative of (steepness/k)*sin(f) along the wave's own bearing.\n float second = -steepness * k * sin(f);\n h.x += second * dir.x * dir.x;\n h.y += second * dir.y * dir.y;\n h.z += second * dir.x * dir.y;\n }\n return h;\n}\n\n\n/**\n * Shallowest the source may be and still be treated as a source.\n *\n * The slant path goes as 1/sin(elevation), so a light on the horizon would send\n * the sampled water position kilometres away and the pattern would smear into\n * noise. Clamping keeps a sunset dramatic instead of broken.\n */\nconst float MIN_ELEVATION = 0.16;\n\nvoid main() {\n float waterY = vParams.x;\n float drop = vWorldPos.y - waterY;\n\n /*\n * Everything is masked rather than branched on, and fwidth() below is why:\n * screen-space derivatives are undefined in non-uniform control flow, so a\n * discard above them would make the antialiasing of the caustic lines a\n * driver-dependent guess. Nothing is saved by branching either \u2014 a masked\n * fragment adds zero under additive blending.\n *\n * Either side of the surface is lit, and the distance from it is what matters\n * rather than the direction. This began as a soffit-only effect \u2014 light thrown\n * *up* onto the underside of a bridge \u2014 on the reasoning that below the water\n * there is nothing to light. That is only true when the water is a ceiling to\n * you. A pool floor, a flooded corridor, a tank with a lamp over it: the water\n * focuses light onto the bottom exactly as it does onto a soffit, and it is by\n * far the more common of the two.\n *\n * Only the surface's own thin band is excluded, where the sheet and the water\n * are coplanar and the pattern would be meaningless.\n *\n * Too far from it and the net has spread into nothing. And the sheet dissolves\n * at its own boundary, so a lit surface does not end in a lit rectangle.\n */\n float distanceFromWater = abs(drop);\n float clear = step(0.02, distanceFromWater);\n float height = 1.0 - smoothstep(uMaxDrop * 0.45, uMaxDrop, distanceFromWater);\n float rim = min(min(vLocal.x, 1.0 - vLocal.x), min(vLocal.y, 1.0 - vLocal.y));\n float edge = smoothstep(0.0, 0.12, rim);\n float reach = clear * height * edge;\n\n /*\n * Which water lights *this* point. For a flat surface the reflected ray leaves\n * along the mirrored light direction, so walking back from the ceiling gives\n * the water position and the path length in one step. This is why the net\n * slides along the ground as the sun moves rather than sitting under itself.\n */\n float elevation = max(uLightDir.y, MIN_ELEVATION);\n float slant = 1.0 / elevation;\n float d = drop * slant;\n vec2 water = vWorldPos.xz + uLightDir.xz * d;\n\n vec3 h = gerstnerCurvature(water, uTime, uWindDir, uWaveGain);\n float a = 1.0 - 2.0 * d * h.x;\n float b = 1.0 - 2.0 * d * h.y;\n float c = 2.0 * d * h.z;\n float det = a * b - c * c;\n\n /*\n * The bright net is where the determinant crosses zero, so its width is a\n * gradient question, not a constant. Widening the core by the screen-space\n * derivative antialiases it for free \u2014 without that, a line thinner than a\n * pixel strobes as the camera moves, which is the usual reason an effect like\n * this ends up looking like noise.\n */\n float soft = max(0.05, fwidth(det) * 1.6);\n float lines = soft / max(abs(det), soft);\n /*\n * Between the lines, the faint brightening of merely convergent water \u2014 and it has to\n * stay faint. At 0.45 it lit the *whole* sheet, and since the sheet hangs a few\n * centimetres under the soffit, the result read as a pale plate floating below the\n * bridge rather than as light on its underside \u2014 reported as being able to see through\n * the bridge ceiling, which looked glassy and transparent. The plate was the sheet.\n *\n * A tighter threshold and a fifth of the weight: the net is bright, everything between\n * it is almost nothing, and a soffit with no caustics on it is simply a soffit.\n */\n float sheen = clamp(1.0 / max(abs(det), 0.18) - 0.85, 0.0, 1.0);\n float caustic = clamp(lines * 0.9 + sheen * 0.09, 0.0, 1.6);\n\n float fog = mediumFog(distance(vWorldPos, uCameraPos), vWorldPos.y);\n\n // Additive: this is light arriving at a surface that is already shaded.\n outColor = vec4(uTint * (caustic * uStrength * reach * (1.0 - fog)), 1.0);\n}\n";
|
|
38
|
+
export declare const CAUSTICS_FRAG = "#version 300 es\nprecision highp float;\n\nin vec3 vWorldPos;\nin vec2 vLocal;\nin vec2 vParams;\n\nuniform vec3 uCameraPos;\nuniform vec2 uWindDir;\nuniform float uWaveGain;\nuniform float uTime;\n/** Toward the dominant source, as everywhere else in the engine. */\nuniform vec3 uLightDir;\n/** Colour of the light the water is reflecting. */\nuniform vec3 uTint;\nuniform float uStrength;\n/** How far from the water, on either side, the effect has died out entirely. */\nuniform float uMaxDrop;\n\nuniform vec3 uFogColor;\n/** Extinction per metre **at the camera's own height**, not at the base. */\nuniform float uFogDensity;\n/** Reciprocal scale height of the haze, per metre. 0 is a uniform medium. */\nuniform float uFogHeightFalloff;\n/** The camera's height, so a fragment's own height is one subtraction away. */\nuniform float uFogEyeY;\nuniform vec3 uUnderwaterColor;\nuniform float uUnderwaterFogDensity;\nuniform float uUnderwaterFactor;\n/** 0 integrates a medium (Beer-Lambert); 1 ramps linearly between near and far. */\nuniform int uFogMode;\n/** Linear mode only: where haze begins, and where it is total. */\nuniform float uFogNear;\nuniform float uFogFar;\n\n/** What distance converges toward: the day's haze, or the water column. */\nvec3 mediumColor() {\n return mix(uFogColor, uUnderwaterColor, uUnderwaterFactor);\n}\n\n/**\n * How much of a surface standing at height pointY, dist metres away, is lost to\n * the medium in between. 0 is perfectly clear, 1 is the medium's own colour.\n */\nfloat mediumFog(float dist, float pointY) {\n /*\n * A depth ramp rather than a medium, for worlds authored against one.\n *\n * Beer-Lambert is the honest model and stays the default: it integrates something\n * physically there, so extinction begins at the eye and never stops. A linear near/far\n * fog is not that \u2014 it is a *look*, with a hard start distance before which the air is\n * perfectly clear, and it is what the fixed-function pipeline offered and what several\n * renderers still expose. Approximating one with the other is not a matter of tuning:\n * matched at half occlusion, the exponential curve still lays haze over everything\n * inside near, where the ramp lays none, and near-field colour desaturates toward the\n * fog colour with nothing to show for it.\n *\n * Height falloff has no meaning here and is ignored: a ramp is defined on distance\n * alone.\n */\n if (uFogMode == 1) {\n float span = max(uFogFar - uFogNear, 1e-4);\n float ramp = clamp((dist - uFogNear) / span, 0.0, 1.0);\n if (uUnderwaterFactor <= 0.0) return ramp;\n float wetLinear = uUnderwaterFogDensity * dist;\n return mix(ramp, 1.0 - exp2(-wetLinear * wetLinear * 1.442695), uUnderwaterFactor);\n }\n\n // Where the medium begins along the ray: uFogNear, the linear mode's slot, which this mode\n // never read before Atmosphere.fogStart. The ray is integrated from there on, at the density\n // of that point; at a start of 0 every term is the one it always was. See render/fog.ts.\n float start = min(uFogNear, dist);\n float before = dist > 0.0 ? start / dist : 0.0;\n float t = (pointY - uFogEyeY) * uFogHeightFalloff;\n float rest = t * (1.0 - before);\n // (1 - e^-t)/t, which tends to 1 as the ray levels out. Guarded rather than\n // branched: at t = 0 the quotient is 0/0, and 1e-4 of a scale height is 8 mm.\n float denom = abs(rest) < 1e-4 ? 1e-4 : rest;\n float air =\n 1.0 - exp(-uFogDensity * exp(-t * before) * (dist - start) * (1.0 - exp(-denom)) / denom);\n if (uUnderwaterFactor <= 0.0) return air;\n // Water is its own medium and keeps its own curve, so crossing the surface\n // changes nothing about how the water column reads.\n float wet = uUnderwaterFogDensity * dist;\n return mix(air, 1.0 - exp2(-wet * wet * 1.442695), uUnderwaterFactor);\n}\n\n\nout vec4 outColor;\n\n\nconst int GERSTNER_COUNT = 4;\n/*\n * direction.xy, steepness, wavelength. Amplitude is steepness/k, so these sum to\n * roughly a 1m swell \u2014 enough to read as a living surface, low enough that crests\n * never wash over an island whose deck sits ~1.8m above sea level.\n */\nconst vec4 GERSTNER_WAVES[4] = vec4[4](\n vec4( 1.0, 0.0, 0.115, 34.0),\n vec4( 0.6, 0.8, 0.100, 18.0),\n vec4(-0.7, 0.7, 0.080, 9.0),\n vec4( 0.2, -0.98, 0.060, 5.0)\n);\n\n/*\n * Steer each wave toward the wind, but only partway. A real sea is a dominant\n * swell plus cross-chop; rotating all four onto one bearing gives corduroy,\n * which reads as a material rather than as water.\n */\nvec2 gerstnerDirection(int i, vec2 windDir) {\n return normalize(mix(normalize(GERSTNER_WAVES[i].xy), windDir, 0.62));\n}\n\nfloat gerstnerWavenumber(int i) {\n return 6.28318530718 / GERSTNER_WAVES[i].w;\n}\n\n/** Deep-water phase in radians at a point on the resting plane. */\nfloat gerstnerPhase(float k, vec2 dir, vec2 p, float time) {\n float c = sqrt(9.81 / k); // deep-water phase speed\n return k * (dot(dir, p) - c * time);\n}\n\nstruct GerstnerSurface {\n /** Displacement from the resting plane \u2014 horizontal as well as vertical. */\n vec3 offset;\n vec3 normal;\n /** Height in units of steepness; positive near a crest. */\n float crest;\n};\n\nGerstnerSurface gerstnerSurface(vec2 p, float time, vec2 windDir, float gain) {\n GerstnerSurface s;\n s.offset = vec3(0.0);\n s.normal = vec3(0.0, 1.0, 0.0);\n s.crest = 0.0;\n for (int i = 0; i < GERSTNER_COUNT; i++) {\n vec2 dir = gerstnerDirection(i, windDir);\n float steepness = GERSTNER_WAVES[i].z * gain;\n float k = gerstnerWavenumber(i);\n float f = gerstnerPhase(k, dir, p, time);\n float a = steepness / k; // amplitude from steepness\n\n s.offset.x += dir.x * a * cos(f);\n s.offset.y += a * sin(f);\n s.offset.z += dir.y * a * cos(f);\n\n // Analytic normal: the partial derivatives of the displacement above.\n s.normal.x -= dir.x * steepness * cos(f);\n s.normal.z -= dir.y * steepness * cos(f);\n s.normal.y -= steepness * sin(f);\n\n s.crest += sin(f) * steepness;\n }\n return s;\n}\n\n/**\n * Second derivatives of the height field: (d2h/dx2, d2h/dz2, d2h/dxdz).\n *\n * How the surface *bends*, which is what decides where reflected light\n * converges. Analytic from the same sum, so it cannot disagree with the shape\n * the vertex stage displaced.\n */\nvec3 gerstnerCurvature(vec2 p, float time, vec2 windDir, float gain) {\n vec3 h = vec3(0.0);\n for (int i = 0; i < GERSTNER_COUNT; i++) {\n vec2 dir = gerstnerDirection(i, windDir);\n float steepness = GERSTNER_WAVES[i].z * gain;\n float k = gerstnerWavenumber(i);\n float f = gerstnerPhase(k, dir, p, time);\n // Second derivative of (steepness/k)*sin(f) along the wave's own bearing.\n float second = -steepness * k * sin(f);\n h.x += second * dir.x * dir.x;\n h.y += second * dir.y * dir.y;\n h.z += second * dir.x * dir.y;\n }\n return h;\n}\n\n\n/**\n * Shallowest the source may be and still be treated as a source.\n *\n * The slant path goes as 1/sin(elevation), so a light on the horizon would send\n * the sampled water position kilometres away and the pattern would smear into\n * noise. Clamping keeps a sunset dramatic instead of broken.\n */\nconst float MIN_ELEVATION = 0.16;\n\nvoid main() {\n float waterY = vParams.x;\n float drop = vWorldPos.y - waterY;\n\n /*\n * Everything is masked rather than branched on, and fwidth() below is why:\n * screen-space derivatives are undefined in non-uniform control flow, so a\n * discard above them would make the antialiasing of the caustic lines a\n * driver-dependent guess. Nothing is saved by branching either \u2014 a masked\n * fragment adds zero under additive blending.\n *\n * Either side of the surface is lit, and the distance from it is what matters\n * rather than the direction. This began as a soffit-only effect \u2014 light thrown\n * *up* onto the underside of a bridge \u2014 on the reasoning that below the water\n * there is nothing to light. That is only true when the water is a ceiling to\n * you. A pool floor, a flooded corridor, a tank with a lamp over it: the water\n * focuses light onto the bottom exactly as it does onto a soffit, and it is by\n * far the more common of the two.\n *\n * Only the surface's own thin band is excluded, where the sheet and the water\n * are coplanar and the pattern would be meaningless.\n *\n * Too far from it and the net has spread into nothing. And the sheet dissolves\n * at its own boundary, so a lit surface does not end in a lit rectangle.\n */\n float distanceFromWater = abs(drop);\n float clear = step(0.02, distanceFromWater);\n float height = 1.0 - smoothstep(uMaxDrop * 0.45, uMaxDrop, distanceFromWater);\n float rim = min(min(vLocal.x, 1.0 - vLocal.x), min(vLocal.y, 1.0 - vLocal.y));\n float edge = smoothstep(0.0, 0.12, rim);\n float reach = clear * height * edge;\n\n /*\n * Which water lights *this* point. For a flat surface the reflected ray leaves\n * along the mirrored light direction, so walking back from the ceiling gives\n * the water position and the path length in one step. This is why the net\n * slides along the ground as the sun moves rather than sitting under itself.\n */\n float elevation = max(uLightDir.y, MIN_ELEVATION);\n float slant = 1.0 / elevation;\n float d = drop * slant;\n vec2 water = vWorldPos.xz + uLightDir.xz * d;\n\n vec3 h = gerstnerCurvature(water, uTime, uWindDir, uWaveGain);\n float a = 1.0 - 2.0 * d * h.x;\n float b = 1.0 - 2.0 * d * h.y;\n float c = 2.0 * d * h.z;\n float det = a * b - c * c;\n\n /*\n * The bright net is where the determinant crosses zero, so its width is a\n * gradient question, not a constant. Widening the core by the screen-space\n * derivative antialiases it for free \u2014 without that, a line thinner than a\n * pixel strobes as the camera moves, which is the usual reason an effect like\n * this ends up looking like noise.\n */\n float soft = max(0.05, fwidth(det) * 1.6);\n float lines = soft / max(abs(det), soft);\n /*\n * Between the lines, the faint brightening of merely convergent water \u2014 and it has to\n * stay faint. At 0.45 it lit the *whole* sheet, and since the sheet hangs a few\n * centimetres under the soffit, the result read as a pale plate floating below the\n * bridge rather than as light on its underside \u2014 reported as being able to see through\n * the bridge ceiling, which looked glassy and transparent. The plate was the sheet.\n *\n * A tighter threshold and a fifth of the weight: the net is bright, everything between\n * it is almost nothing, and a soffit with no caustics on it is simply a soffit.\n */\n float sheen = clamp(1.0 / max(abs(det), 0.18) - 0.85, 0.0, 1.0);\n float caustic = clamp(lines * 0.9 + sheen * 0.09, 0.0, 1.6);\n\n float fog = mediumFog(distance(vWorldPos, uCameraPos), vWorldPos.y);\n\n // Additive: this is light arriving at a surface that is already shaded.\n outColor = vec4(uTint * (caustic * uStrength * reach * (1.0 - fog)), 1.0);\n}\n";
|
|
@@ -100,6 +100,12 @@ void main() {
|
|
|
100
100
|
#endif
|
|
101
101
|
#if INSTANCED
|
|
102
102
|
mat4 model = mat4(aInstanceModel0, aInstanceModel1, aInstanceModel2, aInstanceModel3);
|
|
103
|
+
/* The bottom row is the instance's texture cell, rebuilt as the row it stands for: see flat. */
|
|
104
|
+
vec4 instanceCell = vec4(model[0].w, model[1].w, model[2].w, model[3].w);
|
|
105
|
+
model[0].w = 0.0;
|
|
106
|
+
model[1].w = 0.0;
|
|
107
|
+
model[2].w = 0.0;
|
|
108
|
+
model[3].w = 1.0;
|
|
103
109
|
#else
|
|
104
110
|
mat4 model = uModel;
|
|
105
111
|
#endif
|
|
@@ -124,7 +130,12 @@ void main() {
|
|
|
124
130
|
vLightPosition = uLightViewProj * vec4(bent, world.w);
|
|
125
131
|
gl_Position = vLightPosition;
|
|
126
132
|
#if CUTOUT
|
|
133
|
+
#if INSTANCED
|
|
134
|
+
/* A cutout instance's shadow is cut by its own cell, as its picture is. */
|
|
135
|
+
vUv = vec3((aUv.xy * instanceCell.xy + instanceCell.zw) * uUvScale + uUvOffset, aUv.z);
|
|
136
|
+
#else
|
|
127
137
|
vUv = vec3(aUv.xy * uUvScale + uUvOffset, aUv.z);
|
|
138
|
+
#endif
|
|
128
139
|
vAlphaCutout = uAlphaCutout;
|
|
129
140
|
#endif
|
|
130
141
|
#if GLASS
|
|
@@ -12,4 +12,4 @@
|
|
|
12
12
|
* rainbow is generated rather than sampled.
|
|
13
13
|
*/
|
|
14
14
|
export declare const FILM_VERT = "#version 300 es\nprecision highp float;\n\nlayout(location = 0) in vec3 aPosition;\nlayout(location = 1) in vec3 aNormal;\nlayout(location = 2) in vec3 aColor;\n/*\n * Coverage, on the attribute the mesh format calls emissive.\n *\n * A film has no use for self-illumination, and it very much needs an edge that\n * dissolves: the first oil was flat squares, and a hard straight edge is what made\n * it read as a decal dropped on the world rather than a puddle lying in it. One\n * float per vertex, 1 in the middle of a slick and 0 at its rim.\n */\nlayout(location = 3) in float aCoverage;\n\nuniform mat4 uViewProj;\n/* The mirrored camera's projection, so a film can sample the planar target the way water does. */\nuniform mat4 uReflectionViewProj;\n\nout vec3 vWorld;\nout vec3 vNormal;\nout vec3 vTint;\nout vec4 vReflectionClip;\nout float vCoverage;\n\nvoid main() {\n vWorld = aPosition;\n vNormal = normalize(aNormal);\n vTint = aColor;\n vCoverage = aCoverage;\n vReflectionClip = uReflectionViewProj * vec4(aPosition, 1.0);\n gl_Position = uViewProj * vec4(aPosition, 1.0);\n}\n";
|
|
15
|
-
export declare const FILM_FRAG = "#version 300 es\nprecision highp float;\n\nin vec3 vWorld;\nin vec3 vNormal;\nin vec3 vTint;\nin vec4 vReflectionClip;\n/*\n * The scene mirrored across the plane this film lies on, and how much of it to show.\n *\n * **Sheen is iridescence and this is the image**, which is the split a consumer asked for after\n * finding that raising sheen to make a road wetter only made it oilier. A wet street at night is\n * defined by everything above it appearing twice, and no amount of interference pattern is that.\n *\n * The projection is the water shader's, unchanged and deliberately so: reproducing it outside the\n * engine is the piece most likely to be subtly wrong and least likely to be noticed, since a\n * mirror off by a small skew looks like a wet road until somebody puts a straight line across it.\n */\nuniform sampler2D uReflectionMap;\nuniform int uReflectionEnabled;\nuniform float uReflectionStrength;\nin float vCoverage;\n\nuniform vec3 uCameraPos;\nuniform float uTime;\n/*\n * The mirrored pass clips at the water plane, and it does so **in the fragment\n * shader** rather than with gl_ClipDistance.\n *\n * That is not a style choice. Clip distances in WebGL2 need\n * EXT_clip_cull_distance both enabled on the context and declared with an\n * #extension line, and without the declaration the shader fails to *compile* \u2014\n * which, since programs are built when the renderer is constructed, takes the\n * whole boot with it and leaves a black screen. This shader had never been\n * compiled when it was written, so the mistake sat harmlessly in the file until\n * the day it was wired to something. Discarding is what flat.ts does, it needs\n * no extension, and a film is a thin translucent layer where the cost of a\n * discarded fragment is nothing.\n */\nuniform vec4 uClipPlane;\nuniform int uClipEnabled;\n\nuniform vec3 uFogColor;\n/** Extinction per metre **at the camera's own height**, not at the base. */\nuniform float uFogDensity;\n/** Reciprocal scale height of the haze, per metre. 0 is a uniform medium. */\nuniform float uFogHeightFalloff;\n/** The camera's height, so a fragment's own height is one subtraction away. */\nuniform float uFogEyeY;\nuniform vec3 uUnderwaterColor;\nuniform float uUnderwaterFogDensity;\nuniform float uUnderwaterFactor;\n/** 0 integrates a medium (Beer-Lambert); 1 ramps linearly between near and far. */\nuniform int uFogMode;\n/** Linear mode only: where haze begins, and where it is total. */\nuniform float uFogNear;\nuniform float uFogFar;\n\n/** What distance converges toward: the day's haze, or the water column. */\nvec3 mediumColor() {\n return mix(uFogColor, uUnderwaterColor, uUnderwaterFactor);\n}\n\n/**\n * How much of a surface standing at height pointY, dist metres away, is lost to\n * the medium in between. 0 is perfectly clear, 1 is the medium's own colour.\n */\nfloat mediumFog(float dist, float pointY) {\n /*\n * A depth ramp rather than a medium, for worlds authored against one.\n *\n * Beer-Lambert is the honest model and stays the default: it integrates something\n * physically there, so extinction begins at the eye and never stops. A linear near/far\n * fog is not that \u2014 it is a *look*, with a hard start distance before which the air is\n * perfectly clear, and it is what the fixed-function pipeline offered and what several\n * renderers still expose. Approximating one with the other is not a matter of tuning:\n * matched at half occlusion, the exponential curve still lays haze over everything\n * inside near, where the ramp lays none, and near-field colour desaturates toward the\n * fog colour with nothing to show for it.\n *\n * Height falloff has no meaning here and is ignored: a ramp is defined on distance\n * alone.\n */\n if (uFogMode == 1) {\n float span = max(uFogFar - uFogNear, 1e-4);\n float ramp = clamp((dist - uFogNear) / span, 0.0, 1.0);\n if (uUnderwaterFactor <= 0.0) return ramp;\n float wetLinear = uUnderwaterFogDensity * dist;\n return mix(ramp, 1.0 - exp2(-wetLinear * wetLinear * 1.442695), uUnderwaterFactor);\n }\n\n float t = (pointY - uFogEyeY) * uFogHeightFalloff;\n // (1 - e^-t)/t, which tends to 1 as the ray levels out. Guarded rather than\n // branched: at t = 0 the quotient is 0/0, and 1e-4 of a scale height is 8 mm.\n float denom = abs(t) < 1e-4 ? 1e-4 : t;\n float air = 1.0 - exp(-uFogDensity * dist * (1.0 - exp(-denom)) / denom);\n if (uUnderwaterFactor <= 0.0) return air;\n // Water is its own medium and keeps its own curve, so crossing the surface\n // changes nothing about how the water column reads.\n float wet = uUnderwaterFogDensity * dist;\n return mix(air, 1.0 - exp2(-wet * wet * 1.442695), uUnderwaterFactor);\n}\n\n/** How strong the sheen is. Zero leaves a plain dark film. */\nuniform float uSheen;\n\n/**\n * How rough the film's own surface is, 0 to 1, and how coarse that roughness is.\n *\n * **A film had exactly one finish before this: mirror.** That is right for standing water and\n * wrong for the thing this pass is most used for, because a wet road is not a mirror \u2014 it is\n * water lying *in* aggregate, and the stone underneath breaks the reflection into a smear. With\n * no way to say so, a patch of film over a textured road met it at a hard rim: mirror on one\n * side, stone on the other, with nothing able to cross it. Feathering the coverage cannot fix\n * that, and it was tried \u2014 the alpha was never what the eye was reading.\n *\n * So the reflection is displaced by a noise field rather than blurred. **Displaced, because a\n * blur is the wrong phenomenon**: a rough surface does not soften what it reflects, it scatters\n * it, and a lamp on wet tarmac smears into a streak rather than becoming a soft disc. It is also\n * what a blur cannot afford here \u2014 the mirror has no mip chain (see the `textureLod` note\n * below), so softening it honestly would cost several taps where this costs none.\n *\n * `uFilmRoughnessCycles` is bumps to the metre, and it is the same quantity\n * `Renderer.setSurfaceRelief` takes. **Matching it to the surface underneath is the point**:\n * given the same field at the same scale, the water is broken up by the aggregate that the dry\n * road beside it is showing, and the two stop being different materials.\n *\n * Zero is the mirror this pass has always been, exactly, so nothing that does not ask for\n * roughness renders differently.\n */\nuniform float uFilmRoughness;\nuniform float uFilmRoughnessCycles;\n\nout vec4 fragColor;\n\n/** A hue wheel, without a texture. Standard 6-segment ramp. */\nvec3 hue(float h) {\n h = fract(h) * 6.0;\n vec3 c = clamp(vec3(abs(h - 3.0) - 1.0, 2.0 - abs(h - 2.0), 2.0 - abs(h - 4.0)), 0.0, 1.0);\n return c;\n}\n\n/**\n * The same value noise `flat.ts` builds its relief from, and deliberately the same.\n *\n * Copied rather than shared because these two shaders have no common include beyond the fog, and\n * a film that used a *different* field would defeat the whole purpose of taking a cycle count:\n * the water would be broken up by one pattern and the road by another, which is two materials\n * again with extra steps.\n */\nfloat filmHash(vec3 p) {\n p = fract(p * 0.1031);\n p += dot(p, p.zyx + 31.32);\n return fract((p.x + p.y) * p.z);\n}\n\nfloat filmNoise(vec3 p) {\n vec3 i = floor(p);\n vec3 f = fract(p);\n vec3 u = f * f * (3.0 - 2.0 * f);\n return mix(\n mix(\n mix(filmHash(i + vec3(0.0, 0.0, 0.0)), filmHash(i + vec3(1.0, 0.0, 0.0)), u.x),\n mix(filmHash(i + vec3(0.0, 1.0, 0.0)), filmHash(i + vec3(1.0, 1.0, 0.0)), u.x),\n u.y\n ),\n mix(\n mix(filmHash(i + vec3(0.0, 0.0, 1.0)), filmHash(i + vec3(1.0, 0.0, 1.0)), u.x),\n mix(filmHash(i + vec3(0.0, 1.0, 1.0)), filmHash(i + vec3(1.0, 1.0, 1.0)), u.x),\n u.y\n ),\n u.z\n );\n}\n\nvoid main() {\n if (uClipEnabled != 0 && dot(vec4(vWorld, 1.0), uClipPlane) < 0.0) discard;\n\n vec3 view = normalize(uCameraPos - vWorld);\n float facing = max(dot(normalize(vNormal), view), 0.0);\n /*\n * Fresnel-ish: dark looking straight down at it, bright at a grazing angle.\n * This is the whole effect \u2014 it is what makes a wet patch look wet rather\n * than looking like a patch of dark paint.\n */\n float grazing = pow(1.0 - facing, 3.0);\n\n /*\n * A slow travelling interference, so the sheen crawls rather than sitting\n * still. Two frequencies that do not divide each other, or the pattern\n * repeats visibly across a long slick.\n */\n float ripple =\n sin(vWorld.x * 0.73 + vWorld.z * 0.41 + uTime * 0.55) *\n cos(vWorld.z * 0.91 - vWorld.x * 0.29 - uTime * 0.37);\n\n vec3 sheen = hue(grazing * 1.35 + ripple * 0.14 + 0.55);\n /*\n * Dark underneath, so the film always reads as *less* than the surface it lies on even where\n * the sheen is strongest \u2014 but not so dark that it disappears.\n *\n * A slick that has just launched somebody has to be findable afterwards. It was rounded,\n * feathered and iridescent \u2014 and at a fifth of an opaque tint on a pale deck at dusk,\n * invisible. A hazard you cannot find is not a hazard, it is a trap.\n *\n * So the base is darker and the sheen has a floor: even looking straight down, where the\n * Fresnel term is nearly nothing, a slick is a distinctly wet patch rather than a faint one.\n */\n vec3 base = vTint * 0.9;\n vec3 color = base + sheen * uSheen * (0.22 + grazing * 0.75);\n\n /*\n * The mirrored scene, under the sheen rather than instead of it.\n *\n * Screen-space from the mirrored clip position, exactly as the water pass does it, with the\n * same fade as the sample leaves the target: a reflection that stopped dead at the edge of its\n * own buffer would draw a line across the road where nothing is.\n *\n * Weighted by the grazing term as well as by strength, because that is what a reflection does:\n * a wet surface underfoot shows almost nothing and the same surface down the street shows the\n * whole scene. Added before the fog so the image recedes with everything else in the frame.\n */\n if (uReflectionEnabled != 0 && uReflectionStrength > 0.0 && vReflectionClip.w > 0.0) {\n vec2 reflectionUv = vReflectionClip.xy / vReflectionClip.w * 0.5 + 0.5;\n vec2 border = min(reflectionUv, 1.0 - reflectionUv);\n float valid = smoothstep(0.0, 0.025, min(border.x, border.y));\n /*\n * An explicit level, not an implicit one, and the branch above is the whole reason.\n *\n * An implicit derivative inside control flow that is not provably uniform is what lets a\n * compiler flatten the branch and pay for every arm \u2014 AGENTS.md, 2026-08-07 \u2014 and WGSL\n * does not merely warn: the module fails to compile with \"textureSample must only be\n * called from uniform control flow\", which invalidates the pipeline, which invalidates\n * the command buffer, and the frame still presents. The fifth time on this branch. The\n * mirror has no mip chain, so level 0 is the only level there is; the water shader\n * reaches the same conclusion for the same lookup.\n */\n /*\n * The aggregate under the water, as a displacement of what the water shows.\n *\n * The gradient of the same field the road's relief uses, taken across the surface and used\n * to push the lookup. **Stretched along the reflection rather than isotropic**, and that is\n * the half that makes it read as a road: a reflection on a rough horizontal surface smears\n * *towards the viewer*, because a bump tilts the ray in the plane it is already travelling\n * in. Displacing in a circle gives frosted glass instead, which is a different wet thing.\n *\n * Scaled by the grazing term for the same reason the mirror is: looking straight down at a\n * puddle the reflection is compressed into almost nothing and a displacement of it would be\n * enormous, so the scatter has to fall off exactly where the image does.\n */\n if (uFilmRoughness > 0.0) {\n vec3 at = vWorld * uFilmRoughnessCycles;\n float here = filmNoise(at);\n vec2 slope = vec2(\n filmNoise(at + vec3(0.5, 0.0, 0.0)) - here,\n filmNoise(at + vec3(0.0, 0.0, 0.5)) - here\n );\n /* Along the view, which on a horizontal film is where the image runs. */\n vec2 along = normalize(vec2(view.x, view.z) + vec2(1e-5, 0.0));\n vec2 smear = along * dot(slope, along) * 3.0 + slope * 0.35;\n reflectionUv += smear * uFilmRoughness * 0.016 * (0.2 + grazing * 0.8);\n /* Re-fade at the border: a displaced sample can leave the target the original did not. */\n vec2 movedBorder = min(reflectionUv, 1.0 - reflectionUv);\n valid = min(valid, smoothstep(0.0, 0.025, min(movedBorder.x, movedBorder.y)));\n }\n vec3 mirrored = textureLod(uReflectionMap, clamp(reflectionUv, 0.0, 1.0), 0.0).rgb;\n color = mix(color, mirrored, valid * uReflectionStrength * (0.25 + grazing * 0.75));\n }\n\n // The same fog every other pass applies \u2014 and now literally the same code, which\n // it was not: this ran its own curve while claiming otherwise, so a slick receded\n // at a different rate than the deck it was lying on.\n float fog = mediumFog(length(uCameraPos - vWorld), vWorld.y);\n color = mix(color, mediumColor(), fog);\n\n /*\n * Alpha from coverage, squared.\n *\n * Squared because a linear ramp across the rim still reads as a visible band \u2014\n * the eye finds the constant-gradient edge. Squaring pushes the falloff outward\n * so the slick has a soft shoulder and no perceptible boundary. The sheen fades\n * with it, which is right: the thinnest part of a film is the least iridescent.\n */\n /*\n * Alpha from coverage, and the rim is what is squared \u2014 not the whole patch.\n *\n * Squaring the coverage outright made the *middle* translucent too, which is most of why a\n * slick could not be found. The shoulder still needs to be soft, so the square is mixed in\n * near the edge only: solid through the body, dissolving at the rim.\n */\n float coverage = clamp(vCoverage, 0.0, 1.0);\n float alpha = mix(coverage * coverage, coverage, 0.65) * 0.92;\n fragColor = vec4(color, alpha);\n}\n";
|
|
15
|
+
export declare const FILM_FRAG = "#version 300 es\nprecision highp float;\n\nin vec3 vWorld;\nin vec3 vNormal;\nin vec3 vTint;\nin vec4 vReflectionClip;\n/*\n * The scene mirrored across the plane this film lies on, and how much of it to show.\n *\n * **Sheen is iridescence and this is the image**, which is the split a consumer asked for after\n * finding that raising sheen to make a road wetter only made it oilier. A wet street at night is\n * defined by everything above it appearing twice, and no amount of interference pattern is that.\n *\n * The projection is the water shader's, unchanged and deliberately so: reproducing it outside the\n * engine is the piece most likely to be subtly wrong and least likely to be noticed, since a\n * mirror off by a small skew looks like a wet road until somebody puts a straight line across it.\n */\nuniform sampler2D uReflectionMap;\nuniform int uReflectionEnabled;\nuniform float uReflectionStrength;\nin float vCoverage;\n\nuniform vec3 uCameraPos;\nuniform float uTime;\n/*\n * The mirrored pass clips at the water plane, and it does so **in the fragment\n * shader** rather than with gl_ClipDistance.\n *\n * That is not a style choice. Clip distances in WebGL2 need\n * EXT_clip_cull_distance both enabled on the context and declared with an\n * #extension line, and without the declaration the shader fails to *compile* \u2014\n * which, since programs are built when the renderer is constructed, takes the\n * whole boot with it and leaves a black screen. This shader had never been\n * compiled when it was written, so the mistake sat harmlessly in the file until\n * the day it was wired to something. Discarding is what flat.ts does, it needs\n * no extension, and a film is a thin translucent layer where the cost of a\n * discarded fragment is nothing.\n */\nuniform vec4 uClipPlane;\nuniform int uClipEnabled;\n\nuniform vec3 uFogColor;\n/** Extinction per metre **at the camera's own height**, not at the base. */\nuniform float uFogDensity;\n/** Reciprocal scale height of the haze, per metre. 0 is a uniform medium. */\nuniform float uFogHeightFalloff;\n/** The camera's height, so a fragment's own height is one subtraction away. */\nuniform float uFogEyeY;\nuniform vec3 uUnderwaterColor;\nuniform float uUnderwaterFogDensity;\nuniform float uUnderwaterFactor;\n/** 0 integrates a medium (Beer-Lambert); 1 ramps linearly between near and far. */\nuniform int uFogMode;\n/** Linear mode only: where haze begins, and where it is total. */\nuniform float uFogNear;\nuniform float uFogFar;\n\n/** What distance converges toward: the day's haze, or the water column. */\nvec3 mediumColor() {\n return mix(uFogColor, uUnderwaterColor, uUnderwaterFactor);\n}\n\n/**\n * How much of a surface standing at height pointY, dist metres away, is lost to\n * the medium in between. 0 is perfectly clear, 1 is the medium's own colour.\n */\nfloat mediumFog(float dist, float pointY) {\n /*\n * A depth ramp rather than a medium, for worlds authored against one.\n *\n * Beer-Lambert is the honest model and stays the default: it integrates something\n * physically there, so extinction begins at the eye and never stops. A linear near/far\n * fog is not that \u2014 it is a *look*, with a hard start distance before which the air is\n * perfectly clear, and it is what the fixed-function pipeline offered and what several\n * renderers still expose. Approximating one with the other is not a matter of tuning:\n * matched at half occlusion, the exponential curve still lays haze over everything\n * inside near, where the ramp lays none, and near-field colour desaturates toward the\n * fog colour with nothing to show for it.\n *\n * Height falloff has no meaning here and is ignored: a ramp is defined on distance\n * alone.\n */\n if (uFogMode == 1) {\n float span = max(uFogFar - uFogNear, 1e-4);\n float ramp = clamp((dist - uFogNear) / span, 0.0, 1.0);\n if (uUnderwaterFactor <= 0.0) return ramp;\n float wetLinear = uUnderwaterFogDensity * dist;\n return mix(ramp, 1.0 - exp2(-wetLinear * wetLinear * 1.442695), uUnderwaterFactor);\n }\n\n // Where the medium begins along the ray: uFogNear, the linear mode's slot, which this mode\n // never read before Atmosphere.fogStart. The ray is integrated from there on, at the density\n // of that point; at a start of 0 every term is the one it always was. See render/fog.ts.\n float start = min(uFogNear, dist);\n float before = dist > 0.0 ? start / dist : 0.0;\n float t = (pointY - uFogEyeY) * uFogHeightFalloff;\n float rest = t * (1.0 - before);\n // (1 - e^-t)/t, which tends to 1 as the ray levels out. Guarded rather than\n // branched: at t = 0 the quotient is 0/0, and 1e-4 of a scale height is 8 mm.\n float denom = abs(rest) < 1e-4 ? 1e-4 : rest;\n float air =\n 1.0 - exp(-uFogDensity * exp(-t * before) * (dist - start) * (1.0 - exp(-denom)) / denom);\n if (uUnderwaterFactor <= 0.0) return air;\n // Water is its own medium and keeps its own curve, so crossing the surface\n // changes nothing about how the water column reads.\n float wet = uUnderwaterFogDensity * dist;\n return mix(air, 1.0 - exp2(-wet * wet * 1.442695), uUnderwaterFactor);\n}\n\n/** How strong the sheen is. Zero leaves a plain dark film. */\nuniform float uSheen;\n\n/**\n * How rough the film's own surface is, 0 to 1, and how coarse that roughness is.\n *\n * **A film had exactly one finish before this: mirror.** That is right for standing water and\n * wrong for the thing this pass is most used for, because a wet road is not a mirror \u2014 it is\n * water lying *in* aggregate, and the stone underneath breaks the reflection into a smear. With\n * no way to say so, a patch of film over a textured road met it at a hard rim: mirror on one\n * side, stone on the other, with nothing able to cross it. Feathering the coverage cannot fix\n * that, and it was tried \u2014 the alpha was never what the eye was reading.\n *\n * So the reflection is displaced by a noise field rather than blurred. **Displaced, because a\n * blur is the wrong phenomenon**: a rough surface does not soften what it reflects, it scatters\n * it, and a lamp on wet tarmac smears into a streak rather than becoming a soft disc. It is also\n * what a blur cannot afford here \u2014 the mirror has no mip chain (see the `textureLod` note\n * below), so softening it honestly would cost several taps where this costs none.\n *\n * `uFilmRoughnessCycles` is bumps to the metre, and it is the same quantity\n * `Renderer.setSurfaceRelief` takes. **Matching it to the surface underneath is the point**:\n * given the same field at the same scale, the water is broken up by the aggregate that the dry\n * road beside it is showing, and the two stop being different materials.\n *\n * Zero is the mirror this pass has always been, exactly, so nothing that does not ask for\n * roughness renders differently.\n */\nuniform float uFilmRoughness;\nuniform float uFilmRoughnessCycles;\n\nout vec4 fragColor;\n\n/** A hue wheel, without a texture. Standard 6-segment ramp. */\nvec3 hue(float h) {\n h = fract(h) * 6.0;\n vec3 c = clamp(vec3(abs(h - 3.0) - 1.0, 2.0 - abs(h - 2.0), 2.0 - abs(h - 4.0)), 0.0, 1.0);\n return c;\n}\n\n/**\n * The same value noise `flat.ts` builds its relief from, and deliberately the same.\n *\n * Copied rather than shared because these two shaders have no common include beyond the fog, and\n * a film that used a *different* field would defeat the whole purpose of taking a cycle count:\n * the water would be broken up by one pattern and the road by another, which is two materials\n * again with extra steps.\n */\nfloat filmHash(vec3 p) {\n p = fract(p * 0.1031);\n p += dot(p, p.zyx + 31.32);\n return fract((p.x + p.y) * p.z);\n}\n\nfloat filmNoise(vec3 p) {\n vec3 i = floor(p);\n vec3 f = fract(p);\n vec3 u = f * f * (3.0 - 2.0 * f);\n return mix(\n mix(\n mix(filmHash(i + vec3(0.0, 0.0, 0.0)), filmHash(i + vec3(1.0, 0.0, 0.0)), u.x),\n mix(filmHash(i + vec3(0.0, 1.0, 0.0)), filmHash(i + vec3(1.0, 1.0, 0.0)), u.x),\n u.y\n ),\n mix(\n mix(filmHash(i + vec3(0.0, 0.0, 1.0)), filmHash(i + vec3(1.0, 0.0, 1.0)), u.x),\n mix(filmHash(i + vec3(0.0, 1.0, 1.0)), filmHash(i + vec3(1.0, 1.0, 1.0)), u.x),\n u.y\n ),\n u.z\n );\n}\n\nvoid main() {\n if (uClipEnabled != 0 && dot(vec4(vWorld, 1.0), uClipPlane) < 0.0) discard;\n\n vec3 view = normalize(uCameraPos - vWorld);\n float facing = max(dot(normalize(vNormal), view), 0.0);\n /*\n * Fresnel-ish: dark looking straight down at it, bright at a grazing angle.\n * This is the whole effect \u2014 it is what makes a wet patch look wet rather\n * than looking like a patch of dark paint.\n */\n float grazing = pow(1.0 - facing, 3.0);\n\n /*\n * A slow travelling interference, so the sheen crawls rather than sitting\n * still. Two frequencies that do not divide each other, or the pattern\n * repeats visibly across a long slick.\n */\n float ripple =\n sin(vWorld.x * 0.73 + vWorld.z * 0.41 + uTime * 0.55) *\n cos(vWorld.z * 0.91 - vWorld.x * 0.29 - uTime * 0.37);\n\n vec3 sheen = hue(grazing * 1.35 + ripple * 0.14 + 0.55);\n /*\n * Dark underneath, so the film always reads as *less* than the surface it lies on even where\n * the sheen is strongest \u2014 but not so dark that it disappears.\n *\n * A slick that has just launched somebody has to be findable afterwards. It was rounded,\n * feathered and iridescent \u2014 and at a fifth of an opaque tint on a pale deck at dusk,\n * invisible. A hazard you cannot find is not a hazard, it is a trap.\n *\n * So the base is darker and the sheen has a floor: even looking straight down, where the\n * Fresnel term is nearly nothing, a slick is a distinctly wet patch rather than a faint one.\n */\n vec3 base = vTint * 0.9;\n vec3 color = base + sheen * uSheen * (0.22 + grazing * 0.75);\n\n /*\n * The mirrored scene, under the sheen rather than instead of it.\n *\n * Screen-space from the mirrored clip position, exactly as the water pass does it, with the\n * same fade as the sample leaves the target: a reflection that stopped dead at the edge of its\n * own buffer would draw a line across the road where nothing is.\n *\n * Weighted by the grazing term as well as by strength, because that is what a reflection does:\n * a wet surface underfoot shows almost nothing and the same surface down the street shows the\n * whole scene. Added before the fog so the image recedes with everything else in the frame.\n */\n if (uReflectionEnabled != 0 && uReflectionStrength > 0.0 && vReflectionClip.w > 0.0) {\n vec2 reflectionUv = vReflectionClip.xy / vReflectionClip.w * 0.5 + 0.5;\n vec2 border = min(reflectionUv, 1.0 - reflectionUv);\n float valid = smoothstep(0.0, 0.025, min(border.x, border.y));\n /*\n * An explicit level, not an implicit one, and the branch above is the whole reason.\n *\n * An implicit derivative inside control flow that is not provably uniform is what lets a\n * compiler flatten the branch and pay for every arm \u2014 AGENTS.md, 2026-08-07 \u2014 and WGSL\n * does not merely warn: the module fails to compile with \"textureSample must only be\n * called from uniform control flow\", which invalidates the pipeline, which invalidates\n * the command buffer, and the frame still presents. The fifth time on this branch. The\n * mirror has no mip chain, so level 0 is the only level there is; the water shader\n * reaches the same conclusion for the same lookup.\n */\n /*\n * The aggregate under the water, as a displacement of what the water shows.\n *\n * The gradient of the same field the road's relief uses, taken across the surface and used\n * to push the lookup. **Stretched along the reflection rather than isotropic**, and that is\n * the half that makes it read as a road: a reflection on a rough horizontal surface smears\n * *towards the viewer*, because a bump tilts the ray in the plane it is already travelling\n * in. Displacing in a circle gives frosted glass instead, which is a different wet thing.\n *\n * Scaled by the grazing term for the same reason the mirror is: looking straight down at a\n * puddle the reflection is compressed into almost nothing and a displacement of it would be\n * enormous, so the scatter has to fall off exactly where the image does.\n */\n if (uFilmRoughness > 0.0) {\n vec3 at = vWorld * uFilmRoughnessCycles;\n float here = filmNoise(at);\n vec2 slope = vec2(\n filmNoise(at + vec3(0.5, 0.0, 0.0)) - here,\n filmNoise(at + vec3(0.0, 0.0, 0.5)) - here\n );\n /* Along the view, which on a horizontal film is where the image runs. */\n vec2 along = normalize(vec2(view.x, view.z) + vec2(1e-5, 0.0));\n vec2 smear = along * dot(slope, along) * 3.0 + slope * 0.35;\n reflectionUv += smear * uFilmRoughness * 0.016 * (0.2 + grazing * 0.8);\n /* Re-fade at the border: a displaced sample can leave the target the original did not. */\n vec2 movedBorder = min(reflectionUv, 1.0 - reflectionUv);\n valid = min(valid, smoothstep(0.0, 0.025, min(movedBorder.x, movedBorder.y)));\n }\n vec3 mirrored = textureLod(uReflectionMap, clamp(reflectionUv, 0.0, 1.0), 0.0).rgb;\n color = mix(color, mirrored, valid * uReflectionStrength * (0.25 + grazing * 0.75));\n }\n\n // The same fog every other pass applies \u2014 and now literally the same code, which\n // it was not: this ran its own curve while claiming otherwise, so a slick receded\n // at a different rate than the deck it was lying on.\n float fog = mediumFog(length(uCameraPos - vWorld), vWorld.y);\n color = mix(color, mediumColor(), fog);\n\n /*\n * Alpha from coverage, squared.\n *\n * Squared because a linear ramp across the rim still reads as a visible band \u2014\n * the eye finds the constant-gradient edge. Squaring pushes the falloff outward\n * so the slick has a soft shoulder and no perceptible boundary. The sheen fades\n * with it, which is right: the thinnest part of a film is the least iridescent.\n */\n /*\n * Alpha from coverage, and the rim is what is squared \u2014 not the whole patch.\n *\n * Squaring the coverage outright made the *middle* translucent too, which is most of why a\n * slick could not be found. The shoulder still needs to be soft, so the square is mixed in\n * near the edge only: solid through the body, dissolving at the rim.\n */\n float coverage = clamp(vCoverage, 0.0, 1.0);\n float alpha = mix(coverage * coverage, coverage, 0.65) * 0.92;\n fragColor = vec4(color, alpha);\n}\n";
|
|
@@ -78,7 +78,8 @@ export function grainWeight(luma) {
|
|
|
78
78
|
return 0.25 + 3 * l * (1 - l);
|
|
79
79
|
}
|
|
80
80
|
/** Both backends' words when there is no composite to apply the look in. */
|
|
81
|
-
export const FILM_LOOK_WITHOUT_COMPOSITE = '[driftengine] setVignette and
|
|
81
|
+
export const FILM_LOOK_WITHOUT_COMPOSITE = '[driftengine] setVignette, setFilmGrain and setChromaticAberration need `screenEffects`: ' +
|
|
82
|
+
'without a composite every ' +
|
|
82
83
|
'pass is the last thing to touch the frame, so there is no one place to apply a lens or a ' +
|
|
83
84
|
'print. The look was not applied.';
|
|
84
85
|
/** A vignette strength as the shader takes it: 0 to 4, anything else off. */
|