@driftengine/core 4.12.0 → 4.13.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 +66 -0
- package/README.md +18 -9
- package/dist/geometry/meshBuilder.d.ts +10 -0
- package/dist/geometry/meshBuilder.js +11 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.js +3 -2
- package/dist/math/color.d.ts +25 -0
- package/dist/math/color.js +37 -0
- package/dist/render/backend/webgl2/renderer.d.ts +17 -4
- package/dist/render/backend/webgl2/renderer.js +82 -47
- package/dist/render/backend/webgl2/skinScatter.d.ts +3 -1
- package/dist/render/backend/webgl2/skinScatter.js +5 -1
- package/dist/render/backend/webgpu/boltPass.d.ts +1 -1
- package/dist/render/backend/webgpu/causticsPass.d.ts +1 -1
- package/dist/render/backend/webgpu/filmPass.d.ts +1 -1
- package/dist/render/backend/webgpu/flockPass.d.ts +1 -1
- package/dist/render/backend/webgpu/giFieldPass.d.ts +3 -1
- package/dist/render/backend/webgpu/gpuDrivenPass.d.ts +10 -1
- package/dist/render/backend/webgpu/gpuDrivenPass.js +28 -11
- package/dist/render/backend/webgpu/panelPass.d.ts +1 -1
- package/dist/render/backend/webgpu/postPass.d.ts +2 -2
- package/dist/render/backend/webgpu/renderer.d.ts +19 -8
- package/dist/render/backend/webgpu/renderer.js +94 -38
- package/dist/render/backend/webgpu/scatterPass.d.ts +1 -1
- package/dist/render/backend/webgpu/skinScatterPass.d.ts +5 -1
- package/dist/render/backend/webgpu/skinScatterPass.js +9 -3
- package/dist/render/backend/webgpu/skyPass.d.ts +11 -1
- package/dist/render/backend/webgpu/waterPass.d.ts +1 -1
- package/dist/render/backend/webgpu/windStreakPass.d.ts +1 -1
- package/dist/render/causticsRenderer.d.ts +3 -1
- package/dist/render/causticsRenderer.js +5 -1
- package/dist/render/decalPass.d.ts +3 -1
- package/dist/render/decalPass.js +4 -1
- package/dist/render/flockRenderer.d.ts +1 -1
- package/dist/render/flockRenderer.js +4 -1
- package/dist/render/globalMediumPass.d.ts +3 -1
- package/dist/render/globalMediumPass.js +5 -1
- package/dist/render/gradeColor.d.ts +21 -0
- package/dist/render/gradeColor.js +64 -0
- package/dist/render/lightMatrix.d.ts +13 -0
- package/dist/render/lightMatrix.js +17 -0
- package/dist/render/passGrade.d.ts +13 -0
- package/dist/render/passGrade.js +47 -0
- package/dist/render/plumeRenderer.d.ts +1 -1
- package/dist/render/plumeRenderer.js +4 -1
- package/dist/render/renderQuality.d.ts +57 -18
- package/dist/render/renderQuality.js +3 -3
- package/dist/render/sceneTarget.d.ts +2 -0
- package/dist/render/sceneTarget.js +1 -0
- package/dist/render/sdfTextRenderer.d.ts +1 -1
- package/dist/render/sdfTextRenderer.js +1 -1
- package/dist/render/shaders/arcane.d.ts +1 -1
- package/dist/render/shaders/arcane.js +5 -1
- package/dist/render/shaders/bolt.d.ts +1 -1
- package/dist/render/shaders/bolt.js +4 -1
- package/dist/render/shaders/caustics.d.ts +1 -1
- package/dist/render/shaders/caustics.js +4 -1
- package/dist/render/shaders/decalProject.js +12 -2
- package/dist/render/shaders/film.d.ts +1 -1
- package/dist/render/shaders/film.js +22 -9
- package/dist/render/shaders/fire.d.ts +1 -1
- package/dist/render/shaders/fire.js +4 -1
- package/dist/render/shaders/flat/layerLooks.js +11 -19
- package/dist/render/shaders/flat/layered.js +33 -18
- package/dist/render/shaders/flock.d.ts +1 -1
- package/dist/render/shaders/flock.js +4 -1
- package/dist/render/shaders/generated/arcane.wgsl.d.ts +11 -1
- package/dist/render/shaders/generated/arcane.wgsl.js +11 -1
- package/dist/render/shaders/generated/bolt.wgsl.d.ts +12 -2
- package/dist/render/shaders/generated/bolt.wgsl.js +12 -2
- package/dist/render/shaders/generated/caustics.wgsl.d.ts +12 -2
- package/dist/render/shaders/generated/caustics.wgsl.js +12 -2
- package/dist/render/shaders/generated/decalProject.wgsl.d.ts +6 -1
- package/dist/render/shaders/generated/decalProject.wgsl.js +6 -1
- package/dist/render/shaders/generated/film.wgsl.d.ts +12 -2
- package/dist/render/shaders/generated/film.wgsl.js +12 -2
- package/dist/render/shaders/generated/fire.wgsl.d.ts +11 -1
- package/dist/render/shaders/generated/fire.wgsl.js +11 -1
- package/dist/render/shaders/generated/flat.wgsl.js +23 -23
- package/dist/render/shaders/generated/flock.wgsl.d.ts +12 -2
- package/dist/render/shaders/generated/flock.wgsl.js +12 -2
- package/dist/render/shaders/generated/globalMedium.wgsl.d.ts +12 -2
- package/dist/render/shaders/generated/globalMedium.wgsl.js +12 -2
- package/dist/render/shaders/generated/lightVolume.wgsl.d.ts +36 -16
- package/dist/render/shaders/generated/lightVolume.wgsl.js +39 -19
- package/dist/render/shaders/generated/panel.wgsl.d.ts +12 -2
- package/dist/render/shaders/generated/panel.wgsl.js +12 -2
- package/dist/render/shaders/generated/rush.wgsl.d.ts +31 -26
- package/dist/render/shaders/generated/rush.wgsl.js +31 -26
- package/dist/render/shaders/generated/scatter.wgsl.d.ts +12 -2
- package/dist/render/shaders/generated/scatter.wgsl.js +12 -2
- package/dist/render/shaders/generated/skinBlur.wgsl.d.ts +11 -1
- package/dist/render/shaders/generated/skinBlur.wgsl.js +11 -1
- package/dist/render/shaders/generated/sky.wgsl.d.ts +12 -2
- package/dist/render/shaders/generated/sky.wgsl.js +12 -2
- package/dist/render/shaders/generated/smoke.wgsl.d.ts +11 -1
- package/dist/render/shaders/generated/smoke.wgsl.js +11 -1
- package/dist/render/shaders/generated/text.wgsl.d.ts +11 -1
- package/dist/render/shaders/generated/text.wgsl.js +11 -1
- package/dist/render/shaders/generated/water.wgsl.d.ts +12 -2
- package/dist/render/shaders/generated/water.wgsl.js +12 -2
- package/dist/render/shaders/generated/windStreaks.wgsl.d.ts +12 -2
- package/dist/render/shaders/generated/windStreaks.wgsl.js +12 -2
- package/dist/render/shaders/globalMedium.js +14 -1
- package/dist/render/shaders/gpudriven/present.wgsl.d.ts +15 -6
- package/dist/render/shaders/gpudriven/present.wgsl.js +71 -8
- package/dist/render/shaders/lightVolume.js +4 -1
- package/dist/render/shaders/line.d.ts +1 -1
- package/dist/render/shaders/outputTransform.d.ts +8 -2
- package/dist/render/shaders/outputTransform.js +18 -11
- package/dist/render/shaders/panel.d.ts +1 -1
- package/dist/render/shaders/panel.js +5 -1
- package/dist/render/shaders/particle.d.ts +3 -3
- package/dist/render/shaders/rush.js +8 -0
- package/dist/render/shaders/scatter.d.ts +1 -1
- package/dist/render/shaders/scatter.js +4 -1
- package/dist/render/shaders/sdfText.d.ts +1 -1
- package/dist/render/shaders/skinBlur.js +5 -1
- package/dist/render/shaders/sky.d.ts +1 -1
- package/dist/render/shaders/sky.js +5 -1
- package/dist/render/shaders/smoke.d.ts +1 -1
- package/dist/render/shaders/smoke.js +4 -1
- package/dist/render/shaders/text.d.ts +1 -1
- package/dist/render/shaders/text.js +4 -1
- package/dist/render/shaders/water.d.ts +1 -1
- package/dist/render/shaders/water.js +12 -2
- package/dist/render/shaders/windStreaks.d.ts +1 -1
- package/dist/render/shaders/windStreaks.js +4 -1
- package/dist/render/surfaceLayerLooks.d.ts +16 -4
- package/dist/render/surfaceLayerLooks.js +33 -14
- package/dist/render/surfaceLayers.d.ts +2 -2
- package/dist/render/textRenderer.d.ts +3 -1
- package/dist/render/textRenderer.js +7 -1
- package/dist/render/vertexDefaults.d.ts +1 -1
- package/dist/render/vertexDefaults.js +1 -1
- package/dist/render/waterRenderer.d.ts +1 -1
- package/dist/render/waterRenderer.js +4 -1
- package/dist/render/windStreakRenderer.d.ts +3 -1
- package/dist/render/windStreakRenderer.js +5 -1
- package/package.json +6 -6
- package/src/geometry/meshBuilder.ts +21 -2
- package/src/index.ts +11 -2
- package/src/math/color.ts +39 -0
- package/src/render/backend/webgl2/renderer.ts +134 -44
- package/src/render/backend/webgl2/skinScatter.ts +5 -0
- package/src/render/backend/webgpu/giFieldPass.ts +3 -1
- package/src/render/backend/webgpu/gpuDrivenPass.ts +29 -13
- package/src/render/backend/webgpu/renderer.ts +104 -37
- package/src/render/backend/webgpu/skinScatterPass.ts +17 -1
- package/src/render/causticsRenderer.ts +5 -0
- package/src/render/decalPass.ts +3 -0
- package/src/render/flockRenderer.ts +5 -0
- package/src/render/globalMediumPass.ts +10 -1
- package/src/render/gradeColor.ts +90 -0
- package/src/render/lightMatrix.ts +32 -0
- package/src/render/passGrade.ts +58 -0
- package/src/render/plumeRenderer.ts +5 -0
- package/src/render/renderQuality.ts +60 -21
- package/src/render/sceneTarget.ts +3 -0
- package/src/render/sdfTextRenderer.ts +1 -1
- package/src/render/shaders/arcane.ts +5 -1
- package/src/render/shaders/bolt.ts +4 -1
- package/src/render/shaders/caustics.ts +4 -1
- package/src/render/shaders/decalProject.ts +12 -2
- package/src/render/shaders/film.ts +22 -9
- package/src/render/shaders/fire.ts +4 -1
- package/src/render/shaders/flat/layerLooks.ts +11 -19
- package/src/render/shaders/flat/layered.ts +33 -18
- package/src/render/shaders/flock.ts +4 -1
- package/src/render/shaders/generated/arcane.wgsl.ts +11 -1
- package/src/render/shaders/generated/bolt.wgsl.ts +12 -2
- package/src/render/shaders/generated/caustics.wgsl.ts +12 -2
- package/src/render/shaders/generated/decalProject.wgsl.ts +6 -1
- package/src/render/shaders/generated/film.wgsl.ts +12 -2
- package/src/render/shaders/generated/fire.wgsl.ts +11 -1
- package/src/render/shaders/generated/flat.wgsl.ts +23 -23
- package/src/render/shaders/generated/flock.wgsl.ts +12 -2
- package/src/render/shaders/generated/globalMedium.wgsl.ts +12 -2
- package/src/render/shaders/generated/lightVolume.wgsl.ts +39 -19
- package/src/render/shaders/generated/panel.wgsl.ts +12 -2
- package/src/render/shaders/generated/rush.wgsl.ts +31 -26
- package/src/render/shaders/generated/scatter.wgsl.ts +12 -2
- package/src/render/shaders/generated/skinBlur.wgsl.ts +11 -1
- package/src/render/shaders/generated/sky.wgsl.ts +12 -2
- package/src/render/shaders/generated/smoke.wgsl.ts +11 -1
- package/src/render/shaders/generated/text.wgsl.ts +11 -1
- package/src/render/shaders/generated/water.wgsl.ts +12 -2
- package/src/render/shaders/generated/windStreaks.wgsl.ts +12 -2
- package/src/render/shaders/globalMedium.ts +14 -1
- package/src/render/shaders/gpudriven/present.wgsl.ts +72 -8
- package/src/render/shaders/lightVolume.ts +4 -1
- package/src/render/shaders/outputTransform.ts +19 -11
- package/src/render/shaders/panel.ts +5 -1
- package/src/render/shaders/rush.ts +8 -0
- package/src/render/shaders/scatter.ts +4 -1
- package/src/render/shaders/skinBlur.ts +5 -1
- package/src/render/shaders/sky.ts +5 -1
- package/src/render/shaders/smoke.ts +4 -1
- package/src/render/shaders/text.ts +4 -1
- package/src/render/shaders/water.ts +12 -2
- package/src/render/shaders/windStreaks.ts +4 -1
- package/src/render/surfaceLayerLooks.ts +49 -18
- package/src/render/surfaceLayers.ts +2 -2
- package/src/render/textRenderer.ts +6 -0
- package/src/render/vertexDefaults.ts +1 -1
- package/src/render/waterRenderer.ts +5 -0
- package/src/render/windStreakRenderer.ts +5 -0
|
@@ -97,6 +97,8 @@ export declare class GlobalMediumPass {
|
|
|
97
97
|
* `(ONE, SRC_ALPHA)` on colour is `dst * transmittance + inscatter`; alpha is left alone with
|
|
98
98
|
* `(ZERO, ONE)`, because the frame's own alpha is not the medium's to spend.
|
|
99
99
|
*/
|
|
100
|
-
compositeMedium(depth: WebGLTexture, frameWidth: number, frameHeight: number
|
|
100
|
+
compositeMedium(depth: WebGLTexture, frameWidth: number, frameHeight: number,
|
|
101
|
+
/** The renderer's `passGrade`: how the frame under the medium was graded. */
|
|
102
|
+
outputTransform?: number, outputExposure?: number): void;
|
|
101
103
|
dispose(): void;
|
|
102
104
|
}
|
|
@@ -192,7 +192,9 @@ export class GlobalMediumPass {
|
|
|
192
192
|
* `(ONE, SRC_ALPHA)` on colour is `dst * transmittance + inscatter`; alpha is left alone with
|
|
193
193
|
* `(ZERO, ONE)`, because the frame's own alpha is not the medium's to spend.
|
|
194
194
|
*/
|
|
195
|
-
compositeMedium(depth, frameWidth, frameHeight
|
|
195
|
+
compositeMedium(depth, frameWidth, frameHeight,
|
|
196
|
+
/** The renderer's `passGrade`: how the frame under the medium was graded. */
|
|
197
|
+
outputTransform = 0, outputExposure = 1) {
|
|
196
198
|
if (this.target === null || this.unavailable)
|
|
197
199
|
return;
|
|
198
200
|
const { gl } = this;
|
|
@@ -215,6 +217,8 @@ export class GlobalMediumPass {
|
|
|
215
217
|
this.mediumTexel[1] = 1 / Math.max(1, this.height);
|
|
216
218
|
gl.uniform2fv(u['uMediumTexel'] ?? null, this.mediumTexel);
|
|
217
219
|
gl.uniform4fv(u['uDepthToViewZ'] ?? null, this.depthToViewZ);
|
|
220
|
+
gl.uniform1i(u['uOutputTransform'] ?? null, outputTransform);
|
|
221
|
+
gl.uniform1f(u['uOutputExposure'] ?? null, outputExposure);
|
|
218
222
|
gl.drawArrays(gl.TRIANGLES, 0, 3);
|
|
219
223
|
gl.bindVertexArray(null);
|
|
220
224
|
gl.activeTexture(gl.TEXTURE1);
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The forward grade on the CPU: what `applyOutputTransform` in `shaders/outputTransform.ts` does to
|
|
3
|
+
* a colour, for a colour no shader draws — a clear colour, written to a target before any pass runs.
|
|
4
|
+
*
|
|
5
|
+
* **Why a clear colour is graded at all.** Every pass grades what it draws when nothing after it
|
|
6
|
+
* will, so a colour handed to the renderer means one thing wherever it lands: linear light. A clear
|
|
7
|
+
* left as it was given is the one colour in the frame read as a display value, so a scene whose fog
|
|
8
|
+
* fades to its background colour fades to a different colour than the background it meets; with
|
|
9
|
+
* `hdrScene` the resolve grades the clear with everything else, and a frame without it has to agree.
|
|
10
|
+
*
|
|
11
|
+
* **A second copy of the curve, and what holds the two together** is the hand-derived values in
|
|
12
|
+
* `gradeColor.test.ts`, the same ones the shader's own constants give: the sRGB encode's two
|
|
13
|
+
* segments, the ACES fit through its two matrices, and the highlight shoulder. Code 4, the filmic
|
|
14
|
+
* curve, is the composite's alone, so a forward code never carries it (`forwardTransformCode`).
|
|
15
|
+
*/
|
|
16
|
+
import type { Vec3 } from '../math/color.ts';
|
|
17
|
+
/**
|
|
18
|
+
* `color` graded by forward code `code` at `exposure` into `out`, which may be `color`: 0 leaves it
|
|
19
|
+
* as it is, 1 encodes it as sRGB, 2 runs ACES first and 3 the highlight shoulder. Allocates nothing.
|
|
20
|
+
*/
|
|
21
|
+
export declare function gradeColorInto(code: number, exposure: number, color: Readonly<Vec3>, out: Vec3): Vec3;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/* Columns as the shader's `mat3` reads them, written here as rows of the product. */
|
|
2
|
+
const IN_R = [0.59719, 0.35458, 0.04823];
|
|
3
|
+
const IN_G = [0.076, 0.90834, 0.01566];
|
|
4
|
+
const IN_B = [0.0284, 0.13383, 0.83777];
|
|
5
|
+
const OUT_R = [1.60475, -0.53108, -0.07367];
|
|
6
|
+
const OUT_G = [-0.10208, 1.10813, -0.00605];
|
|
7
|
+
const OUT_B = [-0.00327, -0.07276, 1.07602];
|
|
8
|
+
/**
|
|
9
|
+
* `color` graded by forward code `code` at `exposure` into `out`, which may be `color`: 0 leaves it
|
|
10
|
+
* as it is, 1 encodes it as sRGB, 2 runs ACES first and 3 the highlight shoulder. Allocates nothing.
|
|
11
|
+
*/
|
|
12
|
+
export function gradeColorInto(code, exposure, color, out) {
|
|
13
|
+
let r = color[0];
|
|
14
|
+
let g = color[1];
|
|
15
|
+
let b = color[2];
|
|
16
|
+
if (code === 0) {
|
|
17
|
+
out[0] = r;
|
|
18
|
+
out[1] = g;
|
|
19
|
+
out[2] = b;
|
|
20
|
+
return out;
|
|
21
|
+
}
|
|
22
|
+
if (code === 2) {
|
|
23
|
+
const x = r * exposure;
|
|
24
|
+
const y = g * exposure;
|
|
25
|
+
const z = b * exposure;
|
|
26
|
+
const ir = fit(dot(IN_R, x, y, z));
|
|
27
|
+
const ig = fit(dot(IN_G, x, y, z));
|
|
28
|
+
const ib = fit(dot(IN_B, x, y, z));
|
|
29
|
+
r = clamp01(dot(OUT_R, ir, ig, ib));
|
|
30
|
+
g = clamp01(dot(OUT_G, ir, ig, ib));
|
|
31
|
+
b = clamp01(dot(OUT_B, ir, ig, ib));
|
|
32
|
+
}
|
|
33
|
+
else if (code === 3) {
|
|
34
|
+
r *= exposure;
|
|
35
|
+
g *= exposure;
|
|
36
|
+
b *= exposure;
|
|
37
|
+
const m = Math.max(r, g, b);
|
|
38
|
+
if (m > 0.8) {
|
|
39
|
+
const e = m - 0.8;
|
|
40
|
+
const k = (0.8 + (0.2 * e) / (e + 0.2)) / m;
|
|
41
|
+
r *= k;
|
|
42
|
+
g *= k;
|
|
43
|
+
b *= k;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
out[0] = encode(r);
|
|
47
|
+
out[1] = encode(g);
|
|
48
|
+
out[2] = encode(b);
|
|
49
|
+
return out;
|
|
50
|
+
}
|
|
51
|
+
function dot(row, x, y, z) {
|
|
52
|
+
return row[0] * x + row[1] * y + row[2] * z;
|
|
53
|
+
}
|
|
54
|
+
/** The RRT and ODT fit, `rrtAndOdtFit` in the shader. */
|
|
55
|
+
function fit(v) {
|
|
56
|
+
return (v * (v + 0.0245786) - 0.000090537) / (v * (0.983729 * v + 0.432951) + 0.238081);
|
|
57
|
+
}
|
|
58
|
+
/** Linear to sRGB, the piecewise form the shader uses. */
|
|
59
|
+
function encode(c) {
|
|
60
|
+
return c <= 0.0031308 ? c * 12.92 : 1.055 * Math.max(c, 0) ** (1 / 2.4) - 0.055;
|
|
61
|
+
}
|
|
62
|
+
function clamp01(v) {
|
|
63
|
+
return v < 0 ? 0 : v > 1 ? 1 : v;
|
|
64
|
+
}
|
|
@@ -1,6 +1,19 @@
|
|
|
1
1
|
import { mat4 } from 'gl-matrix';
|
|
2
2
|
import type { Vec3 } from '../math/color.ts';
|
|
3
3
|
export declare function computeLightMatrix(lightDir: Vec3, focusX: number, focusY: number, focusZ: number, radius: number, shadowMapSize: number, out: mat4): number;
|
|
4
|
+
/**
|
|
5
|
+
* A directional light's matrix covering a box of known size whole: a model, a level, a room. The
|
|
6
|
+
* sphere around the box, at its centre, through `computeLightMatrix`, widened by the half texel the
|
|
7
|
+
* centre's snap may move it so a corner on the sphere stays inside. Returns the depth span, as that
|
|
8
|
+
* does.
|
|
9
|
+
*
|
|
10
|
+
* **For a scene whose extent is known rather than one that follows a viewer.** A radius chosen by
|
|
11
|
+
* eye around a guessed centre leaves part of the model outside the map, where it casts nothing; the
|
|
12
|
+
* bounds are the answer, and a loader has them. **What it gives up** is resolution where the box is
|
|
13
|
+
* large: the map's texels are spread over all of it, so a building's shadows are coarser than a
|
|
14
|
+
* focus on the viewer would draw them, which is what `computeLightMatrix` around the viewer is for.
|
|
15
|
+
*/
|
|
16
|
+
export declare function computeLightMatrixForBounds(lightDir: Vec3, min: Readonly<Vec3>, max: Readonly<Vec3>, shadowMapSize: number, out: mat4): number;
|
|
4
17
|
/**
|
|
5
18
|
* The depth an orthographic light matrix spans, in metres: what `computeLightMatrix` returns, read
|
|
6
19
|
* back off any such matrix. Its depth row is the light's axis scaled by two over the span, so the
|
|
@@ -43,6 +43,23 @@ export function computeLightMatrix(lightDir, focusX, focusY, focusZ, radius, sha
|
|
|
43
43
|
// to express its residual depth tolerance in stable world-space metres.
|
|
44
44
|
return radius * 6;
|
|
45
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* A directional light's matrix covering a box of known size whole: a model, a level, a room. The
|
|
48
|
+
* sphere around the box, at its centre, through `computeLightMatrix`, widened by the half texel the
|
|
49
|
+
* centre's snap may move it so a corner on the sphere stays inside. Returns the depth span, as that
|
|
50
|
+
* does.
|
|
51
|
+
*
|
|
52
|
+
* **For a scene whose extent is known rather than one that follows a viewer.** A radius chosen by
|
|
53
|
+
* eye around a guessed centre leaves part of the model outside the map, where it casts nothing; the
|
|
54
|
+
* bounds are the answer, and a loader has them. **What it gives up** is resolution where the box is
|
|
55
|
+
* large: the map's texels are spread over all of it, so a building's shadows are coarser than a
|
|
56
|
+
* focus on the viewer would draw them, which is what `computeLightMatrix` around the viewer is for.
|
|
57
|
+
*/
|
|
58
|
+
export function computeLightMatrixForBounds(lightDir, min, max, shadowMapSize, out) {
|
|
59
|
+
const half = Math.hypot(max[0] - min[0], max[1] - min[1], max[2] - min[2]) / 2;
|
|
60
|
+
const radius = Math.max(half, 1e-3) * (1 + 2 / Math.max(shadowMapSize, 1));
|
|
61
|
+
return computeLightMatrix(lightDir, (min[0] + max[0]) / 2, (min[1] + max[1]) / 2, (min[2] + max[2]) / 2, radius, shadowMapSize, out);
|
|
62
|
+
}
|
|
46
63
|
/**
|
|
47
64
|
* The depth an orthographic light matrix spans, in metres: what `computeLightMatrix` returns, read
|
|
48
65
|
* back off any such matrix. Its depth row is the light's axis scaled by two over the span, so the
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `uOutputTransform` a pass uploads: `storing` inside a probe's face or a capture, `presented`
|
|
3
|
+
* once the frame's resolve has run, `resolveGrades` where that resolve applies the transform.
|
|
4
|
+
*/
|
|
5
|
+
export declare function passGradeCode(storing: boolean, presented: boolean, resolveGrades: boolean, outputTransform: string): number;
|
|
6
|
+
/** The `uOutputExposure` beside it: the frame's exposure wherever the pass grades, else one. */
|
|
7
|
+
export declare function passGradeExposure(storing: boolean, presented: boolean, resolveGrades: boolean, exposure: number): number;
|
|
8
|
+
/**
|
|
9
|
+
* The code an interface draws with, given the pass's: the screen encode wherever the pass grades at
|
|
10
|
+
* all, and nothing where it does not. Never a curve, so never an exposure either: a screen-space
|
|
11
|
+
* caption, a panel or a 2D layer keeps the colour it was picked as whatever the scene is graded by.
|
|
12
|
+
*/
|
|
13
|
+
export declare function interfaceGrade(passCode: number): number;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which grade a pass applies to what it draws, decided once for both backends.
|
|
3
|
+
*
|
|
4
|
+
* **Every pass grades where nothing after it will**, so a colour handed to the renderer means linear
|
|
5
|
+
* light wherever it lands. Three facts decide where that is:
|
|
6
|
+
*
|
|
7
|
+
* - **A probe's face or a capture stores radiance** for something else to light with or to show, so
|
|
8
|
+
* it is never graded. The frame grades what it reads of one; graded at the bake as well, the curve
|
|
9
|
+
* would be applied twice.
|
|
10
|
+
* - **A frame whose resolve grades** (`hdrScene` over a scene target) leaves every pass before the
|
|
11
|
+
* resolve linear, because grading early throws away the range the float target exists to keep.
|
|
12
|
+
* - **Past the present, a pass lands on the canvas the resolve has already written**, so nothing
|
|
13
|
+
* after it will grade it and it grades itself with the frame's forward code, composite or not. An
|
|
14
|
+
* interface drawn after `endFrame` escapes the screen-space chain; it does not escape the grade.
|
|
15
|
+
*
|
|
16
|
+
* **An interface takes the screen encode and not the curve** (`interfaceGrade`). A tone curve and
|
|
17
|
+
* an exposure are how a scene's light is brought to the screen, and a caption or a panel is not
|
|
18
|
+
* light: its colour was picked for the screen, so it is encoded wherever the frame is and comes out
|
|
19
|
+
* as picked under every transform, at every exposure. What it gives up is an interface drawn inside
|
|
20
|
+
* a frame whose resolve grades (`hdrScene`): that one is part of the picture the resolve tone maps,
|
|
21
|
+
* and is drawn after `endFrame` to stay out of it. What would make any of this wrong is a composite
|
|
22
|
+
* step running after the present, which none does.
|
|
23
|
+
*/
|
|
24
|
+
import { forwardTransformCode } from './vertexDefaults.js';
|
|
25
|
+
/**
|
|
26
|
+
* The `uOutputTransform` a pass uploads: `storing` inside a probe's face or a capture, `presented`
|
|
27
|
+
* once the frame's resolve has run, `resolveGrades` where that resolve applies the transform.
|
|
28
|
+
*/
|
|
29
|
+
export function passGradeCode(storing, presented, resolveGrades, outputTransform) {
|
|
30
|
+
if (storing)
|
|
31
|
+
return 0;
|
|
32
|
+
return presented || !resolveGrades ? forwardTransformCode(outputTransform) : 0;
|
|
33
|
+
}
|
|
34
|
+
/** The `uOutputExposure` beside it: the frame's exposure wherever the pass grades, else one. */
|
|
35
|
+
export function passGradeExposure(storing, presented, resolveGrades, exposure) {
|
|
36
|
+
if (storing)
|
|
37
|
+
return 1;
|
|
38
|
+
return presented || !resolveGrades ? exposure : 1;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* The code an interface draws with, given the pass's: the screen encode wherever the pass grades at
|
|
42
|
+
* all, and nothing where it does not. Never a curve, so never an exposure either: a screen-space
|
|
43
|
+
* caption, a panel or a 2D layer keeps the colour it was picked as whatever the scene is graded by.
|
|
44
|
+
*/
|
|
45
|
+
export function interfaceGrade(passCode) {
|
|
46
|
+
return passCode === 0 ? 0 : 1;
|
|
47
|
+
}
|
|
@@ -89,7 +89,7 @@ export declare class PlumeRenderer {
|
|
|
89
89
|
* the sky at depth 1.0 under LEQUAL would otherwise overwrite anything drawn
|
|
90
90
|
* against open background.
|
|
91
91
|
*/
|
|
92
|
-
draw(gl: WebGL2RenderingContext, camera: FrameView, timeSeconds: number, atmosphere: Atmosphere, underwaterEnabled: boolean, atmosphereCameraY: number, clipPlane: Float32Array | null, windX?: number, windZ?: number, originX?: number, originY?: number, originZ?: number): void;
|
|
92
|
+
draw(gl: WebGL2RenderingContext, camera: FrameView, timeSeconds: number, atmosphere: Atmosphere, underwaterEnabled: boolean, atmosphereCameraY: number, clipPlane: Float32Array | null, windX?: number, windZ?: number, originX?: number, originY?: number, originZ?: number, outputTransform?: number, outputExposure?: number): void;
|
|
93
93
|
dispose(gl: WebGL2RenderingContext): void;
|
|
94
94
|
/** Uploads one attribute and returns its buffer, for the rare one a caller can change. */
|
|
95
95
|
private attach;
|
|
@@ -93,7 +93,7 @@ export class PlumeRenderer {
|
|
|
93
93
|
* the sky at depth 1.0 under LEQUAL would otherwise overwrite anything drawn
|
|
94
94
|
* against open background.
|
|
95
95
|
*/
|
|
96
|
-
draw(gl, camera, timeSeconds, atmosphere, underwaterEnabled, atmosphereCameraY, clipPlane, windX = 0, windZ = 0, originX = 0, originY = 0, originZ = 0) {
|
|
96
|
+
draw(gl, camera, timeSeconds, atmosphere, underwaterEnabled, atmosphereCameraY, clipPlane, windX = 0, windZ = 0, originX = 0, originY = 0, originZ = 0, outputTransform = 0, outputExposure = 1) {
|
|
97
97
|
if (this.indexCount === 0)
|
|
98
98
|
return;
|
|
99
99
|
const u = this.uniforms;
|
|
@@ -121,6 +121,9 @@ export class PlumeRenderer {
|
|
|
121
121
|
gl.uniform1i(u['uClipEnabled'] ?? null, clipPlane === null ? 0 : 1);
|
|
122
122
|
if (clipPlane !== null)
|
|
123
123
|
gl.uniform4fv(u['uClipPlane'] ?? null, clipPlane);
|
|
124
|
+
/* The renderer's `passGrade`: the frame's, but none in a probe's face or a capture. */
|
|
125
|
+
gl.uniform1i(u['uOutputTransform'] ?? null, outputTransform);
|
|
126
|
+
gl.uniform1f(u['uOutputExposure'] ?? null, outputExposure);
|
|
124
127
|
bindAtmosphere(gl, u, atmosphere, atmosphereCameraY, underwaterEnabled);
|
|
125
128
|
gl.enable(gl.BLEND);
|
|
126
129
|
if (this.blend === 'additive')
|
|
@@ -50,22 +50,35 @@ export type GlassShadows = 'full' | 'half' | 'off';
|
|
|
50
50
|
export type SkinScattering = 'pre-integrated' | 'screen-space';
|
|
51
51
|
export interface RenderQuality {
|
|
52
52
|
/**
|
|
53
|
-
* The transfer curve applied at the end of every shaded pass.
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
53
|
+
* The transfer curve applied at the end of every shaded pass. Construction-time, like every
|
|
54
|
+
* other quality control here.
|
|
55
|
+
*
|
|
56
|
+
* **`srgb` by default since 4.13.0, the encoding a display expects**: the light the renderer
|
|
57
|
+
* computes is linear, and a texture's colours are decoded to linear when they are read, so a
|
|
58
|
+
* frame written without encoding them back is a frame of linear values read as display values —
|
|
59
|
+
* every midtone far too dark. It is the encoding most renderers apply by default, so a model
|
|
60
|
+
* drawn here with no options reads as it does elsewhere. **What it gives up** is a highlight's
|
|
61
|
+
* colour: above 1 each channel clips on its own, so a bright saturated source goes to white
|
|
62
|
+
* rather than rolling off, which a curve with a shoulder (`aces`, `shoulder`) is for.
|
|
63
|
+
*
|
|
64
|
+
* **`none` was the default until 4.13.0, and it is the frame that reads as dark**: values
|
|
65
|
+
* written as they are computed, which is right only for a world whose colours were chosen by
|
|
66
|
+
* eye as display values and lit to match. It was reported by eye as "dark", "flat" or "not
|
|
67
|
+
* vivid", which sounds like a lighting problem, so that is where people looked; a consumer
|
|
68
|
+
* built, tuned and shipped six worlds under it before anybody said the word, and a model drawn
|
|
69
|
+
* from the manual came out darker than in any other engine. A world graded under it says
|
|
70
|
+
* `outputTransform: 'none'` and draws exactly as it did. **What would make `srgb` the wrong
|
|
71
|
+
* default** is a renderer whose colours are not linear, which this one's are not.
|
|
72
|
+
*
|
|
73
|
+
* **Every pass applies it where nothing after it will**, and the clear colour with them, so a
|
|
74
|
+
* colour handed to the renderer is linear wherever it lands: a frame with `hdrScene` is graded
|
|
75
|
+
* once at the resolve, and one without it is graded pass by pass to the same curve. A colour
|
|
76
|
+
* picked by eye is a display value, so it is stated through `srgbColor` (or a mesh built with
|
|
77
|
+
* `colorSpace: 'srgb'`), which gives back exactly the colour picked where a surface is lit at 1
|
|
78
|
+
* under `srgb`, and through the curve under a tone curve. **An interface** (screen text, a panel,
|
|
79
|
+
* the 2D layer) takes the screen encode and never the curve or the exposure, so its colours come
|
|
80
|
+
* out as picked under every transform; inside an `hdrScene` frame it is part of the picture the
|
|
81
|
+
* resolve grades, and is drawn after `endFrame` to stay out of the curve.
|
|
69
82
|
*/
|
|
70
83
|
readonly outputTransform: OutputTransform;
|
|
71
84
|
/**
|
|
@@ -259,9 +272,35 @@ export interface RenderQuality {
|
|
|
259
272
|
* at a shadow's edge — that is what the blur is for.
|
|
260
273
|
*/
|
|
261
274
|
readonly skinScattering: SkinScattering;
|
|
262
|
-
/**
|
|
275
|
+
/**
|
|
276
|
+
* How far a sun shadow may reach along the ground from its caster, in metres, before it
|
|
277
|
+
* dissolves. **Not a distance from the camera**: a shadow's own length, from where it leaves its
|
|
278
|
+
* caster to where it lands, fading from 35% of this to all of it.
|
|
279
|
+
*
|
|
280
|
+
* **1,000 since 4.13.0, so no building's shadow dissolves**: the fade starts at 350 m, past any
|
|
281
|
+
* wall, tower or canyon a shadow map covers at a useful resolution, so what bounds a shadow is
|
|
282
|
+
* its map, whose edge already fades. **What it gives up** is a way to hide a long shadow's
|
|
283
|
+
* stair-stepping at a low sun, which is what a short reach was for. It was 6 until 4.13.0: a
|
|
284
|
+
* tuning for short casters, which left a building's walls and arcades casting nothing on its
|
|
285
|
+
* floor under a high sun — the shadows vanished silently and read as random lighting. **What
|
|
286
|
+
* would make it wrong** is a scene whose shadows are too coarse to show at their full length;
|
|
287
|
+
* there a reach of the length that looks right is the setting, and the map around the viewer
|
|
288
|
+
* (`computeLightMatrix`) the better fix.
|
|
289
|
+
*/
|
|
263
290
|
readonly directionalShadowMaxDistance: number;
|
|
264
|
-
/**
|
|
291
|
+
/**
|
|
292
|
+
* How low the sun may stand before its shadows fade, as the ground a metre of height may shadow:
|
|
293
|
+
* the sun's horizontal reach per vertical metre, so 1 is 45° of elevation and 60 about 1°. Every
|
|
294
|
+
* sun shadow fades from 65% of this to all of it.
|
|
295
|
+
*
|
|
296
|
+
* **60 since 4.13.0: shadows hold until the sun is about a degree above the horizon**, the
|
|
297
|
+
* stretch of a day when they are longest and most worth seeing. **What it gives up**: a sun on
|
|
298
|
+
* the horizon throws shadows kilometres long that a map can hold only as stripes, and the
|
|
299
|
+
* last degree is where they fade rather than show. It was 3 until 4.13.0, which dropped every
|
|
300
|
+
* sun shadow below about 18° — an evening without shadows. **What would make it wrong** is a
|
|
301
|
+
* scene whose low sun streaks; a smaller number here is the setting, at the cost of the
|
|
302
|
+
* shadows going earlier in the evening.
|
|
303
|
+
*/
|
|
265
304
|
readonly directionalShadowMaxSlope: number;
|
|
266
305
|
/**
|
|
267
306
|
* Metres a point light may drift from where its shadow was baked before re-baking.
|
|
@@ -17,7 +17,7 @@ export const DIRECTIONAL_SHADOW_FADE_START = 0.65;
|
|
|
17
17
|
* six times, while the directional map spends all of it on one focused area.
|
|
18
18
|
*/
|
|
19
19
|
export const DEFAULT_RENDER_QUALITY = Object.freeze({
|
|
20
|
-
outputTransform: '
|
|
20
|
+
outputTransform: 'srgb',
|
|
21
21
|
outputExposure: 1,
|
|
22
22
|
pointLightFalloff: 'smooth',
|
|
23
23
|
/**
|
|
@@ -65,8 +65,8 @@ export const DEFAULT_RENDER_QUALITY = Object.freeze({
|
|
|
65
65
|
glassShadows: 'full',
|
|
66
66
|
pipelineCompile: 'wait',
|
|
67
67
|
skinScattering: 'pre-integrated',
|
|
68
|
-
directionalShadowMaxDistance:
|
|
69
|
-
directionalShadowMaxSlope:
|
|
68
|
+
directionalShadowMaxDistance: 1000,
|
|
69
|
+
directionalShadowMaxSlope: 60,
|
|
70
70
|
pointShadowRebakeDistance: 0.001,
|
|
71
71
|
pointShadowFacesPerFrame: 2,
|
|
72
72
|
/*
|
|
@@ -303,6 +303,8 @@ export declare class SceneTarget {
|
|
|
303
303
|
readonly transform: number;
|
|
304
304
|
readonly exposure: number;
|
|
305
305
|
readonly film?: Float32Array;
|
|
306
|
+
/** Whether the forward passes graded the scene because this resolve does not. */
|
|
307
|
+
readonly sceneGraded?: boolean;
|
|
306
308
|
}, motion?: {
|
|
307
309
|
readonly reprojection: Float32Array;
|
|
308
310
|
readonly strength: number;
|
|
@@ -819,6 +819,7 @@ export class SceneTarget {
|
|
|
819
819
|
gl.uniform1f(this.uniforms['uReach'] ?? null, reachUv);
|
|
820
820
|
gl.uniform1i(this.uniforms['uOutputTransform'] ?? null, grade.transform);
|
|
821
821
|
gl.uniform1f(this.uniforms['uOutputExposure'] ?? null, grade.exposure);
|
|
822
|
+
gl.uniform1i(this.uniforms['uSceneGraded'] ?? null, grade.sceneGraded === true ? 1 : 0);
|
|
822
823
|
if (grade.film !== undefined) {
|
|
823
824
|
gl.uniform4fv(this.uniforms['uFilmA'] ?? null, grade.film, 0, 4);
|
|
824
825
|
gl.uniform4fv(this.uniforms['uFilmB'] ?? null, grade.film, 4, 4);
|
|
@@ -65,7 +65,7 @@ export declare class SdfTextRenderer {
|
|
|
65
65
|
*/
|
|
66
66
|
/**
|
|
67
67
|
* `outputTransform` and `outputExposure` come from the renderer rather than from here, because
|
|
68
|
-
* whether this pass grades depends on whether anything after it will. See `
|
|
68
|
+
* whether this pass grades depends on whether anything after it will. See `passGrade.ts`.
|
|
69
69
|
*/
|
|
70
70
|
draw(viewProj: ReadonlyMat4, model: Float32Array, color: Vec3, opacity: number, outputTransform: number, outputExposure: number): boolean;
|
|
71
71
|
dispose(): void;
|
|
@@ -103,7 +103,7 @@ export class SdfTextRenderer {
|
|
|
103
103
|
*/
|
|
104
104
|
/**
|
|
105
105
|
* `outputTransform` and `outputExposure` come from the renderer rather than from here, because
|
|
106
|
-
* whether this pass grades depends on whether anything after it will. See `
|
|
106
|
+
* whether this pass grades depends on whether anything after it will. See `passGrade.ts`.
|
|
107
107
|
*/
|
|
108
108
|
draw(viewProj, model, color, opacity, outputTransform, outputExposure) {
|
|
109
109
|
if (this.layout.quadCount === 0 || opacity <= 0 || this.font === null || this.atlas === null) {
|
|
@@ -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 // 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";
|
|
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/* Before `uTint`, where fire and smoke declare it, so one field table serves all three. */\n\n/** 0 none, 1 sRGB, 2 ACES then sRGB, 3 the highlight shoulder then sRGB. See renderQuality.ts. */\nuniform int uOutputTransform;\n/** Scales the scene into the tone curve. 1 is the reference grade. See renderQuality.ts. */\nuniform float uOutputExposure;\n\n/*\n * ACES: the reference fit, not the cheap one.\n *\n * This was Narkowicz's single rational function, on the argument that the difference from\n * the reference curve is invisible next to the gain. That argument holds for a look and\n * fails for a *match*, which is what this option is for \u2014 aces exists so a world ported\n * from three.js reads the same, and three.js applies the full fit: into AP1 through\n * ACESInputMat, the RRT and ODT together, then back out through ACESOutputMat.\n *\n * The two curves are not a shade apart. Narkowicz is markedly brighter through the\n * midtones, which is exactly where an interior scene lives, and it is a large part of a\n * measured ~1.7x that survived correcting the colour space and the missing Lambert pi.\n * An approximation of a standard is the wrong thing to hold when the standard is the\n * specification.\n */\nvec3 rrtAndOdtFit(vec3 v) {\n vec3 a = v * (v + 0.0245786) - 0.000090537;\n vec3 b = v * (0.983729 * v + 0.4329510) + 0.238081;\n return a / b;\n}\n\nvec3 acesFilmic(vec3 x) {\n /*\n * Exposure first, because a tone curve is only defined relative to one.\n *\n * The reference RRT and ODT are graded for a dark cinema. Every renderer that ships ACES\n * scales its input to suit whatever it is actually shown on, and the factor each one\n * picks is a *look*, not a standard - so this engine takes no position and reads it from\n * the caller. Neutral by default, which is the reference grade unmodified. three.js's own\n * pick is 1 / 0.6, for a consumer matching one.\n */\n x *= uOutputExposure;\n // Columns, as GLSL reads them: sRGB -> XYZ -> D65_2_D60 -> AP1 -> RRT_SAT.\n const mat3 ACES_INPUT = mat3(\n 0.59719, 0.07600, 0.02840,\n 0.35458, 0.90834, 0.13383,\n 0.04823, 0.01566, 0.83777\n );\n // ODT_SAT -> XYZ -> D60_2_D65 -> sRGB.\n const mat3 ACES_OUTPUT = mat3(\n 1.60475, -0.10208, -0.00327,\n -0.53108, 1.10813, -0.07276,\n -0.07367, -0.00605, 1.07602\n );\n return clamp(ACES_OUTPUT * rrtAndOdtFit(ACES_INPUT * x), 0.0, 1.0);\n}\n\n\n/*\n * Linear to sRGB. The piecewise form rather than a plain pow: the linear toe near black\n * is where banding lives in an 8-bit buffer, and the cheap approximation puts it in the\n * wrong place.\n */\nvec3 linearToSrgb(vec3 c) {\n vec3 low = c * 12.92;\n vec3 high = 1.055 * pow(max(c, vec3(0.0)), vec3(1.0 / 2.4)) - 0.055;\n return mix(high, low, step(c, vec3(0.0031308)));\n}\n\n\nvec3 highlightShoulder(vec3 c) {\n float m = max(c.r, max(c.g, c.b));\n if (m <= 0.8) return c;\n float e = m - 0.8;\n return c * ((0.8 + 0.2 * e / (e + 0.2)) / m);\n}\n\nvec3 applyOutputTransform(vec3 c) {\n if (uOutputTransform == 0) return c;\n if (uOutputTransform == 2) c = acesFilmic(c);\n if (uOutputTransform == 3) c = highlightShoulder(c * uOutputExposure);\n return linearToSrgb(c);\n}\n\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 /* Graded where nothing after this will grade it, as every pass is. The alpha is coverage. */\n outColor = vec4(applyOutputTransform(col), alpha);\n}\n";
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { FOG_GLSL } from './fog.js';
|
|
2
|
+
import { OUTPUT_TRANSFORM_GLSL } from './outputTransform.js';
|
|
2
3
|
/**
|
|
3
4
|
* An arcane aura: the volume a floating object sits inside.
|
|
4
5
|
*
|
|
@@ -30,6 +31,8 @@ ${FOG_GLSL}
|
|
|
30
31
|
uniform int uNoiseOctaves;
|
|
31
32
|
uniform vec4 uClipPlane;
|
|
32
33
|
uniform int uClipEnabled;
|
|
34
|
+
/* Before \`uTint\`, where fire and smoke declare it, so one field table serves all three. */
|
|
35
|
+
${OUTPUT_TRANSFORM_GLSL}
|
|
33
36
|
/** The caller's colour, so an aura shifts with the world's own palette. */
|
|
34
37
|
uniform vec3 uTint;
|
|
35
38
|
|
|
@@ -97,6 +100,7 @@ void main() {
|
|
|
97
100
|
col = mix(col, mediumColor(), fog);
|
|
98
101
|
alpha *= 1.0 - fog;
|
|
99
102
|
|
|
100
|
-
|
|
103
|
+
/* Graded where nothing after this will grade it, as every pass is. The alpha is coverage. */
|
|
104
|
+
outColor = vec4(applyOutputTransform(col), alpha);
|
|
101
105
|
}
|
|
102
106
|
`;
|
|
@@ -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 // 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";
|
|
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\n/** 0 none, 1 sRGB, 2 ACES then sRGB, 3 the highlight shoulder then sRGB. See renderQuality.ts. */\nuniform int uOutputTransform;\n/** Scales the scene into the tone curve. 1 is the reference grade. See renderQuality.ts. */\nuniform float uOutputExposure;\n\n/*\n * ACES: the reference fit, not the cheap one.\n *\n * This was Narkowicz's single rational function, on the argument that the difference from\n * the reference curve is invisible next to the gain. That argument holds for a look and\n * fails for a *match*, which is what this option is for \u2014 aces exists so a world ported\n * from three.js reads the same, and three.js applies the full fit: into AP1 through\n * ACESInputMat, the RRT and ODT together, then back out through ACESOutputMat.\n *\n * The two curves are not a shade apart. Narkowicz is markedly brighter through the\n * midtones, which is exactly where an interior scene lives, and it is a large part of a\n * measured ~1.7x that survived correcting the colour space and the missing Lambert pi.\n * An approximation of a standard is the wrong thing to hold when the standard is the\n * specification.\n */\nvec3 rrtAndOdtFit(vec3 v) {\n vec3 a = v * (v + 0.0245786) - 0.000090537;\n vec3 b = v * (0.983729 * v + 0.4329510) + 0.238081;\n return a / b;\n}\n\nvec3 acesFilmic(vec3 x) {\n /*\n * Exposure first, because a tone curve is only defined relative to one.\n *\n * The reference RRT and ODT are graded for a dark cinema. Every renderer that ships ACES\n * scales its input to suit whatever it is actually shown on, and the factor each one\n * picks is a *look*, not a standard - so this engine takes no position and reads it from\n * the caller. Neutral by default, which is the reference grade unmodified. three.js's own\n * pick is 1 / 0.6, for a consumer matching one.\n */\n x *= uOutputExposure;\n // Columns, as GLSL reads them: sRGB -> XYZ -> D65_2_D60 -> AP1 -> RRT_SAT.\n const mat3 ACES_INPUT = mat3(\n 0.59719, 0.07600, 0.02840,\n 0.35458, 0.90834, 0.13383,\n 0.04823, 0.01566, 0.83777\n );\n // ODT_SAT -> XYZ -> D60_2_D65 -> sRGB.\n const mat3 ACES_OUTPUT = mat3(\n 1.60475, -0.10208, -0.00327,\n -0.53108, 1.10813, -0.07276,\n -0.07367, -0.00605, 1.07602\n );\n return clamp(ACES_OUTPUT * rrtAndOdtFit(ACES_INPUT * x), 0.0, 1.0);\n}\n\n\n/*\n * Linear to sRGB. The piecewise form rather than a plain pow: the linear toe near black\n * is where banding lives in an 8-bit buffer, and the cheap approximation puts it in the\n * wrong place.\n */\nvec3 linearToSrgb(vec3 c) {\n vec3 low = c * 12.92;\n vec3 high = 1.055 * pow(max(c, vec3(0.0)), vec3(1.0 / 2.4)) - 0.055;\n return mix(high, low, step(c, vec3(0.0031308)));\n}\n\n\nvec3 highlightShoulder(vec3 c) {\n float m = max(c.r, max(c.g, c.b));\n if (m <= 0.8) return c;\n float e = m - 0.8;\n return c * ((0.8 + 0.2 * e / (e + 0.2)) / m);\n}\n\nvec3 applyOutputTransform(vec3 c) {\n if (uOutputTransform == 0) return c;\n if (uOutputTransform == 2) c = acesFilmic(c);\n if (uOutputTransform == 3) c = highlightShoulder(c * uOutputExposure);\n return linearToSrgb(c);\n}\n\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 /* Graded where nothing after this will grade it, as every pass is, light added included. */\n outColor = vec4(applyOutputTransform(lit * energy * (1.0 - fog)), 1.0);\n}\n";
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { FOG_GLSL } from './fog.js';
|
|
2
|
+
import { OUTPUT_TRANSFORM_GLSL } from './outputTransform.js';
|
|
2
3
|
/**
|
|
3
4
|
* Electrical arcs: a filament with a saturating core and a wide soft glow.
|
|
4
5
|
*
|
|
@@ -92,6 +93,7 @@ ${FOG_GLSL}
|
|
|
92
93
|
uniform vec3 uCameraPos;
|
|
93
94
|
|
|
94
95
|
out vec4 outColor;
|
|
96
|
+
${OUTPUT_TRANSFORM_GLSL}
|
|
95
97
|
|
|
96
98
|
float hash11(float p) {
|
|
97
99
|
return fract(sin(p * 12.9898) * 43758.5453123);
|
|
@@ -162,6 +164,7 @@ void main() {
|
|
|
162
164
|
* faint: at a typical energy of 0.5 it delivered a quarter of what it asked for.
|
|
163
165
|
* Every emissive material here has to pick one channel and mean it.
|
|
164
166
|
*/
|
|
165
|
-
|
|
167
|
+
/* Graded where nothing after this will grade it, as every pass is, light added included. */
|
|
168
|
+
outColor = vec4(applyOutputTransform(lit * energy * (1.0 - fog)), 1.0);
|
|
166
169
|
}
|
|
167
170
|
`;
|