@driftengine/core 4.8.1 → 4.8.3
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 +142 -0
- package/README.md +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +8 -0
- package/dist/input/actionMap.d.ts +6 -1
- package/dist/input/actionMap.js +20 -7
- package/dist/input/input.d.ts +28 -0
- package/dist/input/input.js +71 -9
- package/dist/input/inputHarness.d.ts +9 -0
- package/dist/input/inputHarness.js +34 -2
- package/dist/render/backend/materialChanges.d.ts +7 -2
- package/dist/render/backend/materialChanges.js +8 -3
- package/dist/render/backend/webgl2/renderer.d.ts +75 -3
- package/dist/render/backend/webgl2/renderer.js +209 -22
- package/dist/render/backend/webgpu/buffers.d.ts +2 -0
- package/dist/render/backend/webgpu/buffers.js +2 -0
- package/dist/render/backend/webgpu/causticsPass.js +2 -1
- package/dist/render/backend/webgpu/depthPass.d.ts +13 -3
- package/dist/render/backend/webgpu/depthPass.js +4 -3
- package/dist/render/backend/webgpu/filmPass.js +2 -1
- package/dist/render/backend/webgpu/flatPass.d.ts +5 -0
- package/dist/render/backend/webgpu/flatPass.js +18 -8
- package/dist/render/backend/webgpu/linePass.js +2 -1
- package/dist/render/backend/webgpu/panelPass.js +2 -1
- package/dist/render/backend/webgpu/particlePass.js +2 -1
- package/dist/render/backend/webgpu/pipelineCache.d.ts +40 -8
- package/dist/render/backend/webgpu/pipelineCache.js +76 -9
- package/dist/render/backend/webgpu/plumePass.js +2 -1
- package/dist/render/backend/webgpu/postPass.d.ts +12 -2
- package/dist/render/backend/webgpu/postPass.js +19 -5
- package/dist/render/backend/webgpu/renderer.d.ts +66 -2
- package/dist/render/backend/webgpu/renderer.js +189 -38
- package/dist/render/backend/webgpu/sdfTextPass.js +2 -1
- package/dist/render/backend/webgpu/textPass.js +2 -1
- package/dist/render/backend/webgpu/waterPass.js +2 -1
- package/dist/render/backend/webgpu/windStreakPass.js +2 -1
- package/dist/render/boltBatch.js +2 -1
- package/dist/render/causticsRenderer.js +2 -1
- package/dist/render/cutoutCaster.d.ts +3 -1
- package/dist/render/cutoutCaster.js +4 -0
- package/dist/render/decalPass.js +2 -1
- package/dist/render/globalMediumPass.js +3 -2
- package/dist/render/lineBatch.js +5 -1
- package/dist/render/mapsWithoutUvs.d.ts +24 -0
- package/dist/render/mapsWithoutUvs.js +24 -0
- package/dist/render/mesh.d.ts +2 -0
- package/dist/render/mesh.js +3 -0
- package/dist/render/oitPass.js +2 -1
- package/dist/render/particleBatch.js +3 -2
- package/dist/render/particlePool.d.ts +21 -2
- package/dist/render/particlePool.js +72 -37
- package/dist/render/pass.d.ts +12 -0
- package/dist/render/plumeRenderer.js +3 -2
- package/dist/render/renderQuality.d.ts +16 -8
- package/dist/render/sceneCoverage.d.ts +54 -0
- package/dist/render/sceneCoverage.js +72 -0
- package/dist/render/sdfTextRenderer.js +2 -1
- package/dist/render/shaders/depth.js +3 -2
- package/dist/render/shaders/flat/glassTint.d.ts +28 -0
- package/dist/render/shaders/flat/glassTint.js +31 -0
- package/dist/render/shaders/flat/index.d.ts +12 -0
- package/dist/render/shaders/flat/index.js +13 -2
- package/dist/render/shaders/flat/main.d.ts +1 -1
- package/dist/render/shaders/flat/main.js +60 -25
- package/dist/render/shaders/flat/surfaceEffects.d.ts +1 -1
- package/dist/render/shaders/flat/surfaceEffects.js +6 -2
- package/dist/render/shaders/generated/depth.wgsl.d.ts +32 -12
- package/dist/render/shaders/generated/depth.wgsl.js +32 -12
- package/dist/render/shaders/generated/flat.wgsl.d.ts +89 -0
- package/dist/render/shaders/generated/flat.wgsl.js +127 -38
- 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 +3 -3
- package/dist/render/shaders/generated/particle.wgsl.js +3 -3
- package/dist/render/shaders/generated/rush.wgsl.d.ts +1 -1
- package/dist/render/shaders/generated/rush.wgsl.js +1 -1
- package/dist/render/shaders/generated/sdfText.wgsl.d.ts +1 -1
- package/dist/render/shaders/generated/sdfText.wgsl.js +1 -1
- package/dist/render/shaders/generated/temporalResolve.wgsl.d.ts +1 -1
- package/dist/render/shaders/generated/temporalResolve.wgsl.js +1 -1
- package/dist/render/shaders/line.d.ts +1 -1
- package/dist/render/shaders/outputTransform.d.ts +14 -1
- package/dist/render/shaders/outputTransform.js +23 -1
- package/dist/render/shaders/particle.d.ts +3 -3
- package/dist/render/shaders/recon/resolve.wgsl.js +15 -1
- package/dist/render/shaders/rush.js +13 -2
- package/dist/render/shaders/sdfText.d.ts +1 -1
- package/dist/render/shaders/temporalResolve.js +12 -5
- package/dist/render/ssrPass.js +2 -1
- package/dist/render/surfaceTexture.d.ts +13 -0
- package/dist/render/surfaceTexture.js +4 -0
- package/dist/render/textRenderer.js +2 -1
- package/dist/render/vertexDefaults.js +1 -0
- package/dist/render/viewModel.d.ts +29 -0
- package/dist/render/viewModel.js +35 -0
- package/dist/render/waterRenderer.js +2 -1
- package/dist/render/windStreakRenderer.js +2 -1
- package/package.json +6 -6
- package/src/index.ts +22 -1
- package/src/input/actionMap.ts +23 -8
- package/src/input/input.ts +79 -9
- package/src/input/inputHarness.ts +27 -2
- package/src/render/backend/materialChanges.ts +8 -3
- package/src/render/backend/webgl2/renderer.ts +223 -29
- package/src/render/backend/webgpu/buffers.ts +5 -0
- package/src/render/backend/webgpu/causticsPass.ts +2 -1
- package/src/render/backend/webgpu/depthPass.ts +4 -3
- package/src/render/backend/webgpu/filmPass.ts +2 -1
- package/src/render/backend/webgpu/flatPass.ts +43 -26
- package/src/render/backend/webgpu/linePass.ts +2 -1
- package/src/render/backend/webgpu/panelPass.ts +2 -1
- package/src/render/backend/webgpu/particlePass.ts +2 -1
- package/src/render/backend/webgpu/pipelineCache.ts +82 -8
- package/src/render/backend/webgpu/plumePass.ts +2 -1
- package/src/render/backend/webgpu/postPass.ts +20 -5
- package/src/render/backend/webgpu/renderer.ts +191 -37
- package/src/render/backend/webgpu/sdfTextPass.ts +2 -1
- package/src/render/backend/webgpu/textPass.ts +2 -1
- package/src/render/backend/webgpu/waterPass.ts +2 -1
- package/src/render/backend/webgpu/windStreakPass.ts +2 -1
- package/src/render/boltBatch.ts +2 -1
- package/src/render/causticsRenderer.ts +2 -1
- package/src/render/cutoutCaster.ts +15 -2
- package/src/render/decalPass.ts +2 -1
- package/src/render/globalMediumPass.ts +3 -2
- package/src/render/lineBatch.ts +3 -1
- package/src/render/mapsWithoutUvs.ts +43 -0
- package/src/render/mesh.ts +3 -0
- package/src/render/oitPass.ts +2 -1
- package/src/render/particleBatch.ts +3 -2
- package/src/render/particlePool.ts +107 -56
- package/src/render/pass.ts +12 -0
- package/src/render/plumeRenderer.ts +3 -2
- package/src/render/renderQuality.ts +16 -8
- package/src/render/sceneCoverage.ts +78 -0
- package/src/render/sdfTextRenderer.ts +2 -1
- package/src/render/shaders/depth.ts +3 -2
- package/src/render/shaders/flat/glassTint.ts +48 -0
- package/src/render/shaders/flat/index.ts +25 -2
- package/src/render/shaders/flat/main.ts +60 -25
- package/src/render/shaders/flat/surfaceEffects.ts +6 -2
- package/src/render/shaders/generated/depth.wgsl.ts +32 -12
- package/src/render/shaders/generated/flat.wgsl.ts +127 -38
- package/src/render/shaders/generated/line.wgsl.ts +1 -1
- package/src/render/shaders/generated/particle.wgsl.ts +3 -3
- package/src/render/shaders/generated/rush.wgsl.ts +1 -1
- package/src/render/shaders/generated/sdfText.wgsl.ts +1 -1
- package/src/render/shaders/generated/temporalResolve.wgsl.ts +1 -1
- package/src/render/shaders/outputTransform.ts +24 -1
- package/src/render/shaders/recon/resolve.wgsl.ts +15 -1
- package/src/render/shaders/rush.ts +13 -2
- package/src/render/shaders/temporalResolve.ts +12 -5
- package/src/render/ssrPass.ts +2 -1
- package/src/render/surfaceTexture.ts +16 -0
- package/src/render/textRenderer.ts +2 -1
- package/src/render/vertexDefaults.ts +1 -0
- package/src/render/viewModel.ts +41 -0
- package/src/render/waterRenderer.ts +2 -1
- package/src/render/windStreakRenderer.ts +2 -1
|
@@ -26,6 +26,19 @@
|
|
|
26
26
|
* grades, so a pass that also graded would apply a curve to its own output; every upload site
|
|
27
27
|
* gates on the same `gradeHere` the mesh pass uses.
|
|
28
28
|
*/
|
|
29
|
+
/**
|
|
30
|
+
* A highlight shoulder that keeps a colour's hue: past 0.8 the brightest channel eases toward 1 —
|
|
31
|
+
* `0.8 + 0.2·e / (e + 0.2)` of an excess `e` — and the other two are scaled by the same factor, so
|
|
32
|
+
* an overbright red stays red rather than clipping channel by channel toward white.
|
|
33
|
+
*
|
|
34
|
+
* **Per channel is what sRGB alone does, and it is a hue shift**: `(1.6, 0.4, 0.2)` clips to
|
|
35
|
+
* `(1, 0.4, 0.2)`, a pinker colour and a wider core, where this gives `(0.96, 0.24, 0.12)`. The
|
|
36
|
+
* curve is the one a ported game's own resolve applies, read out of its build, so a port matching
|
|
37
|
+
* it reads the same. **What it gives up** is ACES's filmic toe and saturation roll-off: it touches
|
|
38
|
+
* nothing below 0.8 and never desaturates, so a scene that wants a film's look still wants ACES.
|
|
39
|
+
* Shared by the forward passes and the resolve, so the two cannot disagree about it.
|
|
40
|
+
*/
|
|
41
|
+
export declare const HIGHLIGHT_SHOULDER_GLSL = "\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";
|
|
29
42
|
/**
|
|
30
43
|
* `uOutputTransform` and `uOutputExposure`, the fit, and `applyOutputTransform`.
|
|
31
44
|
*
|
|
@@ -34,4 +47,4 @@
|
|
|
34
47
|
* caller's, per draw, and a permutation per output transform would double a pipeline count for
|
|
35
48
|
* a branch that costs one compare.
|
|
36
49
|
*/
|
|
37
|
-
export declare const OUTPUT_TRANSFORM_GLSL = "\n/** 0 none, 1 sRGB, 2 ACES 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 * 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\nvec3 applyOutputTransform(vec3 c) {\n if (uOutputTransform == 0) return c;\n if (uOutputTransform == 2) c = acesFilmic(c);\n return linearToSrgb(c);\n}\n";
|
|
50
|
+
export declare const OUTPUT_TRANSFORM_GLSL = "\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 * 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";
|
|
@@ -26,6 +26,26 @@
|
|
|
26
26
|
* grades, so a pass that also graded would apply a curve to its own output; every upload site
|
|
27
27
|
* gates on the same `gradeHere` the mesh pass uses.
|
|
28
28
|
*/
|
|
29
|
+
/**
|
|
30
|
+
* A highlight shoulder that keeps a colour's hue: past 0.8 the brightest channel eases toward 1 —
|
|
31
|
+
* `0.8 + 0.2·e / (e + 0.2)` of an excess `e` — and the other two are scaled by the same factor, so
|
|
32
|
+
* an overbright red stays red rather than clipping channel by channel toward white.
|
|
33
|
+
*
|
|
34
|
+
* **Per channel is what sRGB alone does, and it is a hue shift**: `(1.6, 0.4, 0.2)` clips to
|
|
35
|
+
* `(1, 0.4, 0.2)`, a pinker colour and a wider core, where this gives `(0.96, 0.24, 0.12)`. The
|
|
36
|
+
* curve is the one a ported game's own resolve applies, read out of its build, so a port matching
|
|
37
|
+
* it reads the same. **What it gives up** is ACES's filmic toe and saturation roll-off: it touches
|
|
38
|
+
* nothing below 0.8 and never desaturates, so a scene that wants a film's look still wants ACES.
|
|
39
|
+
* Shared by the forward passes and the resolve, so the two cannot disagree about it.
|
|
40
|
+
*/
|
|
41
|
+
export const HIGHLIGHT_SHOULDER_GLSL = `
|
|
42
|
+
vec3 highlightShoulder(vec3 c) {
|
|
43
|
+
float m = max(c.r, max(c.g, c.b));
|
|
44
|
+
if (m <= 0.8) return c;
|
|
45
|
+
float e = m - 0.8;
|
|
46
|
+
return c * ((0.8 + 0.2 * e / (e + 0.2)) / m);
|
|
47
|
+
}
|
|
48
|
+
`;
|
|
29
49
|
/**
|
|
30
50
|
* `uOutputTransform` and `uOutputExposure`, the fit, and `applyOutputTransform`.
|
|
31
51
|
*
|
|
@@ -35,7 +55,7 @@
|
|
|
35
55
|
* a branch that costs one compare.
|
|
36
56
|
*/
|
|
37
57
|
export const OUTPUT_TRANSFORM_GLSL = `
|
|
38
|
-
/** 0 none, 1 sRGB, 2 ACES then sRGB. See renderQuality.ts. */
|
|
58
|
+
/** 0 none, 1 sRGB, 2 ACES then sRGB, 3 the highlight shoulder then sRGB. See renderQuality.ts. */
|
|
39
59
|
uniform int uOutputTransform;
|
|
40
60
|
/** Scales the scene into the tone curve. 1 is the reference grade. See renderQuality.ts. */
|
|
41
61
|
uniform float uOutputExposure;
|
|
@@ -98,9 +118,11 @@ vec3 linearToSrgb(vec3 c) {
|
|
|
98
118
|
return mix(high, low, step(c, vec3(0.0031308)));
|
|
99
119
|
}
|
|
100
120
|
|
|
121
|
+
${HIGHLIGHT_SHOULDER_GLSL}
|
|
101
122
|
vec3 applyOutputTransform(vec3 c) {
|
|
102
123
|
if (uOutputTransform == 0) return c;
|
|
103
124
|
if (uOutputTransform == 2) c = acesFilmic(c);
|
|
125
|
+
if (uOutputTransform == 3) c = highlightShoulder(c * uOutputExposure);
|
|
104
126
|
return linearToSrgb(c);
|
|
105
127
|
}
|
|
106
128
|
`;
|
|
@@ -47,7 +47,7 @@ export declare const PARTICLE_VERT = "#version 300 es\n/** Per-vertex: the quad
|
|
|
47
47
|
* and unlit smoke in a world lit by fires is the tell that gives an effect away
|
|
48
48
|
* fastest. A puff catching a flame's colour is most of what makes it belong.
|
|
49
49
|
*/
|
|
50
|
-
export declare const PARTICLE_SMOKE_FRAG = "#version 300 es\nprecision highp float;\n\nin vec2 vUv;\nin vec3 vColor;\nin float vAlpha;\nin float vAge;\nin float vSeed;\nin vec3 vWorldPos;\nin vec3 vNormal;\n\nuniform float uTime;\nuniform vec3 uDirectionalDir;\nuniform vec3 uDirectionalColor;\nuniform vec3 uAmbient;\nuniform vec3 uCameraPos;\nuniform int uNoiseOctaves;\n/** How strongly the noise eats into the puff. 0 is a plain soft ball. */\nuniform float uErosion;\n\n#define MAX_LIGHTS 16\nuniform int uLightCount;\nuniform vec3 uLightPos[MAX_LIGHTS];\nuniform vec3 uLightColor[MAX_LIGHTS];\nuniform float uLightRadius[MAX_LIGHTS];\n// How present each light is, 0 to 1. See pointLightSelection.ts.\nuniform float uLightWeight[MAX_LIGHTS];\n\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\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\nfloat billow(vec2 p, float t, int octaves) {\n float v = valueNoise(p * 2.4 + vec2(0.0, -t * 0.5)) * 0.62;\n if (octaves == 1) return v / 0.62;\n v += valueNoise(p * 5.1 + vec2(t * 0.15, -t * 0.8)) * 0.38;\n return v / 1.0;\n}\n\n\nout vec4 outColor;\n\n/** 0 none, 1 sRGB, 2 ACES 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 * 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\nvec3 applyOutputTransform(vec3 c) {\n if (uOutputTransform == 0) return c;\n if (uOutputTransform == 2) c = acesFilmic(c);\n return linearToSrgb(c);\n}\n\n\nvoid main() {\n /*\n * A round, soft body first. 'smoothstep' rather than a hard disc: the edge is\n * the entire difference between a puff and a sprite, and a cut-off circle reads\n * as a decal however good the noise inside it is.\n */\n float r = length(vUv);\n float body = 1.0 - smoothstep(0.35, 1.0, r);\n if (body <= 0.0) discard;\n\n /*\n * Then eaten into by noise that drifts and coarsens as the puff ages, so it\n * dissipates by *breaking up* rather than by uniformly thinning. Uniform\n * thinning is what a fading sprite does, and it is what the old cube did.\n */\n float t = uTime * 0.55 + vSeed * 7.31;\n float n = billow(vUv * (0.9 + vAge * 1.4) + vec2(vSeed * 3.7, 0.0), t, uNoiseOctaves);\n float erode = mix(1.0, n, uErosion * (0.35 + vAge * 0.65));\n float mask = clamp(body * erode, 0.0, 1.0);\n // Feather the last of it out rather than letting a noise threshold flicker.\n mask *= smoothstep(0.0, 0.25, mask);\n if (mask <= 0.002) discard;\n\n vec3 n3 = normalize(vNormal);\n /*\n * Wrapped diffuse. Smoke transmits as well as reflects, so the terminator on a\n * puff sits well past 90\u00B0 \u2014 a hard 'max(dot, 0)' gives a lump with a black side,\n * which is exactly the \"coloured box\" failure the fidelity rules name.\n */\n float ndl = dot(n3, uDirectionalDir) * 0.5 + 0.5;\n vec3 lit = uAmbient + uDirectionalColor * ndl;\n\n for (int i = 0; i < MAX_LIGHTS; i++) {\n if (i >= uLightCount) break;\n vec3 toLight = uLightPos[i] - vWorldPos;\n float dist = length(toLight);\n float radius = uLightRadius[i];\n if (radius <= 0.0 || dist >= radius) continue;\n float falloff = 1.0 - dist / radius;\n float wrapped = dot(n3, toLight / max(dist, 1e-4)) * 0.5 + 0.5;\n lit += uLightColor[i] * falloff * falloff * wrapped * uLightWeight[i];\n }\n\n vec3 shaded = vColor * lit;\n float fog = mediumFog(distance(vWorldPos, uCameraPos), vWorldPos.y);\n outColor = vec4(applyOutputTransform(mix(shaded, mediumColor(), fog)), mask * vAlpha);\n}\n";
|
|
50
|
+
export declare const PARTICLE_SMOKE_FRAG = "#version 300 es\nprecision highp float;\n\nin vec2 vUv;\nin vec3 vColor;\nin float vAlpha;\nin float vAge;\nin float vSeed;\nin vec3 vWorldPos;\nin vec3 vNormal;\n\nuniform float uTime;\nuniform vec3 uDirectionalDir;\nuniform vec3 uDirectionalColor;\nuniform vec3 uAmbient;\nuniform vec3 uCameraPos;\nuniform int uNoiseOctaves;\n/** How strongly the noise eats into the puff. 0 is a plain soft ball. */\nuniform float uErosion;\n\n#define MAX_LIGHTS 16\nuniform int uLightCount;\nuniform vec3 uLightPos[MAX_LIGHTS];\nuniform vec3 uLightColor[MAX_LIGHTS];\nuniform float uLightRadius[MAX_LIGHTS];\n// How present each light is, 0 to 1. See pointLightSelection.ts.\nuniform float uLightWeight[MAX_LIGHTS];\n\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\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\nfloat billow(vec2 p, float t, int octaves) {\n float v = valueNoise(p * 2.4 + vec2(0.0, -t * 0.5)) * 0.62;\n if (octaves == 1) return v / 0.62;\n v += valueNoise(p * 5.1 + vec2(t * 0.15, -t * 0.8)) * 0.38;\n return v / 1.0;\n}\n\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 * 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\nvoid main() {\n /*\n * A round, soft body first. 'smoothstep' rather than a hard disc: the edge is\n * the entire difference between a puff and a sprite, and a cut-off circle reads\n * as a decal however good the noise inside it is.\n */\n float r = length(vUv);\n float body = 1.0 - smoothstep(0.35, 1.0, r);\n if (body <= 0.0) discard;\n\n /*\n * Then eaten into by noise that drifts and coarsens as the puff ages, so it\n * dissipates by *breaking up* rather than by uniformly thinning. Uniform\n * thinning is what a fading sprite does, and it is what the old cube did.\n */\n float t = uTime * 0.55 + vSeed * 7.31;\n float n = billow(vUv * (0.9 + vAge * 1.4) + vec2(vSeed * 3.7, 0.0), t, uNoiseOctaves);\n float erode = mix(1.0, n, uErosion * (0.35 + vAge * 0.65));\n float mask = clamp(body * erode, 0.0, 1.0);\n // Feather the last of it out rather than letting a noise threshold flicker.\n mask *= smoothstep(0.0, 0.25, mask);\n if (mask <= 0.002) discard;\n\n vec3 n3 = normalize(vNormal);\n /*\n * Wrapped diffuse. Smoke transmits as well as reflects, so the terminator on a\n * puff sits well past 90\u00B0 \u2014 a hard 'max(dot, 0)' gives a lump with a black side,\n * which is exactly the \"coloured box\" failure the fidelity rules name.\n */\n float ndl = dot(n3, uDirectionalDir) * 0.5 + 0.5;\n vec3 lit = uAmbient + uDirectionalColor * ndl;\n\n for (int i = 0; i < MAX_LIGHTS; i++) {\n if (i >= uLightCount) break;\n vec3 toLight = uLightPos[i] - vWorldPos;\n float dist = length(toLight);\n float radius = uLightRadius[i];\n if (radius <= 0.0 || dist >= radius) continue;\n float falloff = 1.0 - dist / radius;\n float wrapped = dot(n3, toLight / max(dist, 1e-4)) * 0.5 + 0.5;\n lit += uLightColor[i] * falloff * falloff * wrapped * uLightWeight[i];\n }\n\n vec3 shaded = vColor * lit;\n float fog = mediumFog(distance(vWorldPos, uCameraPos), vWorldPos.y);\n outColor = vec4(applyOutputTransform(mix(shaded, mediumColor(), fog)), mask * vAlpha);\n}\n";
|
|
51
51
|
/**
|
|
52
52
|
* Sparks and embers: additive, hot in the middle, and stretched along travel.
|
|
53
53
|
*
|
|
@@ -55,7 +55,7 @@ export declare const PARTICLE_SMOKE_FRAG = "#version 300 es\nprecision highp flo
|
|
|
55
55
|
* is what turned the first version into a grey cube at night. The core is allowed
|
|
56
56
|
* past white so it reads as incandescent rather than as a pale dot.
|
|
57
57
|
*/
|
|
58
|
-
export declare const PARTICLE_SPARK_FRAG = "#version 300 es\nprecision highp float;\n\nin vec2 vUv;\nin vec3 vColor;\nin float vAlpha;\nin float vAge;\nin float vSeed;\nin vec3 vWorldPos;\nin vec3 vNormal;\n\nuniform float uTime;\nuniform vec3 uCameraPos;\n/** How far past white the centre may 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(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\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\nfloat billow(vec2 p, float t, int octaves) {\n float v = valueNoise(p * 2.4 + vec2(0.0, -t * 0.5)) * 0.62;\n if (octaves == 1) return v / 0.62;\n v += valueNoise(p * 5.1 + vec2(t * 0.15, -t * 0.8)) * 0.38;\n return v / 1.0;\n}\n\n\nout vec4 outColor;\n\n/** 0 none, 1 sRGB, 2 ACES 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 * 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\nvec3 applyOutputTransform(vec3 c) {\n if (uOutputTransform == 0) return c;\n if (uOutputTransform == 2) c = acesFilmic(c);\n return linearToSrgb(c);\n}\n\n\nvoid main() {\n float r = length(vUv);\n if (r > 1.0) discard;\n\n /*\n * Two falloffs again \u2014 a tight core and a broad halo \u2014 because one gives either\n * a hard dot or a soft smudge. The halo is what makes a shower of embers read\n * as glowing air rather than as a scatter of points.\n */\n float core = pow(1.0 - clamp(r, 0.0, 1.0), 6.0);\n float halo = pow(1.0 - clamp(r, 0.0, 1.0), 1.6);\n\n /*\n * Embers flicker as they tumble and cool. Per-particle phase from the seed, so\n * a shower crackles instead of pulsing as one body.\n */\n float flick = 0.7 + 0.3 * valueNoise(vec2(uTime * 9.0 + vSeed * 13.0, vSeed));\n\n // Cooling: the last of a spark's life is dimmer as well as smaller.\n float cool = 1.0 - vAge * vAge;\n float energy = (halo * 0.55 + core * uCoreGain) * flick * cool * vAlpha;\n\n float fog = mediumFog(distance(vWorldPos, uCameraPos), vWorldPos.y);\n energy *= 1.0 - fog;\n /*\n * Alpha 1, energy in the colour \u2014 see the note in 'bolt.ts'. Additive blending is\n * 'rgb * a', so carrying the energy in both channels squares it and quietly halves\n * every emissive effect in the game.\n */\n outColor = vec4(applyOutputTransform(vColor * energy), 1.0);\n}\n";
|
|
58
|
+
export declare const PARTICLE_SPARK_FRAG = "#version 300 es\nprecision highp float;\n\nin vec2 vUv;\nin vec3 vColor;\nin float vAlpha;\nin float vAge;\nin float vSeed;\nin vec3 vWorldPos;\nin vec3 vNormal;\n\nuniform float uTime;\nuniform vec3 uCameraPos;\n/** How far past white the centre may 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(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\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\nfloat billow(vec2 p, float t, int octaves) {\n float v = valueNoise(p * 2.4 + vec2(0.0, -t * 0.5)) * 0.62;\n if (octaves == 1) return v / 0.62;\n v += valueNoise(p * 5.1 + vec2(t * 0.15, -t * 0.8)) * 0.38;\n return v / 1.0;\n}\n\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 * 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\nvoid main() {\n float r = length(vUv);\n if (r > 1.0) discard;\n\n /*\n * Two falloffs again \u2014 a tight core and a broad halo \u2014 because one gives either\n * a hard dot or a soft smudge. The halo is what makes a shower of embers read\n * as glowing air rather than as a scatter of points.\n */\n float core = pow(1.0 - clamp(r, 0.0, 1.0), 6.0);\n float halo = pow(1.0 - clamp(r, 0.0, 1.0), 1.6);\n\n /*\n * Embers flicker as they tumble and cool. Per-particle phase from the seed, so\n * a shower crackles instead of pulsing as one body.\n */\n float flick = 0.7 + 0.3 * valueNoise(vec2(uTime * 9.0 + vSeed * 13.0, vSeed));\n\n // Cooling: the last of a spark's life is dimmer as well as smaller.\n float cool = 1.0 - vAge * vAge;\n float energy = (halo * 0.55 + core * uCoreGain) * flick * cool * vAlpha;\n\n float fog = mediumFog(distance(vWorldPos, uCameraPos), vWorldPos.y);\n energy *= 1.0 - fog;\n /*\n * Alpha 1, energy in the colour \u2014 see the note in 'bolt.ts'. Additive blending is\n * 'rgb * a', so carrying the energy in both channels squares it and quietly halves\n * every emissive effect in the game.\n */\n outColor = vec4(applyOutputTransform(vColor * energy), 1.0);\n}\n";
|
|
59
59
|
/**
|
|
60
60
|
* Dust motes and drifting glints: unlit, alpha blended, and fogged only when the caller asks.
|
|
61
61
|
*
|
|
@@ -79,4 +79,4 @@ export declare const PARTICLE_SPARK_FRAG = "#version 300 es\nprecision highp flo
|
|
|
79
79
|
* option and stay unconditionally fogged, matching every particle drawn before this material
|
|
80
80
|
* existed; see `ParticleBatchOptions.fog`.
|
|
81
81
|
*/
|
|
82
|
-
export declare const PARTICLE_MOTE_FRAG = "#version 300 es\nprecision highp float;\n\nin vec2 vUv;\nin vec3 vColor;\nin float vAlpha;\nin float vAge;\nin float vSeed;\nin vec3 vWorldPos;\nin vec3 vNormal;\n\nuniform vec3 uCameraPos;\n/**\n * Whether the fragment stage mixes toward the medium's own colour at all. 0 \u2014 what a batch\n * gets by leaving ParticleBatchOptions.fog unset \u2014 leaves vColor untouched by distance, which\n * is what a caller reaching for this material over 'spark'/'smoke' is usually asking for: an\n * unlit point drawn exactly as its own colour says. 1 mixes toward mediumColor() by\n * mediumFog(), the same medium every other pass in the frame recedes into.\n */\nuniform int uFogEnabled;\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/** 0 none, 1 sRGB, 2 ACES 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 * 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\nvec3 applyOutputTransform(vec3 c) {\n if (uOutputTransform == 0) return c;\n if (uOutputTransform == 2) c = acesFilmic(c);\n return linearToSrgb(c);\n}\n\n\nvoid main() {\n /*\n * One soft, round falloff \u2014 no erosion, no core past white, because this material draws\n * neither a body being eaten by noise nor a light source. Squared rather than linear so the\n * edge feathers instead of ramping, which is what keeps a field of these reading as dust\n * rather than as a scatter of hard-edged discs.\n */\n float r = length(vUv);\n float body = 1.0 - smoothstep(0.4, 1.0, r);\n if (body <= 0.0) discard;\n float alpha = body * body * vAlpha;\n if (alpha <= 0.002) discard;\n\n vec3 color = vColor;\n if (uFogEnabled != 0) {\n float fog = mediumFog(distance(vWorldPos, uCameraPos), vWorldPos.y);\n color = mix(color, mediumColor(), fog);\n }\n /*\n * A real alpha blend (SRC_ALPHA, ONE_MINUS_SRC_ALPHA) rather than spark's energy-in-colour\n * trick: this point genuinely occludes what little is behind it instead of adding light to it.\n */\n outColor = vec4(applyOutputTransform(color), alpha);\n}\n";
|
|
82
|
+
export declare const PARTICLE_MOTE_FRAG = "#version 300 es\nprecision highp float;\n\nin vec2 vUv;\nin vec3 vColor;\nin float vAlpha;\nin float vAge;\nin float vSeed;\nin vec3 vWorldPos;\nin vec3 vNormal;\n\nuniform vec3 uCameraPos;\n/**\n * Whether the fragment stage mixes toward the medium's own colour at all. 0 \u2014 what a batch\n * gets by leaving ParticleBatchOptions.fog unset \u2014 leaves vColor untouched by distance, which\n * is what a caller reaching for this material over 'spark'/'smoke' is usually asking for: an\n * unlit point drawn exactly as its own colour says. 1 mixes toward mediumColor() by\n * mediumFog(), the same medium every other pass in the frame recedes into.\n */\nuniform int uFogEnabled;\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/** 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 * 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\nvoid main() {\n /*\n * One soft, round falloff \u2014 no erosion, no core past white, because this material draws\n * neither a body being eaten by noise nor a light source. Squared rather than linear so the\n * edge feathers instead of ramping, which is what keeps a field of these reading as dust\n * rather than as a scatter of hard-edged discs.\n */\n float r = length(vUv);\n float body = 1.0 - smoothstep(0.4, 1.0, r);\n if (body <= 0.0) discard;\n float alpha = body * body * vAlpha;\n if (alpha <= 0.002) discard;\n\n vec3 color = vColor;\n if (uFogEnabled != 0) {\n float fog = mediumFog(distance(vWorldPos, uCameraPos), vWorldPos.y);\n color = mix(color, mediumColor(), fog);\n }\n /*\n * A real alpha blend (SRC_ALPHA, ONE_MINUS_SRC_ALPHA) rather than spark's energy-in-colour\n * trick: this point genuinely occludes what little is behind it instead of adding light to it.\n */\n outColor = vec4(applyOutputTransform(color), alpha);\n}\n";
|
|
@@ -943,6 +943,19 @@ fn reconSceneBilinear(p: vec2<f32>) -> vec3<f32> {
|
|
|
943
943
|
return textureSampleLevel(sceneTexture, linearSampler, uv, 0.0).xyz;
|
|
944
944
|
}
|
|
945
945
|
|
|
946
|
+
/*
|
|
947
|
+
* The surface's share of an output pixel, read from the jittered scene it was rebuilt from
|
|
948
|
+
* (sceneCoverage.ts): what the blended draws that landed before the reconstruction left of it.
|
|
949
|
+
* The late ones then cover the picture this is stored in, so the composite's occlusion reads both.
|
|
950
|
+
* Bilinear and at the unjittered position, which is where the colour beside it stands.
|
|
951
|
+
*/
|
|
952
|
+
fn reconSceneShare(ox: i32, oy: i32) -> f32 {
|
|
953
|
+
let output = vec2<f32>(f32(recon.outputSize.x), f32(recon.outputSize.y));
|
|
954
|
+
let render = vec2<f32>(f32(recon.renderSize.x), f32(recon.renderSize.y));
|
|
955
|
+
let at = (vec2<f32>(f32(ox), f32(oy)) + 0.5) / output + recon.jitter / render;
|
|
956
|
+
return textureSampleLevel(sceneTexture, linearSampler, vec2<f32>(at.x, 1.0 - at.y), 0.0).w;
|
|
957
|
+
}
|
|
958
|
+
|
|
946
959
|
fn reconHistoryBilinear(p: vec2<f32>) -> vec4<f32> {
|
|
947
960
|
let size = vec2<f32>(f32(recon.outputSize.x), f32(recon.outputSize.y));
|
|
948
961
|
let uv = vec2<f32>(p.x + 0.5, size.y - 0.5 - p.y) / size;
|
|
@@ -965,7 +978,8 @@ fn sharpenMain(@builtin(global_invocation_id) id: vec3<u32>) {
|
|
|
965
978
|
if (id.x >= recon.outputSize.x || id.y >= recon.outputSize.y) { return; }
|
|
966
979
|
let y = i32(recon.outputSize.y) - 1 - i32(id.y);
|
|
967
980
|
let value = reconSharpenAt(i32(id.x), y);
|
|
968
|
-
|
|
981
|
+
let share = reconSceneShare(i32(id.x), y);
|
|
982
|
+
textureStore(resolved, vec2<i32>(i32(id.x), i32(id.y)), vec4<f32>(value, share));
|
|
969
983
|
}
|
|
970
984
|
`;
|
|
971
985
|
}
|
|
@@ -24,6 +24,7 @@ import { DEPTH_OF_FIELD_GLSL } from './depthOfField.js';
|
|
|
24
24
|
import { AUTO_EXPOSURE_GLSL } from './exposure.js';
|
|
25
25
|
import { LOCAL_EXPOSURE_GLSL } from './localExposure.js';
|
|
26
26
|
import { FILM_LOOK_GLSL } from './filmLook.js';
|
|
27
|
+
import { HIGHLIGHT_SHOULDER_GLSL } from './outputTransform.js';
|
|
27
28
|
export const RUSH_FRAG = `#version 300 es
|
|
28
29
|
precision highp float;
|
|
29
30
|
/*
|
|
@@ -240,9 +241,11 @@ vec3 acesFilmic(vec3 x) {
|
|
|
240
241
|
return clamp(ACES_OUTPUT * (a / b), 0.0, 1.0);
|
|
241
242
|
}
|
|
242
243
|
|
|
244
|
+
${HIGHLIGHT_SHOULDER_GLSL}
|
|
243
245
|
vec3 grade(vec3 c) {
|
|
244
246
|
if (uOutputTransform == 0) return c;
|
|
245
247
|
if (uOutputTransform == 2) c = acesFilmic(c);
|
|
248
|
+
if (uOutputTransform == 3) c = highlightShoulder(c * uOutputExposure);
|
|
246
249
|
return linearToSrgb(c);
|
|
247
250
|
}
|
|
248
251
|
|
|
@@ -333,7 +336,8 @@ vec3 finish(vec3 light) {
|
|
|
333
336
|
}
|
|
334
337
|
|
|
335
338
|
void main() {
|
|
336
|
-
|
|
339
|
+
vec4 sampled = textureLod(uScene, vUv, 0.0);
|
|
340
|
+
vec3 scene = sampled.rgb;
|
|
337
341
|
if (uMotionStrength > 0.0) scene = cameraBlur(scene);
|
|
338
342
|
/*
|
|
339
343
|
* Defocus after the camera smear and before everything else, because a lens is the last thing
|
|
@@ -348,8 +352,15 @@ void main() {
|
|
|
348
352
|
* not then darkened again along its own trail. mix from 1 rather than a multiply by the
|
|
349
353
|
* strength, so the parameter reads as "how much of this occlusion", and 0 is exactly the
|
|
350
354
|
* frame that existed before the effect did.
|
|
355
|
+
*
|
|
356
|
+
* **Scaled by the surface's share of the pixel**, which the scene carries in its alpha
|
|
357
|
+
* (sceneCoverage.ts): the occlusion was measured from the depth the opaque world left, and a
|
|
358
|
+
* pane, a puff of smoke or a caption in front of a corner wrote no depth, so darkening all of the
|
|
359
|
+
* pixel darkened them by a corner they hide. Where nothing blended landed the share is 1 and
|
|
360
|
+
* this is the frame it was.
|
|
351
361
|
*/
|
|
352
|
-
float
|
|
362
|
+
float share = clamp(sampled.a, 0.0, 1.0);
|
|
363
|
+
float ao = mix(1.0, textureLod(uAo, vUv + uAoOffset, 0.0).r, uAoStrength * share);
|
|
353
364
|
|
|
354
365
|
/*
|
|
355
366
|
* Distance from the centre, corrected so the falloff is a circle on screen rather than
|
|
@@ -6,4 +6,4 @@
|
|
|
6
6
|
* the alpha test is not uniform control flow.
|
|
7
7
|
*/
|
|
8
8
|
export declare const SDF_TEXT_VERT = "#version 300 es\nprecision highp float;\n\nlayout(location = 0) in vec3 aPosition;\nlayout(location = 1) in vec2 aUv;\n\nuniform mat4 uViewProj;\nuniform mat4 uModel;\n\nout vec2 vUv;\n\nvoid main() {\n vUv = aUv;\n gl_Position = uViewProj * uModel * vec4(aPosition, 1.0);\n}\n";
|
|
9
|
-
export declare const SDF_TEXT_FRAG = "#version 300 es\nprecision highp float;\n\nin vec2 vUv;\n\n/* An array of one: every surface texture is an array, and an atlas is read at layer 0. */\nuniform mediump sampler2DArray uAtlas;\nuniform vec3 uColor;\nuniform float uOpacity;\n/* Texels the field spans, from the atlas metrics. Scales the antialiasing band. */\nuniform float uDistanceRange;\nuniform vec2 uAtlasSize;\n\nout vec4 outColor;\n\n/** 0 none, 1 sRGB, 2 ACES 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 * 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\nvec3 applyOutputTransform(vec3 c) {\n if (uOutputTransform == 0) return c;\n if (uOutputTransform == 2) c = acesFilmic(c);\n return linearToSrgb(c);\n}\n\n\n/*\n * The middle of the three channels.\n *\n * A single-channel field rounds every corner off at small sizes, because one distance\n * cannot describe two edges meeting. Three channels carry the corner and the median\n * recovers it.\n */\nfloat median(vec3 rgb) {\n return max(min(rgb.r, rgb.g), min(max(rgb.r, rgb.g), rgb.b));\n}\n\nvoid main() {\n vec3 sampled = textureLod(uAtlas, vec3(vUv, 0.0), 0.0).rgb;\n float distance = median(sampled) - 0.5;\n\n /*\n * The band is one pixel wide *in screen space*, which is what lets one atlas serve every\n * size. A constant band is tuned for one size and is wrong at all the others.\n */\n vec2 texelsPerPixel = fwidth(vUv) * uAtlasSize;\n float pixelRange = max(0.5 * (texelsPerPixel.x + texelsPerPixel.y), 0.0001);\n float coverage = clamp(distance * uDistanceRange / pixelRange + 0.5, 0.0, 1.0);\n\n outColor = vec4(applyOutputTransform(uColor), coverage * uOpacity);\n}\n";
|
|
9
|
+
export declare const SDF_TEXT_FRAG = "#version 300 es\nprecision highp float;\n\nin vec2 vUv;\n\n/* An array of one: every surface texture is an array, and an atlas is read at layer 0. */\nuniform mediump sampler2DArray uAtlas;\nuniform vec3 uColor;\nuniform float uOpacity;\n/* Texels the field spans, from the atlas metrics. Scales the antialiasing band. */\nuniform float uDistanceRange;\nuniform vec2 uAtlasSize;\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 * 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\n/*\n * The middle of the three channels.\n *\n * A single-channel field rounds every corner off at small sizes, because one distance\n * cannot describe two edges meeting. Three channels carry the corner and the median\n * recovers it.\n */\nfloat median(vec3 rgb) {\n return max(min(rgb.r, rgb.g), min(max(rgb.r, rgb.g), rgb.b));\n}\n\nvoid main() {\n vec3 sampled = textureLod(uAtlas, vec3(vUv, 0.0), 0.0).rgb;\n float distance = median(sampled) - 0.5;\n\n /*\n * The band is one pixel wide *in screen space*, which is what lets one atlas serve every\n * size. A constant band is tuned for one size and is wrong at all the others.\n */\n vec2 texelsPerPixel = fwidth(vUv) * uAtlasSize;\n float pixelRange = max(0.5 * (texelsPerPixel.x + texelsPerPixel.y), 0.0001);\n float coverage = clamp(distance * uDistanceRange / pixelRange + 0.5, 0.0, 1.0);\n\n outColor = vec4(applyOutputTransform(uColor), coverage * uOpacity);\n}\n";
|
|
@@ -202,11 +202,18 @@ vec3 historyCatmullRom(vec2 uv) {
|
|
|
202
202
|
}
|
|
203
203
|
|
|
204
204
|
void main() {
|
|
205
|
-
|
|
205
|
+
/*
|
|
206
|
+
* **The alpha is this frame's and takes no history.** It is how much of the pixel is still the
|
|
207
|
+
* opaque surface (sceneCoverage.ts), which the composite's occlusion reads after this has
|
|
208
|
+
* replaced the scene, so it passes through untouched. The history is read for its colour alone.
|
|
209
|
+
*/
|
|
210
|
+
vec4 sampled = textureLod(uScene, vUv, 0.0);
|
|
211
|
+
vec3 current = sampled.rgb;
|
|
212
|
+
float share = sampled.a;
|
|
206
213
|
|
|
207
214
|
/* Nothing to blend towards: the first frame, a resize, a cut, or the effect switched off. */
|
|
208
215
|
if (uHistoryBlend <= 0.0) {
|
|
209
|
-
fragColor = vec4(current,
|
|
216
|
+
fragColor = vec4(current, share);
|
|
210
217
|
flickerOut = freshRecord(current);
|
|
211
218
|
flickerMotionOut = vec4(0.0);
|
|
212
219
|
return;
|
|
@@ -239,7 +246,7 @@ void main() {
|
|
|
239
246
|
vec4 previous = uReprojection * clip;
|
|
240
247
|
/* Behind the previous eye: there is no last-frame position to sample. */
|
|
241
248
|
if (previous.w <= 0.0) {
|
|
242
|
-
fragColor = vec4(current,
|
|
249
|
+
fragColor = vec4(current, share);
|
|
243
250
|
flickerOut = freshRecord(current);
|
|
244
251
|
flickerMotionOut = vec4(metres, 0.0, 0.0, 0.0);
|
|
245
252
|
return;
|
|
@@ -252,7 +259,7 @@ void main() {
|
|
|
252
259
|
* as a smear for as long as the camera kept turning.
|
|
253
260
|
*/
|
|
254
261
|
if (wasUv.x < 0.0 || wasUv.x > 1.0 || wasUv.y < 0.0 || wasUv.y > 1.0) {
|
|
255
|
-
fragColor = vec4(current,
|
|
262
|
+
fragColor = vec4(current, share);
|
|
256
263
|
flickerOut = freshRecord(current);
|
|
257
264
|
flickerMotionOut = vec4(metres, 0.0, 0.0, 0.0);
|
|
258
265
|
return;
|
|
@@ -311,6 +318,6 @@ void main() {
|
|
|
311
318
|
repeated * still
|
|
312
319
|
);
|
|
313
320
|
flickerMotionOut = vec4(metres, swingOut, 0.0, 0.0);
|
|
314
|
-
fragColor = vec4(mix(current, bounded, blend),
|
|
321
|
+
fragColor = vec4(mix(current, bounded, blend), share);
|
|
315
322
|
}
|
|
316
323
|
`;
|
package/dist/render/ssrPass.js
CHANGED
|
@@ -5,6 +5,7 @@ import { compileProgram, uniformLocations } from './shader.js';
|
|
|
5
5
|
import { FULLSCREEN_VERT } from './shaders/fullscreen.js';
|
|
6
6
|
import { SSR_RESOLVE_FRAG } from './shaders/ssrResolve.js';
|
|
7
7
|
import { SSR_TRACE_FRAG } from './shaders/ssrTrace.js';
|
|
8
|
+
import { blendKeeping } from './sceneCoverage.js';
|
|
8
9
|
/**
|
|
9
10
|
* Screen-space reflection on WebGL2: a scissored trace per surface, then one composite.
|
|
10
11
|
*
|
|
@@ -160,7 +161,7 @@ export class SsrPass {
|
|
|
160
161
|
gl.disable(gl.CULL_FACE);
|
|
161
162
|
gl.enable(gl.BLEND);
|
|
162
163
|
/* Premultiplied `over`: the trace already multiplied its colour by the coverage it found. */
|
|
163
|
-
|
|
164
|
+
blendKeeping(gl, gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
|
|
164
165
|
gl.useProgram(this.resolve);
|
|
165
166
|
gl.activeTexture(gl.TEXTURE0);
|
|
166
167
|
gl.bindTexture(gl.TEXTURE_2D, this.reflection);
|
|
@@ -57,6 +57,17 @@ export interface SurfaceMaterial<Texture = SurfaceTexture> {
|
|
|
57
57
|
/** Repeats across the mesh's own UV range, per axis. Applies to every map the material holds. */
|
|
58
58
|
uScale?: number;
|
|
59
59
|
vScale?: number;
|
|
60
|
+
/**
|
|
61
|
+
* Where the texture starts, per axis, added after the scale: a surface samples
|
|
62
|
+
* `uv · scale + offset`. Applies to every map, and to a cutout's shadow and depth as well.
|
|
63
|
+
*
|
|
64
|
+
* **What picks a cell of a flipbook or an atlas through the material**, so one quad draws any
|
|
65
|
+
* cell: a strip of four frames is `uScale: 0.25` and `uOffset: frame * 0.25`. Without it a
|
|
66
|
+
* consumer built one quad mesh per cell — 106 meshes for four strips — because the only way to
|
|
67
|
+
* move a texture was to move the geometry's own coordinates.
|
|
68
|
+
*/
|
|
69
|
+
uOffset?: number;
|
|
70
|
+
vOffset?: number;
|
|
60
71
|
/** Alpha below which a fragment is discarded. See `uAlbedoCutout`. */
|
|
61
72
|
cutout?: number;
|
|
62
73
|
/**
|
|
@@ -199,6 +210,8 @@ export declare class SurfaceTexture {
|
|
|
199
210
|
*/
|
|
200
211
|
constructor(gl: WebGL2RenderingContext, source: TexImageSource | readonly TexImageSource[], options?: SurfaceTextureOptions);
|
|
201
212
|
/** Bind the effects table to `unit`, or `fallback` for a texture given none. */
|
|
213
|
+
/** Whether this texture carries an effects table, which the lit program reads only once one exists. */
|
|
214
|
+
get hasEffects(): boolean;
|
|
202
215
|
bindEffects(gl: WebGL2RenderingContext, unit: number, fallback: WebGLTexture): void;
|
|
203
216
|
/**
|
|
204
217
|
* Replace the pixels, keeping the GPU object and its sampler state.
|
|
@@ -108,6 +108,10 @@ export class SurfaceTexture {
|
|
|
108
108
|
}
|
|
109
109
|
}
|
|
110
110
|
/** Bind the effects table to `unit`, or `fallback` for a texture given none. */
|
|
111
|
+
/** Whether this texture carries an effects table, which the lit program reads only once one exists. */
|
|
112
|
+
get hasEffects() {
|
|
113
|
+
return this.effectsTable !== null;
|
|
114
|
+
}
|
|
111
115
|
bindEffects(gl, unit, fallback) {
|
|
112
116
|
gl.activeTexture(gl.TEXTURE0 + unit);
|
|
113
117
|
gl.bindTexture(gl.TEXTURE_2D, this.effectsTable ?? fallback);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { MAX_CELLS, TEXT_CUBE, TextLayout, deviceSnappedOrigin } from './textLayout.js';
|
|
2
2
|
import { compileProgram, uniformLocations } from './shader.js';
|
|
3
3
|
import { TEXT_FRAG, TEXT_VERT } from './shaders/text.js';
|
|
4
|
+
import { blendCovering } from './sceneCoverage.js';
|
|
4
5
|
/**
|
|
5
6
|
* Text as geometry, on WebGL2.
|
|
6
7
|
*
|
|
@@ -98,7 +99,7 @@ export class TextRenderer {
|
|
|
98
99
|
const reversed = current === gl.GREATER || current === gl.GEQUAL;
|
|
99
100
|
gl.depthFunc(reversed ? gl.GEQUAL : gl.LEQUAL);
|
|
100
101
|
gl.enable(gl.BLEND);
|
|
101
|
-
|
|
102
|
+
blendCovering(gl, gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
|
|
102
103
|
setVec2(gl, this.uniforms['uViewport'], viewportWidth, viewportHeight);
|
|
103
104
|
/* Snapped with the cell below: a whole pitch on a fractional origin still splits a stroke.
|
|
104
105
|
See `deviceSnappedOrigin`. */
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The slice of depth a view model is drawn into: the nearest share of the range.
|
|
3
|
+
*
|
|
4
|
+
* **A first-person view model — arms and a held weapon — must never go into the wall it is pushed
|
|
5
|
+
* against**, and clearing depth to draw it on top costs every pass that reads depth afterwards:
|
|
6
|
+
* ambient occlusion, depth of field and the fog all see a hole where the world was. Squeezing the
|
|
7
|
+
* view model's depth into the nearest sliver of the range does neither. Anything past the near
|
|
8
|
+
* plane in the world lands behind it, the world's depth is kept, and the view model still sorts
|
|
9
|
+
* against itself, at a fraction of the precision.
|
|
10
|
+
*
|
|
11
|
+
* **The near end is where the convention says it is**: 1 under reversed depth and 0 under the
|
|
12
|
+
* conventional sense, which a WebGL2 context without `EXT_clip_control` runs. Taken from the
|
|
13
|
+
* renderer's own `reversedDepth`, never from the constant, for the reason `depthConvention.ts`
|
|
14
|
+
* gives at length.
|
|
15
|
+
*
|
|
16
|
+
* **What it gives up**: a view model is in front of everything, so it cannot be drawn reaching
|
|
17
|
+
* behind a world object in its own frame — which is what a view model is for. And under temporal
|
|
18
|
+
* reconstruction its pixels take no motion of their own, because the motion pass matches the frame's
|
|
19
|
+
* depth exactly and a squeezed draw matches nothing; draw a view model with multisampling.
|
|
20
|
+
*/
|
|
21
|
+
/** The share of depth a view model is drawn into when the caller names none: the nearest 1%. */
|
|
22
|
+
export declare const VIEW_MODEL_DEPTH_SHARE = 0.01;
|
|
23
|
+
/**
|
|
24
|
+
* Fill `out` with the `[min, max]` window depth a view model's draws are squeezed into.
|
|
25
|
+
*
|
|
26
|
+
* `share` is clamped to `[1e-4, 1]`: zero would collapse every view-model fragment onto one depth
|
|
27
|
+
* and lose its own sorting, and more than the whole range is no squeeze.
|
|
28
|
+
*/
|
|
29
|
+
export declare function viewModelDepthRange(reversed: boolean, share: number, out: [number, number] | Float32Array): void;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The slice of depth a view model is drawn into: the nearest share of the range.
|
|
3
|
+
*
|
|
4
|
+
* **A first-person view model — arms and a held weapon — must never go into the wall it is pushed
|
|
5
|
+
* against**, and clearing depth to draw it on top costs every pass that reads depth afterwards:
|
|
6
|
+
* ambient occlusion, depth of field and the fog all see a hole where the world was. Squeezing the
|
|
7
|
+
* view model's depth into the nearest sliver of the range does neither. Anything past the near
|
|
8
|
+
* plane in the world lands behind it, the world's depth is kept, and the view model still sorts
|
|
9
|
+
* against itself, at a fraction of the precision.
|
|
10
|
+
*
|
|
11
|
+
* **The near end is where the convention says it is**: 1 under reversed depth and 0 under the
|
|
12
|
+
* conventional sense, which a WebGL2 context without `EXT_clip_control` runs. Taken from the
|
|
13
|
+
* renderer's own `reversedDepth`, never from the constant, for the reason `depthConvention.ts`
|
|
14
|
+
* gives at length.
|
|
15
|
+
*
|
|
16
|
+
* **What it gives up**: a view model is in front of everything, so it cannot be drawn reaching
|
|
17
|
+
* behind a world object in its own frame — which is what a view model is for. And under temporal
|
|
18
|
+
* reconstruction its pixels take no motion of their own, because the motion pass matches the frame's
|
|
19
|
+
* depth exactly and a squeezed draw matches nothing; draw a view model with multisampling.
|
|
20
|
+
*/
|
|
21
|
+
/** The share of depth a view model is drawn into when the caller names none: the nearest 1%. */
|
|
22
|
+
export const VIEW_MODEL_DEPTH_SHARE = 0.01;
|
|
23
|
+
/**
|
|
24
|
+
* Fill `out` with the `[min, max]` window depth a view model's draws are squeezed into.
|
|
25
|
+
*
|
|
26
|
+
* `share` is clamped to `[1e-4, 1]`: zero would collapse every view-model fragment onto one depth
|
|
27
|
+
* and lose its own sorting, and more than the whole range is no squeeze.
|
|
28
|
+
*/
|
|
29
|
+
export function viewModelDepthRange(reversed, share, out) {
|
|
30
|
+
const width = Number.isFinite(share)
|
|
31
|
+
? Math.min(1, Math.max(1e-4, share))
|
|
32
|
+
: VIEW_MODEL_DEPTH_SHARE;
|
|
33
|
+
out[0] = reversed ? 1 - width : 0;
|
|
34
|
+
out[1] = reversed ? 1 : width;
|
|
35
|
+
}
|
|
@@ -5,6 +5,7 @@ import { bindAtmosphere } from './atmosphere.js';
|
|
|
5
5
|
import { bindPointLights } from './lightBudget.js';
|
|
6
6
|
import { DEFAULT_RENDER_QUALITY } from './renderQuality.js';
|
|
7
7
|
import { createResolvedWater, resolveWater, waterAppearance } from './waterDraw.js';
|
|
8
|
+
import { blendCovering } from './sceneCoverage.js';
|
|
8
9
|
/** Vertices per side of a bounded body's sheet. A pool is not an ocean. */
|
|
9
10
|
export class WaterRenderer {
|
|
10
11
|
program;
|
|
@@ -145,7 +146,7 @@ export class WaterRenderer {
|
|
|
145
146
|
gl.uniform2f(u['uReflectionTexelSize'] ?? null, reflectionReady && reflection !== null ? 1 / reflection.textureWidth : 1, reflectionReady && reflection !== null ? 1 / reflection.textureHeight : 1);
|
|
146
147
|
gl.uniform1i(u['uReflectionFilterTaps'] ?? null, this.reflectionFilterTaps);
|
|
147
148
|
gl.enable(gl.BLEND);
|
|
148
|
-
|
|
149
|
+
blendCovering(gl, gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
|
|
149
150
|
// Waves are displaced far enough that back faces show at grazing angles.
|
|
150
151
|
gl.disable(gl.CULL_FACE);
|
|
151
152
|
gl.bindVertexArray(w.bounded ? this.patchVao : this.vao);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { compileProgram, uniformLocations } from './shader.js';
|
|
2
2
|
import { WIND_STREAK_FRAG, WIND_STREAK_VERT } from './shaders/windStreaks.js';
|
|
3
3
|
import { WIND_STREAK_DEFAULTS, buildWindStreakLattice, createResolvedWindStreaks, resolveWindStreaks, windStreakStrength, } from './windStreakDraw.js';
|
|
4
|
+
import { blendCovering } from './sceneCoverage.js';
|
|
4
5
|
export class WindStreakRenderer {
|
|
5
6
|
program;
|
|
6
7
|
uniforms;
|
|
@@ -68,7 +69,7 @@ export class WindStreakRenderer {
|
|
|
68
69
|
gl.uniform1f(u['uTime'] ?? null, timeSeconds);
|
|
69
70
|
gl.uniform3fv(u['uTint'] ?? null, tint);
|
|
70
71
|
gl.enable(gl.BLEND);
|
|
71
|
-
|
|
72
|
+
blendCovering(gl, gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
|
|
72
73
|
gl.depthMask(false);
|
|
73
74
|
gl.disable(gl.CULL_FACE);
|
|
74
75
|
gl.bindVertexArray(this.vao);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@driftengine/core",
|
|
3
|
-
"version": "4.8.
|
|
3
|
+
"version": "4.8.3",
|
|
4
4
|
"description": "The runtime of DriftEngine, a WebGPU game engine with a WebGL2 fallback: renderer, loop, input, cameras, scene graph and physics",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -29,8 +29,8 @@
|
|
|
29
29
|
],
|
|
30
30
|
"sideEffects": false,
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@driftengine/drft": "4.8.
|
|
33
|
-
"@driftengine/physics": "4.8.
|
|
32
|
+
"@driftengine/drft": "4.8.3",
|
|
33
|
+
"@driftengine/physics": "4.8.3",
|
|
34
34
|
"gl-matrix": "^3.4.3"
|
|
35
35
|
},
|
|
36
36
|
"author": "Drift Technologies",
|
|
@@ -67,8 +67,8 @@
|
|
|
67
67
|
"prepack": "node ../../scripts/build.mjs @driftengine/core"
|
|
68
68
|
},
|
|
69
69
|
"devDependencies": {
|
|
70
|
-
"@driftengine/nav": "4.8.
|
|
71
|
-
"@driftengine/network": "4.8.
|
|
72
|
-
"@driftengine/texture": "4.8.
|
|
70
|
+
"@driftengine/nav": "4.8.3",
|
|
71
|
+
"@driftengine/network": "4.8.3",
|
|
72
|
+
"@driftengine/texture": "4.8.3"
|
|
73
73
|
}
|
|
74
74
|
}
|