@driftengine/core 4.13.4 → 4.13.5
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 +36 -0
- package/README.md +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/render/anisotropicLobe.d.ts +5 -0
- package/dist/render/anisotropicLobe.js +13 -0
- package/dist/render/atmosphere.d.ts +79 -1
- package/dist/render/atmosphere.js +66 -19
- package/dist/render/backend/webgl2/renderer.d.ts +34 -2
- package/dist/render/backend/webgl2/renderer.js +44 -12
- package/dist/render/backend/webgpu/boltPass.d.ts +1 -1
- package/dist/render/backend/webgpu/causticsPass.d.ts +1 -1
- package/dist/render/backend/webgpu/filmPass.d.ts +1 -1
- package/dist/render/backend/webgpu/gpuDrivenPass.js +20 -3
- package/dist/render/backend/webgpu/linePass.d.ts +1 -1
- package/dist/render/backend/webgpu/mediumFields.d.ts +17 -0
- package/dist/render/backend/webgpu/mediumFields.js +74 -0
- package/dist/render/backend/webgpu/plumePass.d.ts +3 -3
- package/dist/render/backend/webgpu/postPass.d.ts +7 -2
- package/dist/render/backend/webgpu/renderer.d.ts +13 -3
- package/dist/render/backend/webgpu/renderer.js +86 -150
- package/dist/render/backend/webgpu/scatterPass.d.ts +1 -1
- package/dist/render/backend/webgpu/waterPass.d.ts +1 -1
- package/dist/render/environmentMixing.d.ts +18 -0
- package/dist/render/environmentMixing.js +40 -0
- package/dist/render/fog.d.ts +43 -1
- package/dist/render/fog.js +87 -22
- package/dist/render/globalMedium.d.ts +112 -10
- package/dist/render/globalMedium.js +96 -1
- package/dist/render/globalMediumPass.d.ts +7 -3
- package/dist/render/globalMediumPass.js +22 -1
- package/dist/render/hairLobes.d.ts +5 -3
- package/dist/render/hairLobes.js +11 -7
- package/dist/render/lightBudget.d.ts +12 -0
- package/dist/render/lightBudget.js +24 -0
- package/dist/render/sceneTarget.d.ts +5 -3
- package/dist/render/sceneTarget.js +14 -8
- package/dist/render/shaders/arcane.d.ts +1 -1
- package/dist/render/shaders/bolt.d.ts +1 -1
- package/dist/render/shaders/caustics.d.ts +1 -1
- package/dist/render/shaders/film.d.ts +1 -1
- package/dist/render/shaders/film.js +4 -1
- package/dist/render/shaders/fire.d.ts +1 -1
- package/dist/render/shaders/flat/anisotropic.d.ts +8 -1
- package/dist/render/shaders/flat/anisotropic.js +25 -3
- package/dist/render/shaders/flat/hair.js +31 -7
- package/dist/render/shaders/flat/main.d.ts +1 -1
- package/dist/render/shaders/flat/main.js +20 -1
- package/dist/render/shaders/flat/models.js +1 -1
- package/dist/render/shaders/flat/preamble.js +7 -0
- package/dist/render/shaders/fog.d.ts +4 -4
- package/dist/render/shaders/fog.js +81 -32
- package/dist/render/shaders/generated/arcane.wgsl.d.ts +29 -39
- package/dist/render/shaders/generated/arcane.wgsl.js +29 -39
- package/dist/render/shaders/generated/bolt.wgsl.d.ts +26 -36
- package/dist/render/shaders/generated/bolt.wgsl.js +26 -36
- package/dist/render/shaders/generated/caustics.wgsl.d.ts +25 -35
- package/dist/render/shaders/generated/caustics.wgsl.js +25 -35
- package/dist/render/shaders/generated/film.wgsl.d.ts +28 -38
- package/dist/render/shaders/generated/film.wgsl.js +28 -38
- package/dist/render/shaders/generated/fire.wgsl.d.ts +28 -38
- package/dist/render/shaders/generated/fire.wgsl.js +28 -38
- package/dist/render/shaders/generated/flat.wgsl.d.ts +1466 -1546
- package/dist/render/shaders/generated/flat.wgsl.js +1489 -1569
- package/dist/render/shaders/generated/globalMedium.wgsl.d.ts +98 -12
- package/dist/render/shaders/generated/globalMedium.wgsl.js +98 -12
- package/dist/render/shaders/generated/line.wgsl.d.ts +25 -35
- package/dist/render/shaders/generated/line.wgsl.js +25 -35
- package/dist/render/shaders/generated/particle.wgsl.d.ts +100 -140
- package/dist/render/shaders/generated/particle.wgsl.js +100 -140
- package/dist/render/shaders/generated/rush.wgsl.d.ts +20 -10
- package/dist/render/shaders/generated/rush.wgsl.js +20 -10
- package/dist/render/shaders/generated/scatter.wgsl.d.ts +25 -35
- package/dist/render/shaders/generated/scatter.wgsl.js +25 -35
- package/dist/render/shaders/generated/smoke.wgsl.d.ts +28 -38
- package/dist/render/shaders/generated/smoke.wgsl.js +28 -38
- package/dist/render/shaders/generated/water.wgsl.d.ts +29 -39
- package/dist/render/shaders/generated/water.wgsl.js +29 -39
- package/dist/render/shaders/globalMedium.js +114 -10
- package/dist/render/shaders/gpudriven/blendRaster.wgsl.js +2 -1
- package/dist/render/shaders/gpudriven/fog.wgsl.d.ts +10 -4
- package/dist/render/shaders/gpudriven/fog.wgsl.js +77 -19
- package/dist/render/shaders/gpudriven/frameBlock.wgsl.d.ts +1 -1
- package/dist/render/shaders/gpudriven/frameBlock.wgsl.js +12 -1
- package/dist/render/shaders/gpudriven/shade.wgsl.js +3 -1
- package/dist/render/shaders/line.d.ts +1 -1
- package/dist/render/shaders/line.js +4 -2
- package/dist/render/shaders/localExposure.d.ts +42 -1
- package/dist/render/shaders/localExposure.js +73 -6
- package/dist/render/shaders/particle.d.ts +3 -3
- package/dist/render/shaders/particle.js +10 -6
- package/dist/render/shaders/scatter.d.ts +1 -1
- package/dist/render/shaders/scatter.js +4 -2
- package/dist/render/shaders/smoke.d.ts +1 -1
- package/dist/render/shaders/water.d.ts +1 -1
- package/dist/render/shaders/water.js +2 -1
- package/dist/render/surfaceModel.d.ts +23 -4
- package/dist/render/surfaceModel.js +12 -2
- package/package.json +6 -6
- package/src/index.ts +12 -2
- package/src/render/anisotropicLobe.ts +20 -0
- package/src/render/atmosphere.ts +153 -20
- package/src/render/backend/webgl2/renderer.ts +63 -13
- package/src/render/backend/webgpu/gpuDrivenPass.ts +20 -3
- package/src/render/backend/webgpu/mediumFields.ts +87 -0
- package/src/render/backend/webgpu/renderer.ts +109 -157
- package/src/render/environmentMixing.ts +48 -0
- package/src/render/fog.ts +125 -25
- package/src/render/globalMedium.ts +214 -11
- package/src/render/globalMediumPass.ts +29 -4
- package/src/render/hairLobes.ts +35 -4
- package/src/render/lightBudget.ts +32 -0
- package/src/render/sceneTarget.ts +15 -11
- package/src/render/shaders/film.ts +4 -1
- package/src/render/shaders/flat/anisotropic.ts +25 -3
- package/src/render/shaders/flat/hair.ts +31 -7
- package/src/render/shaders/flat/main.ts +20 -1
- package/src/render/shaders/flat/models.ts +1 -1
- package/src/render/shaders/flat/preamble.ts +7 -0
- package/src/render/shaders/fog.ts +81 -32
- package/src/render/shaders/generated/arcane.wgsl.ts +29 -39
- package/src/render/shaders/generated/bolt.wgsl.ts +26 -36
- package/src/render/shaders/generated/caustics.wgsl.ts +25 -35
- package/src/render/shaders/generated/film.wgsl.ts +28 -38
- package/src/render/shaders/generated/fire.wgsl.ts +28 -38
- package/src/render/shaders/generated/flat.wgsl.ts +1489 -1569
- package/src/render/shaders/generated/globalMedium.wgsl.ts +98 -12
- package/src/render/shaders/generated/line.wgsl.ts +25 -35
- package/src/render/shaders/generated/particle.wgsl.ts +100 -140
- package/src/render/shaders/generated/rush.wgsl.ts +20 -10
- package/src/render/shaders/generated/scatter.wgsl.ts +25 -35
- package/src/render/shaders/generated/smoke.wgsl.ts +28 -38
- package/src/render/shaders/generated/water.wgsl.ts +29 -39
- package/src/render/shaders/globalMedium.ts +114 -10
- package/src/render/shaders/gpudriven/blendRaster.wgsl.ts +2 -1
- package/src/render/shaders/gpudriven/fog.wgsl.ts +78 -19
- package/src/render/shaders/gpudriven/frameBlock.wgsl.ts +12 -1
- package/src/render/shaders/gpudriven/shade.wgsl.ts +3 -1
- package/src/render/shaders/line.ts +4 -2
- package/src/render/shaders/localExposure.ts +115 -6
- package/src/render/shaders/particle.ts +10 -6
- package/src/render/shaders/scatter.ts +4 -2
- package/src/render/shaders/water.ts +2 -1
- package/src/render/surfaceModel.ts +41 -6
|
@@ -329,8 +329,10 @@ void main() {
|
|
|
329
329
|
}
|
|
330
330
|
|
|
331
331
|
vec3 shaded = vColor * lit;
|
|
332
|
-
float
|
|
333
|
-
|
|
332
|
+
float dist = distance(vWorldPos, uCameraPos);
|
|
333
|
+
float fog = mediumFog(dist, vWorldPos.y);
|
|
334
|
+
vec3 glow = mediumGlow(dist, vWorldPos.y, normalize(vWorldPos - uCameraPos));
|
|
335
|
+
outColor = vec4(applyOutputTransform(mix(shaded, mediumColor(), fog) + glow), mask * vAlpha);
|
|
334
336
|
}
|
|
335
337
|
`;
|
|
336
338
|
/**
|
|
@@ -456,8 +458,9 @@ void main() {
|
|
|
456
458
|
|
|
457
459
|
vec3 color = vColor;
|
|
458
460
|
if (uFogEnabled != 0) {
|
|
459
|
-
float
|
|
460
|
-
|
|
461
|
+
float dist = distance(vWorldPos, uCameraPos);
|
|
462
|
+
float fog = mediumFog(dist, vWorldPos.y);
|
|
463
|
+
color = mix(color, mediumColor(), fog) + mediumGlow(dist, vWorldPos.y, normalize(vWorldPos - uCameraPos));
|
|
461
464
|
}
|
|
462
465
|
/*
|
|
463
466
|
* A real alpha blend (SRC_ALPHA, ONE_MINUS_SRC_ALPHA) rather than spark's energy-in-colour
|
|
@@ -555,8 +558,9 @@ void main() {
|
|
|
555
558
|
if (alpha <= 0.001) discard;
|
|
556
559
|
vec3 color = vColor * image.rgb;
|
|
557
560
|
if (uFogEnabled != 0) {
|
|
558
|
-
float
|
|
559
|
-
|
|
561
|
+
float dist = distance(vWorldPos, uCameraPos);
|
|
562
|
+
float fog = mediumFog(dist, vWorldPos.y);
|
|
563
|
+
color = mix(color, mediumColor(), fog) + mediumGlow(dist, vWorldPos.y, normalize(vWorldPos - uCameraPos));
|
|
560
564
|
}
|
|
561
565
|
outColor = vec4(applyOutputTransform(color), alpha);
|
|
562
566
|
}
|
|
@@ -11,4 +11,4 @@ export declare const SCATTER_VERT = "#version 300 es\nlayout(location = 0) in ve
|
|
|
11
11
|
* program is not obliged to read every attribute a VAO offers.
|
|
12
12
|
*/
|
|
13
13
|
export declare const SCATTER_DEPTH_VERT = "#version 300 es\nlayout(location = 0) in vec3 aPosition;\nlayout(location = 3) in vec3 aInstancePos;\nlayout(location = 4) in vec2 aScaleYaw;\nlayout(location = 6) in vec3 aWindResponse;\n\nuniform mat4 uLightViewProj;\n\nuniform vec2 uWindDirection;\nuniform float uWindSpeed;\nuniform float uWindGust;\nuniform float uWindTime;\n/* Spatial frequency of the travelling gust, per metre. */\nuniform vec2 uWindSpatialPhase;\n/*\n * Where something heavy has just been: xyz world position, w how pressed, 0 unused.\n *\n * Uniforms rather than a texture or a re-upload, because this is the hottest shader\n * in the game and the effect is a metre wide. Eight of them is about two seconds of\n * trail at running pace. See TrampleField.\n */\nuniform vec4 uTrample[8];\n/* How far a press reaches, metres, and how much of a plant it takes away. */\nuniform float uTrampleRadius;\nuniform float uTrampleDepth;\n\n/* Yaw as (sin, cos). Computed once per vertex and shared by position and normal. */\nvec2 scatterYaw(float yaw) {\n return vec2(sin(yaw), cos(yaw));\n}\n\n/* The instance's local offset from its root, after scale, yaw, wind and trampling. */\nvec3 scatterOffset(\n vec3 position,\n vec3 instancePos,\n vec2 scaleYaw,\n vec3 windResponse,\n vec2 yawTrig\n) {\n float s = yawTrig.x;\n float c = yawTrig.y;\n\n vec3 local = position * scaleYaw.x;\n vec3 rotated = vec3(local.x * c + local.z * s, local.y, -local.x * s + local.z * c);\n\n /*\n * Wind phase comes from world position, so neighbouring plants are never in\n * lockstep and a gust visibly travels across a field instead of the whole\n * field pulsing at once. Direction and strength come from the shared wind \u2014\n * this adds spatial variation, never a second wind.\n *\n * Amplitude rises with the square of height, so the base stays planted and\n * only the tip travels. A linear falloff slides the whole plant sideways,\n * which reads as the ground moving.\n */\n float phase = dot(instancePos.xz, uWindSpatialPhase) + uWindTime + windResponse.z;\n float rooted = clamp(rotated.y * windResponse.x, 0.0, 1.0);\n float bend = (uWindSpeed + uWindGust * windResponse.y) * sin(phase) * rooted * rooted;\n rotated.xz += uWindDirection * bend;\n\n /*\n * Trampling: pressed toward the ground and pushed away from whatever pressed it.\n *\n * The strongest press wins rather than the sum. Two overlapping presses adding up\n * would drive a blade through the deck, and a character crossing their own trail is\n * exactly when that would happen.\n *\n * Direction comes from the *press*, so a plant leans away from where the foot was\n * instead of all of them lying the same way, which is what makes it read as\n * something having gone through rather than as a second wind.\n */\n float pressed = 0.0;\n vec2 away = vec2(0.0);\n for (int i = 0; i < 8; i++) {\n vec4 p = uTrample[i];\n if (p.w <= 0.0) continue;\n vec3 offset = instancePos - p.xyz;\n float dist = length(offset);\n if (dist >= uTrampleRadius) continue;\n float fade = 1.0 - dist / uTrampleRadius;\n float strength = p.w * fade * fade;\n if (strength <= pressed) continue;\n pressed = strength;\n // Degenerate at the exact centre, where any direction is as good as another.\n away = dist > 1e-4 ? offset.xz / dist : vec2(0.0);\n }\n if (pressed > 0.0) {\n float take = pressed * uTrampleDepth;\n // Height goes first \u2014 a flattened plant is shorter, and the fold outward is\n // what makes the loss of height read as bending rather than as sinking.\n rotated.y *= 1.0 - take;\n rotated.xz += away * take * rotated.y * 1.4;\n }\n\n return rotated;\n}\n\nout vec4 vLightPosition;\n\nvoid main() {\n vec3 rotated = scatterOffset(\n aPosition,\n aInstancePos,\n aScaleYaw,\n aWindResponse,\n scatterYaw(aScaleYaw.y)\n );\n vLightPosition = uLightViewProj * vec4(aInstancePos + rotated, 1.0);\n gl_Position = vLightPosition;\n}\n";
|
|
14
|
-
export declare const SCATTER_FRAG = "#version 300 es\nprecision highp float;\n\nin vec3 vColor;\nin vec3 vNormal;\nin vec3 vWorldPos;\n\nuniform vec3 uDirectionalDir;\nuniform vec3 uDirectionalColor;\nuniform vec3 uAmbient;\nuniform vec3 uCameraPos;\n\nuniform vec3 uFogColor;\n/** Extinction per metre **at the camera's own height**, not at the base. */\nuniform float uFogDensity;\n/** Reciprocal scale height of the haze, per metre. 0 is a uniform medium. */\nuniform float uFogHeightFalloff;\n/** The camera's height, so a fragment's own height is one subtraction away. */\nuniform float uFogEyeY;\nuniform vec3 uUnderwaterColor;\nuniform float uUnderwaterFogDensity;\nuniform float uUnderwaterFactor;\n/** 0 integrates a medium (Beer-Lambert); 1 ramps linearly between near and far. */\nuniform int uFogMode;\n/** Linear mode only: where haze begins, and where it is total. */\nuniform float uFogNear;\nuniform float uFogFar;\n\n/** What distance converges toward: the day's haze, or the water column. */\nvec3 mediumColor() {\n return mix(uFogColor, uUnderwaterColor, uUnderwaterFactor);\n}\n\n/**\n * How much of a surface standing at height pointY, dist metres away, is lost to\n * the medium in between. 0 is perfectly clear, 1 is the medium's own colour.\n */\nfloat mediumFog(float dist, float pointY) {\n /*\n * A depth ramp rather than a medium, for worlds authored against one.\n *\n * Beer-Lambert is the honest model and stays the default: it integrates something\n * physically there, so extinction begins at the eye and never stops. A linear near/far\n * fog is not that \u2014 it is a *look*, with a hard start distance before which the air is\n * perfectly clear, and it is what the fixed-function pipeline offered and what several\n * renderers still expose. Approximating one with the other is not a matter of tuning:\n * matched at half occlusion, the exponential curve still lays haze over everything\n * inside near, where the ramp lays none, and near-field colour desaturates toward the\n * fog colour with nothing to show for it.\n *\n * Height falloff has no meaning here and is ignored: a ramp is defined on distance\n * alone.\n */\n if (uFogMode == 1) {\n float span = max(uFogFar - uFogNear, 1e-4);\n float ramp = clamp((dist - uFogNear) / span, 0.0, 1.0);\n if (uUnderwaterFactor <= 0.0) return ramp;\n float wetLinear = uUnderwaterFogDensity * dist;\n return mix(ramp, 1.0 - exp2(-wetLinear * wetLinear * 1.442695), uUnderwaterFactor);\n }\n\n // Where the medium begins along the ray: uFogNear, the linear mode's slot, which this mode\n // never read before Atmosphere.fogStart. The ray is integrated from there on, at the density\n // of that point; at a start of 0 every term is the one it always was. See render/fog.ts.\n float start = min(uFogNear, dist);\n float before = dist > 0.0 ? start / dist : 0.0;\n float t = (pointY - uFogEyeY) * uFogHeightFalloff;\n float rest = t * (1.0 - before);\n // (1 - e^-t)/t, which tends to 1 as the ray levels out. Guarded rather than\n // branched: at t = 0 the quotient is 0/0, and 1e-4 of a scale height is 8 mm.\n float denom = abs(rest) < 1e-4 ? 1e-4 : rest;\n float air =\n 1.0 - exp(-uFogDensity * exp(-t * before) * (dist - start) * (1.0 - exp(-denom)) / denom);\n if (uUnderwaterFactor <= 0.0) return air;\n // Water is its own medium and keeps its own curve, so crossing the surface\n // changes nothing about how the water column reads.\n float wet = uUnderwaterFogDensity * dist;\n return mix(air, 1.0 - exp2(-wet * wet * 1.442695), uUnderwaterFactor);\n}\n\n\nout vec4 outColor;\n\n/** 0 none, 1 sRGB, 2 ACES then sRGB, 3 the highlight shoulder then sRGB. See renderQuality.ts. */\nuniform int uOutputTransform;\n/** Scales the scene into the tone curve. 1 is the reference grade. See renderQuality.ts. */\nuniform float uOutputExposure;\n\n/*\n * ACES: the reference fit, not the cheap one.\n *\n * This was Narkowicz's single rational function, on the argument that the difference from\n * the reference curve is invisible next to the gain. That argument holds for a look and\n * fails for a *match*, which is what this option is for \u2014 aces exists so a world ported\n * from three.js reads the same, and three.js applies the full fit: into AP1 through\n * ACESInputMat, the RRT and ODT together, then back out through ACESOutputMat.\n *\n * The two curves are not a shade apart. Narkowicz is markedly brighter through the\n * midtones, which is exactly where an interior scene lives, and it is a large part of a\n * measured ~1.7x that survived correcting the colour space and the missing Lambert pi.\n * An approximation of a standard is the wrong thing to hold when the standard is the\n * specification.\n */\nvec3 rrtAndOdtFit(vec3 v) {\n vec3 a = v * (v + 0.0245786) - 0.000090537;\n vec3 b = v * (0.983729 * v + 0.4329510) + 0.238081;\n return a / b;\n}\n\nvec3 acesFilmic(vec3 x) {\n /*\n * Exposure first, because a tone curve is only defined relative to one.\n *\n * The reference RRT and ODT are graded for a dark cinema. Every renderer that ships ACES\n * scales its input to suit whatever it is actually shown on, and the factor each one\n * picks is a *look*, not a standard - so this engine takes no position and reads it from\n * the caller. Neutral by default, which is the reference grade unmodified. three.js's own\n * pick is 1 / 0.6, for a consumer matching one.\n */\n x *= uOutputExposure;\n // Columns, as GLSL reads them: sRGB -> XYZ -> D65_2_D60 -> AP1 -> RRT_SAT.\n const mat3 ACES_INPUT = mat3(\n 0.59719, 0.07600, 0.02840,\n 0.35458, 0.90834, 0.13383,\n 0.04823, 0.01566, 0.83777\n );\n // ODT_SAT -> XYZ -> D60_2_D65 -> sRGB.\n const mat3 ACES_OUTPUT = mat3(\n 1.60475, -0.10208, -0.00327,\n -0.53108, 1.10813, -0.07276,\n -0.07367, -0.00605, 1.07602\n );\n return clamp(ACES_OUTPUT * rrtAndOdtFit(ACES_INPUT * x), 0.0, 1.0);\n}\n\n\n/*\n * Linear to sRGB. The piecewise form rather than a plain pow: the linear toe near black\n * is where banding lives in an 8-bit buffer, and the cheap approximation puts it in the\n * wrong place.\n */\nvec3 linearToSrgb(vec3 c) {\n vec3 low = c * 12.92;\n vec3 high = 1.055 * pow(max(c, vec3(0.0)), vec3(1.0 / 2.4)) - 0.055;\n return mix(high, low, step(c, vec3(0.0031308)));\n}\n\n\nvec3 highlightShoulder(vec3 c) {\n float m = max(c.r, max(c.g, c.b));\n if (m <= 0.8) return c;\n float e = m - 0.8;\n return c * ((0.8 + 0.2 * e / (e + 0.2)) / m);\n}\n\nvec3 applyOutputTransform(vec3 c) {\n if (uOutputTransform == 0) return c;\n if (uOutputTransform == 2) c = acesFilmic(c);\n if (uOutputTransform == 3) c = highlightShoulder(c * uOutputExposure);\n return linearToSrgb(c);\n}\n\n\nvoid main() {\n vec3 n = normalize(vNormal);\n /*\n * Two-sided. A blade of grass is a flat sliver seen from both faces, and\n * lighting its back face as though it faced away leaves half of every field\n * black \u2014 the cheapest and most obvious foliage tell there is.\n */\n float ndl = abs(dot(n, uDirectionalDir));\n vec3 lit = vColor * (uAmbient + uDirectionalColor * ndl);\n\n float fog = mediumFog(distance(vWorldPos, uCameraPos), vWorldPos.y);\n\n /* Graded where nothing after this will grade it, as every pass is: the same light as the ground. */\n outColor = vec4(applyOutputTransform(mix(lit, mediumColor(), fog)), 1.0);\n}\n";
|
|
14
|
+
export declare const SCATTER_FRAG = "#version 300 es\nprecision highp float;\n\nin vec3 vColor;\nin vec3 vNormal;\nin vec3 vWorldPos;\n\nuniform vec3 uDirectionalDir;\nuniform vec3 uDirectionalColor;\nuniform vec3 uAmbient;\nuniform vec3 uCameraPos;\n\nuniform vec3 uFogColor;\n/**\n * The haze: its extinction per metre **at the camera's own height**, not at the base; its\n * reciprocal scale height per metre, 0 for a uniform medium; the camera's height, so a fragment's\n * own height is one subtraction away; and the water column's own density.\n *\n * **Four numbers in one vector rather than four uniforms**, and the reason is a phone: a scalar\n * costs a whole row of the uniform grid, and the lit stage at its eight-light rung must fit the 256\n * rows an Adreno 740 offers (uniformVectorBudget.ts). Packed, the haze's eight scalars cost two\n * rows where they cost eight, which is what paid for the second layer and the sun's glow below.\n */\nuniform vec4 uFogMedium;\n/** Where haze begins, where a linear ramp is total, and the mode: 0 a medium, 1 a ramp. */\nuniform vec4 uFogRange;\nuniform vec3 uUnderwaterColor;\nuniform float uUnderwaterFactor;\n/**\n * The second layer's extinction per metre at the camera's height and its falloff; then the least of\n * a surface the haze must leave, Atmosphere.fogMaxOpacity's complement, and the distance past which\n * it hides nothing (0 is none). **Every lane is none at zero**, and so are uFogGlow's: a program a\n * binder never wrote these into draws the haze it drew before they existed, rather than none.\n */\nuniform vec4 uFogLayer2;\n/** The sun's glow in the haze: its colour, black for none, and the exponent of its lobe. */\nuniform vec4 uFogGlow;\n/** Surface \u2192 the glow's light, and how far past the haze's start the glow begins. */\nuniform vec4 uFogGlowDir;\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 * The haze's optical depth from where it begins along the ray to a point at height pointY, dist\n * metres away: each layer's density at the eye carried over what is left of the ray, with the\n * second layer only where there is one. The fog and the sun's glow both integrate it.\n */\nfloat hazeDepth(float dist, float pointY) {\n // Where the medium begins along the ray: uFogRange.x, the linear mode's slot, which this mode\n // never read before Atmosphere.fogStart. The ray is integrated from there on, at the density\n // of that point; at a start of 0 every term is the one it always was. See render/fog.ts.\n float start = min(uFogRange.x, dist);\n float before = dist > 0.0 ? start / dist : 0.0;\n float t = (pointY - uFogMedium.z) * uFogMedium.y;\n float rest = t * (1.0 - before);\n // (1 - e^-t)/t, which tends to 1 as the ray levels out. Guarded rather than\n // branched: at t = 0 the quotient is 0/0, and 1e-4 of a scale height is 8 mm.\n float denom = abs(rest) < 1e-4 ? 1e-4 : rest;\n float depth = uFogMedium.x * exp(-t * before) * (dist - start) * (1.0 - exp(-denom)) / denom;\n if (uFogLayer2.x > 0.0) {\n float t2 = (pointY - uFogMedium.z) * uFogLayer2.y;\n float rest2 = t2 * (1.0 - before);\n float denom2 = abs(rest2) < 1e-4 ? 1e-4 : rest2;\n depth += uFogLayer2.x * exp(-t2 * before) * (dist - start) * (1.0 - exp(-denom2)) / denom2;\n }\n return depth;\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 /* Past the cutoff the air hides nothing, in either mode; the water column keeps its own law. */\n bool cut = uFogLayer2.w > 0.0 && dist > uFogLayer2.w;\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 (uFogRange.z == 1.0) {\n float span = max(uFogRange.y - uFogRange.x, 1e-4);\n float ramp = cut ? 0.0 : min(clamp((dist - uFogRange.x) / span, 0.0, 1.0), 1.0 - uFogLayer2.z);\n if (uUnderwaterFactor <= 0.0) return ramp;\n float wetLinear = uFogMedium.w * dist;\n return mix(ramp, 1.0 - exp2(-wetLinear * wetLinear * 1.442695), uUnderwaterFactor);\n }\n\n // Beer-Lambert through both layers, leaving at least uFogLayer2.z of the surface: a floor on\n // what survives rather than a ceiling on the depth, so the curve keeps its shape above it.\n float air = cut ? 0.0 : 1.0 - max(exp(-hazeDepth(dist, pointY)), uFogLayer2.z);\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 = uFogMedium.w * dist;\n return mix(air, 1.0 - exp2(-wet * wet * 1.442695), uUnderwaterFactor);\n}\n\n/**\n * The sun's light the haze scatters toward the eye along a ray to a point at height pointY, dist\n * metres away in the direction toPoint: added to a surface after its fog, never mixed. A lobe of\n * cos^exponent about the light, by as much of the ray past the glow's own start as the haze hides \u2014\n * the haze's depth per metre carried over that stretch. None in a linear ramp, past the cutoff, or\n * under water, which keeps its own medium.\n */\nvec3 mediumGlow(float dist, float pointY, vec3 toPoint) {\n if (uFogRange.z == 1.0 || max(max(uFogGlow.r, uFogGlow.g), uFogGlow.b) <= 0.0) return vec3(0.0);\n if (uFogLayer2.w > 0.0 && dist > uFogLayer2.w) return vec3(0.0);\n float span = dist - min(uFogRange.x, dist);\n if (span <= 0.0) return vec3(0.0);\n float perMetre = hazeDepth(dist, pointY) / span;\n float hidden = 1.0 - exp(-perMetre * max(span - uFogGlowDir.w, 0.0));\n float lobe = pow(clamp(dot(toPoint, uFogGlowDir.xyz), 0.0, 1.0), uFogGlow.a);\n return uFogGlow.rgb * (lobe * hidden * (1.0 - 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/*\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 vec3 n = normalize(vNormal);\n /*\n * Two-sided. A blade of grass is a flat sliver seen from both faces, and\n * lighting its back face as though it faced away leaves half of every field\n * black \u2014 the cheapest and most obvious foliage tell there is.\n */\n float ndl = abs(dot(n, uDirectionalDir));\n vec3 lit = vColor * (uAmbient + uDirectionalColor * ndl);\n\n float dist = distance(vWorldPos, uCameraPos);\n float fog = mediumFog(dist, vWorldPos.y);\n vec3 glow = mediumGlow(dist, vWorldPos.y, normalize(vWorldPos - uCameraPos));\n\n /* Graded where nothing after this will grade it, as every pass is: the same light as the ground. */\n outColor = vec4(applyOutputTransform(mix(lit, mediumColor(), fog) + glow), 1.0);\n}\n";
|
|
@@ -201,9 +201,11 @@ void main() {
|
|
|
201
201
|
float ndl = abs(dot(n, uDirectionalDir));
|
|
202
202
|
vec3 lit = vColor * (uAmbient + uDirectionalColor * ndl);
|
|
203
203
|
|
|
204
|
-
float
|
|
204
|
+
float dist = distance(vWorldPos, uCameraPos);
|
|
205
|
+
float fog = mediumFog(dist, vWorldPos.y);
|
|
206
|
+
vec3 glow = mediumGlow(dist, vWorldPos.y, normalize(vWorldPos - uCameraPos));
|
|
205
207
|
|
|
206
208
|
/* Graded where nothing after this will grade it, as every pass is: the same light as the ground. */
|
|
207
|
-
outColor = vec4(applyOutputTransform(mix(lit, mediumColor(), fog)), 1.0);
|
|
209
|
+
outColor = vec4(applyOutputTransform(mix(lit, mediumColor(), fog) + glow), 1.0);
|
|
208
210
|
}
|
|
209
211
|
`;
|
|
@@ -5,4 +5,4 @@
|
|
|
5
5
|
* Alpha-blended rather than additive: smoke occludes what is behind it, and
|
|
6
6
|
* adding it would make it glow instead of darken.
|
|
7
7
|
*/
|
|
8
|
-
export declare const SMOKE_FRAG = "#version 300 es\nprecision highp float;\n\nin vec2 vUv;\nin float vSeed;\nin float vDistance;\nin vec3 vWorldPos;\n\nuniform float uTime;\n\nuniform vec3 uFogColor;\n/** Extinction per metre **at the camera's own height**, not at the base. */\nuniform float uFogDensity;\n/** Reciprocal scale height of the haze, per metre. 0 is a uniform medium. */\nuniform float uFogHeightFalloff;\n/** The camera's height, so a fragment's own height is one subtraction away. */\nuniform float uFogEyeY;\nuniform vec3 uUnderwaterColor;\nuniform float uUnderwaterFogDensity;\nuniform float uUnderwaterFactor;\n/** 0 integrates a medium (Beer-Lambert); 1 ramps linearly between near and far. */\nuniform int uFogMode;\n/** Linear mode only: where haze begins, and where it is total. */\nuniform float uFogNear;\nuniform float uFogFar;\n\n/** What distance converges toward: the day's haze, or the water column. */\nvec3 mediumColor() {\n return mix(uFogColor, uUnderwaterColor, uUnderwaterFactor);\n}\n\n/**\n * How much of a surface standing at height pointY, dist metres away, is lost to\n * the medium in between. 0 is perfectly clear, 1 is the medium's own colour.\n */\nfloat mediumFog(float dist, float pointY) {\n /*\n * A depth ramp rather than a medium, for worlds authored against one.\n *\n * Beer-Lambert is the honest model and stays the default: it integrates something\n * physically there, so extinction begins at the eye and never stops. A linear near/far\n * fog is not that \u2014 it is a *look*, with a hard start distance before which the air is\n * perfectly clear, and it is what the fixed-function pipeline offered and what several\n * renderers still expose. Approximating one with the other is not a matter of tuning:\n * matched at half occlusion, the exponential curve still lays haze over everything\n * inside near, where the ramp lays none, and near-field colour desaturates toward the\n * fog colour with nothing to show for it.\n *\n * Height falloff has no meaning here and is ignored: a ramp is defined on distance\n * alone.\n */\n if (uFogMode == 1) {\n float span = max(uFogFar - uFogNear, 1e-4);\n float ramp = clamp((dist - uFogNear) / span, 0.0, 1.0);\n if (uUnderwaterFactor <= 0.0) return ramp;\n float wetLinear = uUnderwaterFogDensity * dist;\n return mix(ramp, 1.0 - exp2(-wetLinear * wetLinear * 1.442695), uUnderwaterFactor);\n }\n\n // Where the medium begins along the ray: uFogNear, the linear mode's slot, which this mode\n // never read before Atmosphere.fogStart. The ray is integrated from there on, at the density\n // of that point; at a start of 0 every term is the one it always was. See render/fog.ts.\n float start = min(uFogNear, dist);\n float before = dist > 0.0 ? start / dist : 0.0;\n float t = (pointY - uFogEyeY) * uFogHeightFalloff;\n float rest = t * (1.0 - before);\n // (1 - e^-t)/t, which tends to 1 as the ray levels out. Guarded rather than\n // branched: at t = 0 the quotient is 0/0, and 1e-4 of a scale height is 8 mm.\n float denom = abs(rest) < 1e-4 ? 1e-4 : rest;\n float air =\n 1.0 - exp(-uFogDensity * exp(-t * before) * (dist - start) * (1.0 - exp(-denom)) / denom);\n if (uUnderwaterFactor <= 0.0) return air;\n // Water is its own medium and keeps its own curve, so crossing the surface\n // changes nothing about how the water column reads.\n float wet = uUnderwaterFogDensity * dist;\n return mix(air, 1.0 - exp2(-wet * wet * 1.442695), uUnderwaterFactor);\n}\n\nuniform int uNoiseOctaves;\nuniform vec4 uClipPlane;\nuniform int uClipEnabled;\n\nout vec4 outColor;\n\n/** 0 none, 1 sRGB, 2 ACES then sRGB, 3 the highlight shoulder then sRGB. See renderQuality.ts. */\nuniform int uOutputTransform;\n/** Scales the scene into the tone curve. 1 is the reference grade. See renderQuality.ts. */\nuniform float uOutputExposure;\n\n/*\n * ACES: the reference fit, not the cheap one.\n *\n * This was Narkowicz's single rational function, on the argument that the difference from\n * the reference curve is invisible next to the gain. That argument holds for a look and\n * fails for a *match*, which is what this option is for \u2014 aces exists so a world ported\n * from three.js reads the same, and three.js applies the full fit: into AP1 through\n * ACESInputMat, the RRT and ODT together, then back out through ACESOutputMat.\n *\n * The two curves are not a shade apart. Narkowicz is markedly brighter through the\n * midtones, which is exactly where an interior scene lives, and it is a large part of a\n * measured ~1.7x that survived correcting the colour space and the missing Lambert pi.\n * An approximation of a standard is the wrong thing to hold when the standard is the\n * specification.\n */\nvec3 rrtAndOdtFit(vec3 v) {\n vec3 a = v * (v + 0.0245786) - 0.000090537;\n vec3 b = v * (0.983729 * v + 0.4329510) + 0.238081;\n return a / b;\n}\n\nvec3 acesFilmic(vec3 x) {\n /*\n * Exposure first, because a tone curve is only defined relative to one.\n *\n * The reference RRT and ODT are graded for a dark cinema. Every renderer that ships ACES\n * scales its input to suit whatever it is actually shown on, and the factor each one\n * picks is a *look*, not a standard - so this engine takes no position and reads it from\n * the caller. Neutral by default, which is the reference grade unmodified. three.js's own\n * pick is 1 / 0.6, for a consumer matching one.\n */\n x *= uOutputExposure;\n // Columns, as GLSL reads them: sRGB -> XYZ -> D65_2_D60 -> AP1 -> RRT_SAT.\n const mat3 ACES_INPUT = mat3(\n 0.59719, 0.07600, 0.02840,\n 0.35458, 0.90834, 0.13383,\n 0.04823, 0.01566, 0.83777\n );\n // ODT_SAT -> XYZ -> D60_2_D65 -> sRGB.\n const mat3 ACES_OUTPUT = mat3(\n 1.60475, -0.10208, -0.00327,\n -0.53108, 1.10813, -0.07276,\n -0.07367, -0.00605, 1.07602\n );\n return clamp(ACES_OUTPUT * rrtAndOdtFit(ACES_INPUT * x), 0.0, 1.0);\n}\n\n\n/*\n * Linear to sRGB. The piecewise form rather than a plain pow: the linear toe near black\n * is where banding lives in an 8-bit buffer, and the cheap approximation puts it in the\n * wrong place.\n */\nvec3 linearToSrgb(vec3 c) {\n vec3 low = c * 12.92;\n vec3 high = 1.055 * pow(max(c, vec3(0.0)), vec3(1.0 / 2.4)) - 0.055;\n return mix(high, low, step(c, vec3(0.0031308)));\n}\n\n\nvec3 highlightShoulder(vec3 c) {\n float m = max(c.r, max(c.g, c.b));\n if (m <= 0.8) return c;\n float e = m - 0.8;\n return c * ((0.8 + 0.2 * e / (e + 0.2)) / m);\n}\n\nvec3 applyOutputTransform(vec3 c) {\n if (uOutputTransform == 0) return c;\n if (uOutputTransform == 2) c = acesFilmic(c);\n if (uOutputTransform == 3) c = highlightShoulder(c * uOutputExposure);\n return linearToSrgb(c);\n}\n\n\nfloat 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) {\n float v = valueNoise(p * 2.2 + vec2(0.0, -t * 0.55)) * 0.55;\n if (uNoiseOctaves == 1) return v / 0.55;\n v += valueNoise(p * 4.7 + vec2(t * 0.12, -t * 0.9)) * 0.29;\n if (uNoiseOctaves == 2) return v / 0.84;\n return v + valueNoise(p * 9.3 + vec2(-t * 0.09, -t * 1.4)) * 0.16;\n}\n\nvoid main() {\n if (uClipEnabled != 0 && dot(vec4(vWorldPos, 1.0), uClipPlane) < 0.0) discard;\n\n float t = uTime + vSeed * 53.0;\n float height = vUv.y;\n\n // Widens as it climbs: sample a compressed space low down, a stretched one up\n // top. Narrow at the bottom is what lets it sit inside the flame's tip.\n float spread = mix(0.5, 1.0, height);\n float centred = (vUv.x - 0.5) / spread + 0.5;\n\n // Soft horizontal falloff rather than a hard cut \u2014 a clipped edge here shows\n // the quad and is the single most obvious way smoke reads as fake.\n float sides = smoothstep(0.0, 0.55, 1.0 - clamp(abs(centred - 0.5) * 2.0, 0.0, 1.0));\n if (sides <= 0.001) discard;\n\n // Leans away over its height instead of rising ruler-straight.\n float drift = (valueNoise(vec2(t * 0.21, vSeed * 7.0)) - 0.5) * 0.6 * height * height;\n float n = billow(vec2(centred + drift, height), t);\n\n float density = n * sides * 1.35 - height * 0.88;\n if (density <= 0.02) discard;\n\n // Colour tells the story of the plume: ember-lit where it leaves the flame,\n // cooling through soot, then dispersing into the fog. Starting at flat grey\n // is what made it look like a separate object parked above the fire.\n vec3 col = mix(vec3(0.90, 0.42, 0.16), vec3(0.11, 0.10, 0.10), smoothstep(0.0, 0.34, height));\n vec3 atmosphereColor = mediumColor();\n col = mix(col, atmosphereColor, smoothstep(0.30, 0.95, height));\n\n // Fade in at the base so it emerges out of the flame, and out at the top so\n // it dissipates rather than ending.\n float emerge = smoothstep(0.0, 0.30, height);\n float dissipate = 1.0 - smoothstep(0.22, 0.75, height);\n\n float fog = mediumFog(vDistance, vWorldPos.y);\n col = mix(col, atmosphereColor, fog);\n float alpha = clamp(density * 0.95, 0.0, 1.0) * emerge * dissipate * 0.13;\n /* Graded where nothing after this will grade it, as every pass is. The alpha is coverage. */\n outColor = vec4(applyOutputTransform(col), alpha * (1.0 - fog));\n}\n";
|
|
8
|
+
export declare const SMOKE_FRAG = "#version 300 es\nprecision highp float;\n\nin vec2 vUv;\nin float vSeed;\nin float vDistance;\nin vec3 vWorldPos;\n\nuniform float uTime;\n\nuniform vec3 uFogColor;\n/**\n * The haze: its extinction per metre **at the camera's own height**, not at the base; its\n * reciprocal scale height per metre, 0 for a uniform medium; the camera's height, so a fragment's\n * own height is one subtraction away; and the water column's own density.\n *\n * **Four numbers in one vector rather than four uniforms**, and the reason is a phone: a scalar\n * costs a whole row of the uniform grid, and the lit stage at its eight-light rung must fit the 256\n * rows an Adreno 740 offers (uniformVectorBudget.ts). Packed, the haze's eight scalars cost two\n * rows where they cost eight, which is what paid for the second layer and the sun's glow below.\n */\nuniform vec4 uFogMedium;\n/** Where haze begins, where a linear ramp is total, and the mode: 0 a medium, 1 a ramp. */\nuniform vec4 uFogRange;\nuniform vec3 uUnderwaterColor;\nuniform float uUnderwaterFactor;\n/**\n * The second layer's extinction per metre at the camera's height and its falloff; then the least of\n * a surface the haze must leave, Atmosphere.fogMaxOpacity's complement, and the distance past which\n * it hides nothing (0 is none). **Every lane is none at zero**, and so are uFogGlow's: a program a\n * binder never wrote these into draws the haze it drew before they existed, rather than none.\n */\nuniform vec4 uFogLayer2;\n/** The sun's glow in the haze: its colour, black for none, and the exponent of its lobe. */\nuniform vec4 uFogGlow;\n/** Surface \u2192 the glow's light, and how far past the haze's start the glow begins. */\nuniform vec4 uFogGlowDir;\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 * The haze's optical depth from where it begins along the ray to a point at height pointY, dist\n * metres away: each layer's density at the eye carried over what is left of the ray, with the\n * second layer only where there is one. The fog and the sun's glow both integrate it.\n */\nfloat hazeDepth(float dist, float pointY) {\n // Where the medium begins along the ray: uFogRange.x, the linear mode's slot, which this mode\n // never read before Atmosphere.fogStart. The ray is integrated from there on, at the density\n // of that point; at a start of 0 every term is the one it always was. See render/fog.ts.\n float start = min(uFogRange.x, dist);\n float before = dist > 0.0 ? start / dist : 0.0;\n float t = (pointY - uFogMedium.z) * uFogMedium.y;\n float rest = t * (1.0 - before);\n // (1 - e^-t)/t, which tends to 1 as the ray levels out. Guarded rather than\n // branched: at t = 0 the quotient is 0/0, and 1e-4 of a scale height is 8 mm.\n float denom = abs(rest) < 1e-4 ? 1e-4 : rest;\n float depth = uFogMedium.x * exp(-t * before) * (dist - start) * (1.0 - exp(-denom)) / denom;\n if (uFogLayer2.x > 0.0) {\n float t2 = (pointY - uFogMedium.z) * uFogLayer2.y;\n float rest2 = t2 * (1.0 - before);\n float denom2 = abs(rest2) < 1e-4 ? 1e-4 : rest2;\n depth += uFogLayer2.x * exp(-t2 * before) * (dist - start) * (1.0 - exp(-denom2)) / denom2;\n }\n return depth;\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 /* Past the cutoff the air hides nothing, in either mode; the water column keeps its own law. */\n bool cut = uFogLayer2.w > 0.0 && dist > uFogLayer2.w;\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 (uFogRange.z == 1.0) {\n float span = max(uFogRange.y - uFogRange.x, 1e-4);\n float ramp = cut ? 0.0 : min(clamp((dist - uFogRange.x) / span, 0.0, 1.0), 1.0 - uFogLayer2.z);\n if (uUnderwaterFactor <= 0.0) return ramp;\n float wetLinear = uFogMedium.w * dist;\n return mix(ramp, 1.0 - exp2(-wetLinear * wetLinear * 1.442695), uUnderwaterFactor);\n }\n\n // Beer-Lambert through both layers, leaving at least uFogLayer2.z of the surface: a floor on\n // what survives rather than a ceiling on the depth, so the curve keeps its shape above it.\n float air = cut ? 0.0 : 1.0 - max(exp(-hazeDepth(dist, pointY)), uFogLayer2.z);\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 = uFogMedium.w * dist;\n return mix(air, 1.0 - exp2(-wet * wet * 1.442695), uUnderwaterFactor);\n}\n\n/**\n * The sun's light the haze scatters toward the eye along a ray to a point at height pointY, dist\n * metres away in the direction toPoint: added to a surface after its fog, never mixed. A lobe of\n * cos^exponent about the light, by as much of the ray past the glow's own start as the haze hides \u2014\n * the haze's depth per metre carried over that stretch. None in a linear ramp, past the cutoff, or\n * under water, which keeps its own medium.\n */\nvec3 mediumGlow(float dist, float pointY, vec3 toPoint) {\n if (uFogRange.z == 1.0 || max(max(uFogGlow.r, uFogGlow.g), uFogGlow.b) <= 0.0) return vec3(0.0);\n if (uFogLayer2.w > 0.0 && dist > uFogLayer2.w) return vec3(0.0);\n float span = dist - min(uFogRange.x, dist);\n if (span <= 0.0) return vec3(0.0);\n float perMetre = hazeDepth(dist, pointY) / span;\n float hidden = 1.0 - exp(-perMetre * max(span - uFogGlowDir.w, 0.0));\n float lobe = pow(clamp(dot(toPoint, uFogGlowDir.xyz), 0.0, 1.0), uFogGlow.a);\n return uFogGlow.rgb * (lobe * hidden * (1.0 - uUnderwaterFactor));\n}\n\nuniform int uNoiseOctaves;\nuniform vec4 uClipPlane;\nuniform int uClipEnabled;\n\nout vec4 outColor;\n\n/** 0 none, 1 sRGB, 2 ACES then sRGB, 3 the highlight shoulder then sRGB. See renderQuality.ts. */\nuniform int uOutputTransform;\n/** Scales the scene into the tone curve. 1 is the reference grade. See renderQuality.ts. */\nuniform float uOutputExposure;\n\n/*\n * ACES: the reference fit, not the cheap one.\n *\n * This was Narkowicz's single rational function, on the argument that the difference from\n * the reference curve is invisible next to the gain. That argument holds for a look and\n * fails for a *match*, which is what this option is for \u2014 aces exists so a world ported\n * from three.js reads the same, and three.js applies the full fit: into AP1 through\n * ACESInputMat, the RRT and ODT together, then back out through ACESOutputMat.\n *\n * The two curves are not a shade apart. Narkowicz is markedly brighter through the\n * midtones, which is exactly where an interior scene lives, and it is a large part of a\n * measured ~1.7x that survived correcting the colour space and the missing Lambert pi.\n * An approximation of a standard is the wrong thing to hold when the standard is the\n * specification.\n */\nvec3 rrtAndOdtFit(vec3 v) {\n vec3 a = v * (v + 0.0245786) - 0.000090537;\n vec3 b = v * (0.983729 * v + 0.4329510) + 0.238081;\n return a / b;\n}\n\nvec3 acesFilmic(vec3 x) {\n /*\n * Exposure first, because a tone curve is only defined relative to one.\n *\n * The reference RRT and ODT are graded for a dark cinema. Every renderer that ships ACES\n * scales its input to suit whatever it is actually shown on, and the factor each one\n * picks is a *look*, not a standard - so this engine takes no position and reads it from\n * the caller. Neutral by default, which is the reference grade unmodified. three.js's own\n * pick is 1 / 0.6, for a consumer matching one.\n */\n x *= uOutputExposure;\n // Columns, as GLSL reads them: sRGB -> XYZ -> D65_2_D60 -> AP1 -> RRT_SAT.\n const mat3 ACES_INPUT = mat3(\n 0.59719, 0.07600, 0.02840,\n 0.35458, 0.90834, 0.13383,\n 0.04823, 0.01566, 0.83777\n );\n // ODT_SAT -> XYZ -> D60_2_D65 -> sRGB.\n const mat3 ACES_OUTPUT = mat3(\n 1.60475, -0.10208, -0.00327,\n -0.53108, 1.10813, -0.07276,\n -0.07367, -0.00605, 1.07602\n );\n return clamp(ACES_OUTPUT * rrtAndOdtFit(ACES_INPUT * x), 0.0, 1.0);\n}\n\n\n/*\n * Linear to sRGB. The piecewise form rather than a plain pow: the linear toe near black\n * is where banding lives in an 8-bit buffer, and the cheap approximation puts it in the\n * wrong place.\n */\nvec3 linearToSrgb(vec3 c) {\n vec3 low = c * 12.92;\n vec3 high = 1.055 * pow(max(c, vec3(0.0)), vec3(1.0 / 2.4)) - 0.055;\n return mix(high, low, step(c, vec3(0.0031308)));\n}\n\n\nvec3 highlightShoulder(vec3 c) {\n float m = max(c.r, max(c.g, c.b));\n if (m <= 0.8) return c;\n float e = m - 0.8;\n return c * ((0.8 + 0.2 * e / (e + 0.2)) / m);\n}\n\nvec3 applyOutputTransform(vec3 c) {\n if (uOutputTransform == 0) return c;\n if (uOutputTransform == 2) c = acesFilmic(c);\n if (uOutputTransform == 3) c = highlightShoulder(c * uOutputExposure);\n return linearToSrgb(c);\n}\n\n\nfloat 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) {\n float v = valueNoise(p * 2.2 + vec2(0.0, -t * 0.55)) * 0.55;\n if (uNoiseOctaves == 1) return v / 0.55;\n v += valueNoise(p * 4.7 + vec2(t * 0.12, -t * 0.9)) * 0.29;\n if (uNoiseOctaves == 2) return v / 0.84;\n return v + valueNoise(p * 9.3 + vec2(-t * 0.09, -t * 1.4)) * 0.16;\n}\n\nvoid main() {\n if (uClipEnabled != 0 && dot(vec4(vWorldPos, 1.0), uClipPlane) < 0.0) discard;\n\n float t = uTime + vSeed * 53.0;\n float height = vUv.y;\n\n // Widens as it climbs: sample a compressed space low down, a stretched one up\n // top. Narrow at the bottom is what lets it sit inside the flame's tip.\n float spread = mix(0.5, 1.0, height);\n float centred = (vUv.x - 0.5) / spread + 0.5;\n\n // Soft horizontal falloff rather than a hard cut \u2014 a clipped edge here shows\n // the quad and is the single most obvious way smoke reads as fake.\n float sides = smoothstep(0.0, 0.55, 1.0 - clamp(abs(centred - 0.5) * 2.0, 0.0, 1.0));\n if (sides <= 0.001) discard;\n\n // Leans away over its height instead of rising ruler-straight.\n float drift = (valueNoise(vec2(t * 0.21, vSeed * 7.0)) - 0.5) * 0.6 * height * height;\n float n = billow(vec2(centred + drift, height), t);\n\n float density = n * sides * 1.35 - height * 0.88;\n if (density <= 0.02) discard;\n\n // Colour tells the story of the plume: ember-lit where it leaves the flame,\n // cooling through soot, then dispersing into the fog. Starting at flat grey\n // is what made it look like a separate object parked above the fire.\n vec3 col = mix(vec3(0.90, 0.42, 0.16), vec3(0.11, 0.10, 0.10), smoothstep(0.0, 0.34, height));\n vec3 atmosphereColor = mediumColor();\n col = mix(col, atmosphereColor, smoothstep(0.30, 0.95, height));\n\n // Fade in at the base so it emerges out of the flame, and out at the top so\n // it dissipates rather than ending.\n float emerge = smoothstep(0.0, 0.30, height);\n float dissipate = 1.0 - smoothstep(0.22, 0.75, height);\n\n float fog = mediumFog(vDistance, vWorldPos.y);\n col = mix(col, atmosphereColor, fog);\n float alpha = clamp(density * 0.95, 0.0, 1.0) * emerge * dissipate * 0.13;\n /* Graded where nothing after this will grade it, as every pass is. The alpha is coverage. */\n outColor = vec4(applyOutputTransform(col), alpha * (1.0 - fog));\n}\n";
|
|
@@ -15,4 +15,4 @@
|
|
|
15
15
|
* ocean from a fixed vertex budget without the surface sliding underfoot.
|
|
16
16
|
*/
|
|
17
17
|
export declare const WATER_VERT = "#version 300 es\nlayout(location = 0) in vec2 aGrid;\n\nuniform mat4 uViewProj;\nuniform mat4 uReflectionViewProj;\nuniform vec3 uCameraPos;\nuniform float uTime;\n/**\n * Where the sheet is laid, in world XZ.\n *\n * The ocean passes its own camera position snapped to whole cells, which is what\n * gives an endless sea from a fixed vertex budget without the surface sliding\n * underfoot. A *bounded* sheet \u2014 a pool in a courtyard \u2014 passes the middle of the\n * thing it is filling instead, and is world-fixed by construction.\n *\n * Snapped on the CPU rather than here so that one number is the only difference\n * between an ocean and a basin, and the shader has no idea which it is drawing.\n */\nuniform vec2 uGridOrigin;\n/**\n * Metres per unit of the sheet's own grid, along each of the body's own axes.\n *\n * One and one for the ocean, whose grid is authored in metres and is two meshes wide.\n * A bounded body \u2014 a pool, a tank, a puddle, a channel \u2014 is drawn from a unit sheet\n * and scaled by its own half-extents, which is what lets one mesh and one material\n * serve every body of water in the world.\n *\n * **Two numbers and not one since 2026-08-28.** A single span makes every bounded\n * body square, and a square that contains a channel floods the ground either side\n * of it. The ocean passes the same number twice and is bit-identical to what it was.\n */\nuniform vec2 uGridSpan;\nuniform vec2 uGridHalf;\nuniform vec2 uNearHalf;\n/**\n * The body's own +z, in world XZ, unit length. (0, 1) leaves the sheet unturned.\n *\n * A direction rather than an angle, so nothing here needs a trig call and a consumer\n * bounding a channel can hand over the direction its run already takes. The across\n * axis is world up crossed with this, `(fz, -fx)`, which is the basis\n * `MeshBuilder.addOrientedBox` builds from a forward vector \u2014 one convention for an\n * oriented thing, so a reader who has met one has met both.\n */\nuniform vec2 uGridForward;\n/** Prevailing wind bearing, normalised. Waves are steered toward it. */\nuniform vec2 uWindDir;\n/** Multiplier on authored steepness \u2014 how built-up the sea is. */\nuniform float uWaveGain;\nuniform float uWaterLevel;\n\nout vec3 vWorldPos;\nout vec3 vNormal;\nout float vCrest;\nout vec4 vReflectionClip;\n/** Grid-local offset, so the sheet can fade out its own boundary. */\nout vec2 vGrid;\n\n\nconst int GERSTNER_COUNT = 4;\n/*\n * direction.xy, steepness, wavelength. Amplitude is steepness/k, so these sum to\n * roughly a 1m swell \u2014 enough to read as a living surface, low enough that crests\n * never wash over an island whose deck sits ~1.8m above sea level.\n */\nconst vec4 GERSTNER_WAVES[4] = vec4[4](\n vec4( 1.0, 0.0, 0.115, 34.0),\n vec4( 0.6, 0.8, 0.100, 18.0),\n vec4(-0.7, 0.7, 0.080, 9.0),\n vec4( 0.2, -0.98, 0.060, 5.0)\n);\n\n/*\n * Steer each wave toward the wind, but only partway. A real sea is a dominant\n * swell plus cross-chop; rotating all four onto one bearing gives corduroy,\n * which reads as a material rather than as water.\n */\nvec2 gerstnerDirection(int i, vec2 windDir) {\n return normalize(mix(normalize(GERSTNER_WAVES[i].xy), windDir, 0.62));\n}\n\nfloat gerstnerWavenumber(int i) {\n return 6.28318530718 / GERSTNER_WAVES[i].w;\n}\n\n/** Deep-water phase in radians at a point on the resting plane. */\nfloat gerstnerPhase(float k, vec2 dir, vec2 p, float time) {\n float c = sqrt(9.81 / k); // deep-water phase speed\n return k * (dot(dir, p) - c * time);\n}\n\nstruct GerstnerSurface {\n /** Displacement from the resting plane \u2014 horizontal as well as vertical. */\n vec3 offset;\n vec3 normal;\n /** Height in units of steepness; positive near a crest. */\n float crest;\n};\n\nGerstnerSurface gerstnerSurface(vec2 p, float time, vec2 windDir, float gain) {\n GerstnerSurface s;\n s.offset = vec3(0.0);\n s.normal = vec3(0.0, 1.0, 0.0);\n s.crest = 0.0;\n for (int i = 0; i < GERSTNER_COUNT; i++) {\n vec2 dir = gerstnerDirection(i, windDir);\n float steepness = GERSTNER_WAVES[i].z * gain;\n float k = gerstnerWavenumber(i);\n float f = gerstnerPhase(k, dir, p, time);\n float a = steepness / k; // amplitude from steepness\n\n s.offset.x += dir.x * a * cos(f);\n s.offset.y += a * sin(f);\n s.offset.z += dir.y * a * cos(f);\n\n // Analytic normal: the partial derivatives of the displacement above.\n s.normal.x -= dir.x * steepness * cos(f);\n s.normal.z -= dir.y * steepness * cos(f);\n s.normal.y -= steepness * sin(f);\n\n s.crest += sin(f) * steepness;\n }\n return s;\n}\n\n/**\n * Second derivatives of the height field: (d2h/dx2, d2h/dz2, d2h/dxdz).\n *\n * How the surface *bends*, which is what decides where reflected light\n * converges. Analytic from the same sum, so it cannot disagree with the shape\n * the vertex stage displaced.\n */\nvec3 gerstnerCurvature(vec2 p, float time, vec2 windDir, float gain) {\n vec3 h = vec3(0.0);\n for (int i = 0; i < GERSTNER_COUNT; i++) {\n vec2 dir = gerstnerDirection(i, windDir);\n float steepness = GERSTNER_WAVES[i].z * gain;\n float k = gerstnerWavenumber(i);\n float f = gerstnerPhase(k, dir, p, time);\n // Second derivative of (steepness/k)*sin(f) along the wave's own bearing.\n float second = -steepness * k * sin(f);\n h.x += second * dir.x * dir.x;\n h.y += second * dir.y * dir.y;\n h.z += second * dir.x * dir.y;\n }\n return h;\n}\n\n\nvoid main() {\n /*\n * Waves must reach nothing by the near sheet's edge, because beyond it lies\n * the flat skirt. A displaced rim meeting an undisplaced skirt would tear the\n * surface open along a visible seam; fading first makes the two meshes agree\n * exactly where they meet. Chebyshev distance, so the fade band follows the\n * rectangular boundary rather than cutting an ellipse inside it.\n *\n * **Measured as a fraction of each axis' own extent**, which is what lets a\n * channel three metres across and twenty-one long fade evenly at both its ends\n * and both its sides. Against one shared extent the narrow axis would be inside\n * the fade band along its whole length and the water would never reach its own\n * bank. For the ocean, whose two extents are equal, this is the same arithmetic\n * with the division moved one step earlier.\n *\n * It is also the honest thing to draw. At a few hundred metres a 1 m swell is\n * well under a pixel, and out on the skirt a float32 phase argument has lost\n * most of its meaningful bits \u2014 a calm horizon is what an ocean actually looks\n * like from that far away.\n */\n vec2 grid = aGrid * uGridSpan;\n /* The same offset the fragment stage fades the rim on, so the two cannot disagree. */\n vGrid = grid;\n vec2 rimFraction = abs(grid) / max(uNearHalf, vec2(1e-4));\n float rimDistance = max(rimFraction.x, rimFraction.y);\n float waveFade = 1.0 - smoothstep(0.35, 0.97, rimDistance);\n\n /*\n * Into the world: across the body, then along it. `right = up x forward` is\n * `(fz, -fx)`, and getting that pair the wrong way round is a sheet mirrored\n * about its own axis, which on a square is invisible and on a channel is a\n * channel across the road instead of along it.\n */\n vec2 laid = vec2(uGridForward.y, -uGridForward.x) * grid.x + uGridForward * grid.y;\n\n vec3 base = vec3(uGridOrigin.x + laid.x, uWaterLevel, uGridOrigin.y + laid.y);\n\n GerstnerSurface sea = gerstnerSurface(base.xz, uTime, uWindDir, waveFade * uWaveGain);\n vec3 pos = base + sea.offset;\n\n vWorldPos = pos;\n vNormal = normalize(sea.normal);\n vCrest = sea.crest;\n vReflectionClip = uReflectionViewProj * vec4(pos, 1.0);\n gl_Position = uViewProj * vec4(pos, 1.0);\n}\n";
|
|
18
|
-
export declare const WATER_FRAG = "#version 300 es\nprecision highp float;\n\nin vec3 vWorldPos;\nin vec3 vNormal;\nin float vCrest;\nin vec4 vReflectionClip;\nin vec2 vGrid;\n\nuniform vec2 uGridHalf;\nuniform float uFoamGain;\n/**\n * How much of what is beneath it a body of water hides, looked straight down.\n *\n * The *density* of the water rather than a rendering trick: a sea hides its own\n * floor, a fountain shows the tiles at the bottom of it, and a puddle is nearly\n * clear. Only the nadir end is configurable \u2014 at a grazing angle every water\n * surface goes reflective, which is Fresnel and not a property of the liquid.\n */\nuniform float uNadirOpacity;\n/** The whole surface's visibility, 0 to 1. Zero draws nothing at all. */\nuniform float uVisibility;\n/**\n * How much this body mirrors regardless of the angle it is seen at, 0 to 1.\n *\n * Zero is physics: reflection is Fresnel alone, which is right for an ocean seen\n * across its own surface and which means *almost nothing* when you stand over a\n * basin and look down into it \u2014 the angle is steep, the Fresnel term is a few\n * percent, and a reflection nobody can see is the same as no reflection: the\n * character's reflection is technically present and far too faint to perceive.\n *\n * So a body may declare itself more mirror than water. It is a lie about optics\n * and an honest one about what a small pool is *for*: you look into it to see\n * something. The sea keeps zero and behaves exactly as it always has.\n */\nuniform float uMirror;\nuniform vec3 uDeepColor;\nuniform vec3 uShallowColor;\nuniform vec3 uDirectionalDir;\nuniform vec3 uDirectionalColor;\nuniform vec3 uAmbient;\n/**\n * The scene's point lights, which water is entitled to as much as stone is.\n *\n * The surface used to answer to the sun alone, on the reasonable assumption that water\n * is a thing that sits under the sky. It is not: a flooded corridor, a cistern, an\n * indoor pool and a harbour at night are all water lit entirely by lamps, and under\n * those this shaded to flat ambient \u2014 a coloured plane, with the specular glitter that\n * is the single strongest cue that a surface *is* water switched off because no\n * directional source existed to make it. A lantern on a jetty threw nothing.\n *\n * Same slots, same weights and the surface shader's falloffs, a light's own exponent among\n * them, so a lamp reaches as far over water as over the ground beside it. **What it gives up**:\n * a spot's cone, which water does not read, so a spot lights the water around it as a lamp\n * would; and the smooth falloff's square, which the surface applies and this has never, so under\n * that falloff a lamp's pool on water is the wider of the two.\n */\n#define MAX_LIGHTS 16\nuniform int uLightCount;\nuniform vec3 uLightPos[MAX_LIGHTS];\nuniform vec3 uLightColor[MAX_LIGHTS];\nuniform float uLightRadius[MAX_LIGHTS];\nuniform float uLightWeight[MAX_LIGHTS];\nuniform int uLightFalloff;\n/* The surface shader's cone record, of which water reads z alone: a light's own falloff exponent. */\nuniform vec4 uLightCone[MAX_LIGHTS];\n\nuniform vec3 uFogColor;\n/** Extinction per metre **at the camera's own height**, not at the base. */\nuniform float uFogDensity;\n/** Reciprocal scale height of the haze, per metre. 0 is a uniform medium. */\nuniform float uFogHeightFalloff;\n/** The camera's height, so a fragment's own height is one subtraction away. */\nuniform float uFogEyeY;\nuniform vec3 uUnderwaterColor;\nuniform float uUnderwaterFogDensity;\nuniform float uUnderwaterFactor;\n/** 0 integrates a medium (Beer-Lambert); 1 ramps linearly between near and far. */\nuniform int uFogMode;\n/** Linear mode only: where haze begins, and where it is total. */\nuniform float uFogNear;\nuniform float uFogFar;\n\n/** What distance converges toward: the day's haze, or the water column. */\nvec3 mediumColor() {\n return mix(uFogColor, uUnderwaterColor, uUnderwaterFactor);\n}\n\n/**\n * How much of a surface standing at height pointY, dist metres away, is lost to\n * the medium in between. 0 is perfectly clear, 1 is the medium's own colour.\n */\nfloat mediumFog(float dist, float pointY) {\n /*\n * A depth ramp rather than a medium, for worlds authored against one.\n *\n * Beer-Lambert is the honest model and stays the default: it integrates something\n * physically there, so extinction begins at the eye and never stops. A linear near/far\n * fog is not that \u2014 it is a *look*, with a hard start distance before which the air is\n * perfectly clear, and it is what the fixed-function pipeline offered and what several\n * renderers still expose. Approximating one with the other is not a matter of tuning:\n * matched at half occlusion, the exponential curve still lays haze over everything\n * inside near, where the ramp lays none, and near-field colour desaturates toward the\n * fog colour with nothing to show for it.\n *\n * Height falloff has no meaning here and is ignored: a ramp is defined on distance\n * alone.\n */\n if (uFogMode == 1) {\n float span = max(uFogFar - uFogNear, 1e-4);\n float ramp = clamp((dist - uFogNear) / span, 0.0, 1.0);\n if (uUnderwaterFactor <= 0.0) return ramp;\n float wetLinear = uUnderwaterFogDensity * dist;\n return mix(ramp, 1.0 - exp2(-wetLinear * wetLinear * 1.442695), uUnderwaterFactor);\n }\n\n // Where the medium begins along the ray: uFogNear, the linear mode's slot, which this mode\n // never read before Atmosphere.fogStart. The ray is integrated from there on, at the density\n // of that point; at a start of 0 every term is the one it always was. See render/fog.ts.\n float start = min(uFogNear, dist);\n float before = dist > 0.0 ? start / dist : 0.0;\n float t = (pointY - uFogEyeY) * uFogHeightFalloff;\n float rest = t * (1.0 - before);\n // (1 - e^-t)/t, which tends to 1 as the ray levels out. Guarded rather than\n // branched: at t = 0 the quotient is 0/0, and 1e-4 of a scale height is 8 mm.\n float denom = abs(rest) < 1e-4 ? 1e-4 : rest;\n float air =\n 1.0 - exp(-uFogDensity * exp(-t * before) * (dist - start) * (1.0 - exp(-denom)) / denom);\n if (uUnderwaterFactor <= 0.0) return air;\n // Water is its own medium and keeps its own curve, so crossing the surface\n // changes nothing about how the water column reads.\n float wet = uUnderwaterFogDensity * dist;\n return mix(air, 1.0 - exp2(-wet * wet * 1.442695), uUnderwaterFactor);\n}\n\nuniform vec3 uCameraPos;\nuniform sampler2D uReflectionMap;\nuniform int uReflectionEnabled;\nuniform vec2 uReflectionTexelSize;\nuniform int uReflectionFilterTaps;\n\n/** 0 none, 1 sRGB, 2 ACES then sRGB, 3 the highlight shoulder then sRGB. See renderQuality.ts. */\nuniform int uOutputTransform;\n/** Scales the scene into the tone curve. 1 is the reference grade. See renderQuality.ts. */\nuniform float uOutputExposure;\n\n/*\n * ACES: the reference fit, not the cheap one.\n *\n * This was Narkowicz's single rational function, on the argument that the difference from\n * the reference curve is invisible next to the gain. That argument holds for a look and\n * fails for a *match*, which is what this option is for \u2014 aces exists so a world ported\n * from three.js reads the same, and three.js applies the full fit: into AP1 through\n * ACESInputMat, the RRT and ODT together, then back out through ACESOutputMat.\n *\n * The two curves are not a shade apart. Narkowicz is markedly brighter through the\n * midtones, which is exactly where an interior scene lives, and it is a large part of a\n * measured ~1.7x that survived correcting the colour space and the missing Lambert pi.\n * An approximation of a standard is the wrong thing to hold when the standard is the\n * specification.\n */\nvec3 rrtAndOdtFit(vec3 v) {\n vec3 a = v * (v + 0.0245786) - 0.000090537;\n vec3 b = v * (0.983729 * v + 0.4329510) + 0.238081;\n return a / b;\n}\n\nvec3 acesFilmic(vec3 x) {\n /*\n * Exposure first, because a tone curve is only defined relative to one.\n *\n * The reference RRT and ODT are graded for a dark cinema. Every renderer that ships ACES\n * scales its input to suit whatever it is actually shown on, and the factor each one\n * picks is a *look*, not a standard - so this engine takes no position and reads it from\n * the caller. Neutral by default, which is the reference grade unmodified. three.js's own\n * pick is 1 / 0.6, for a consumer matching one.\n */\n x *= uOutputExposure;\n // Columns, as GLSL reads them: sRGB -> XYZ -> D65_2_D60 -> AP1 -> RRT_SAT.\n const mat3 ACES_INPUT = mat3(\n 0.59719, 0.07600, 0.02840,\n 0.35458, 0.90834, 0.13383,\n 0.04823, 0.01566, 0.83777\n );\n // ODT_SAT -> XYZ -> D60_2_D65 -> sRGB.\n const mat3 ACES_OUTPUT = mat3(\n 1.60475, -0.10208, -0.00327,\n -0.53108, 1.10813, -0.07276,\n -0.07367, -0.00605, 1.07602\n );\n return clamp(ACES_OUTPUT * rrtAndOdtFit(ACES_INPUT * x), 0.0, 1.0);\n}\n\n\n/*\n * Linear to sRGB. The piecewise form rather than a plain pow: the linear toe near black\n * is where banding lives in an 8-bit buffer, and the cheap approximation puts it in the\n * wrong place.\n */\nvec3 linearToSrgb(vec3 c) {\n vec3 low = c * 12.92;\n vec3 high = 1.055 * pow(max(c, vec3(0.0)), vec3(1.0 / 2.4)) - 0.055;\n return mix(high, low, step(c, vec3(0.0031308)));\n}\n\n\nvec3 highlightShoulder(vec3 c) {\n float m = max(c.r, max(c.g, c.b));\n if (m <= 0.8) return c;\n float e = m - 0.8;\n return c * ((0.8 + 0.2 * e / (e + 0.2)) / m);\n}\n\nvec3 applyOutputTransform(vec3 c) {\n if (uOutputTransform == 0) return c;\n if (uOutputTransform == 2) c = acesFilmic(c);\n if (uOutputTransform == 3) c = highlightShoulder(c * uOutputExposure);\n return linearToSrgb(c);\n}\n\n\nconst vec2 REFLECTION_FILTER[9] = vec2[9](\n vec2( 0.00, 0.00), vec2(-0.62, -0.31), vec2( 0.54, 0.43),\n vec2(-0.34, 0.76), vec2( 0.78, -0.57), vec2(-0.88, 0.28),\n vec2( 0.22, -0.91), vec2( 0.91, 0.08), vec2(-0.18, 0.94)\n);\n// Preserve small moving silhouettes such as a character while still spreading\n// the surrounding taps enough to make the target read as rough water.\nconst float REFLECTION_FILTER_WEIGHT[9] = float[9](\n 0.28, 0.09, 0.09, 0.09, 0.09, 0.09, 0.09, 0.09, 0.09\n);\n\nout vec4 outColor;\n\nvoid main() {\n vec3 n = normalize(vNormal);\n vec3 view = normalize(uCameraPos - vWorldPos);\n\n // Fresnel: water is nearly a mirror at grazing angles and clear straight down.\n // abs keeps the underside physically continuous with the top instead of\n // turning the entire surface into an opaque mirror below the waterline.\n float facing = clamp(abs(dot(n, view)), 0.0, 1.0);\n float fresnel = 0.02 + 0.98 * pow(1.0 - facing, 5.0);\n\n vec3 atmosphereColor = mediumColor();\n\n vec3 col = mix(uDeepColor, uShallowColor, clamp(vCrest * 1.4 + 0.4, 0.0, 1.0));\n vec3 incident = uAmbient +\n uDirectionalColor * max(dot(n, uDirectionalDir), 0.0) * 0.6;\n\n // Specular glitter along the crests \u2014 the strongest single cue that a surface\n // is water rather than a coloured plane.\n vec3 halfVec = normalize(uDirectionalDir + view);\n float spec = pow(max(dot(n, halfVec), 0.0), 220.0);\n vec3 highlight = uDirectionalColor * spec * 1.6;\n\n /*\n * And the same from every lamp in range: a light over water lights it, and glitters\n * on it. Indoors this is the whole of the surface's lighting.\n *\n * The exponent is the sun's, so a crest picks a lamp out as tightly as it picks out\n * the sky \u2014 which is what makes a strip light read as a *streak* across a pool rather\n * than a broad sheen. Weighted by presence like every other consumer of these slots,\n * so a light entering the set fades in instead of appearing.\n */\n for (int i = 0; i < MAX_LIGHTS; i++) {\n if (i >= uLightCount) break;\n vec3 toLight = uLightPos[i] - vWorldPos;\n float lightDist = length(toLight);\n float falloff;\n float exponent = uLightCone[i].z;\n if (exponent > 0.0) {\n float reach = lightDist / max(uLightRadius[i], 1e-4);\n falloff = pow(clamp(1.0 - reach * reach, 0.0, 1.0), exponent);\n } else if (uLightFalloff == 1) {\n float window = clamp(1.0 - pow(lightDist / max(uLightRadius[i], 1e-4), 4.0), 0.0, 1.0);\n falloff = window * window / max(lightDist * lightDist, 0.01);\n } else {\n falloff = clamp(1.0 - lightDist / max(uLightRadius[i], 1e-4), 0.0, 1.0);\n }\n if (falloff <= 0.0) continue;\n vec3 lightDir = toLight / max(lightDist, 1e-4);\n float shaped = falloff * uLightWeight[i];\n incident += uLightColor[i] * max(dot(n, lightDir), 0.0) * shaped * 0.6;\n vec3 lampHalf = normalize(lightDir + view);\n highlight += uLightColor[i] * pow(max(dot(n, lampHalf), 0.0), 220.0) * shaped * 1.6;\n }\n\n col *= incident;\n col += highlight;\n\n // Foam where crests peak \u2014 and only when the wind is actually breaking them.\n float foam = smoothstep(0.55, 0.85, vCrest) * uFoamGain;\n col = mix(col, vec3(0.92, 0.96, 1.0), foam * 0.5);\n\n float dist = distance(vWorldPos, uCameraPos);\n float fog = mediumFog(dist, vWorldPos.y);\n col = mix(col, atmosphereColor, fog);\n /*\n * **Graded here, before the mirror is mixed in**, as every pass grades what it draws when\n * nothing after it will: the mirror's own passes graded what it holds, so mixing first would put\n * the curve on it twice. The air's colour, which stands in where the mirror shows nothing, is\n * graded to meet it, as the sky behind it is.\n */\n col = applyOutputTransform(col);\n vec3 gradedAir = applyOutputTransform(atmosphereColor);\n\n // Transparency is angle-dependent, like real water: looking down you see\n // straight through to the seabed, at grazing angles it turns reflective.\n // This is computed before reflection because straight-alpha compositing must\n // not attenuate reflected radiance a second time below.\n float airOpacity = mix(uNadirOpacity, 0.96, fresnel);\n float underwaterOpacity = mix(uNadirOpacity * 0.55, 0.88, fresnel);\n float opacity = mix(airOpacity, underwaterOpacity, uUnderwaterFactor);\n opacity = mix(opacity, 0.96, foam * 0.7);\n\n // Project the displaced surface into the mirrored camera. The Gerstner\n // displacement supplies broad distortion; the analytic normal adds the\n // smaller ripples that make reflected silhouettes move with each crest.\n vec3 reflected = gradedAir;\n float reflectionValid = 0.0;\n if (uReflectionEnabled != 0 && vReflectionClip.w > 0.0) {\n vec2 reflectionUv = vReflectionClip.xy / vReflectionClip.w * 0.5 + 0.5;\n reflectionUv += n.xz * mix(0.008, 0.022, fresnel);\n vec2 border = min(reflectionUv, 1.0 - reflectionUv);\n reflectionValid = smoothstep(0.0, 0.025, min(border.x, border.y));\n // A planar target is optically sharp, which is correct for a mirror but\n // wrong for a moving water surface. Broaden it over a Poisson footprint\n // that grows gently with viewing distance; normal displacement above\n // still makes every crest steer that softened image independently.\n float blurPixels = 1.35 + min(dist * 0.022, 3.5);\n vec3 sceneReflection = vec3(0.0);\n float filterWeight = 0.0;\n for (int i = 0; i < 9; i++) {\n if (i >= uReflectionFilterTaps) break;\n vec2 sampleUv = reflectionUv +\n REFLECTION_FILTER[i] * uReflectionTexelSize * blurPixels;\n float weight = REFLECTION_FILTER_WEIGHT[i];\n /*\n * `textureLod` rather than `texture`, by the rule `AGENTS.md` records on 2026-08-07:\n * this loop is reached through a branch on `vReflectionClip`, which is not uniform, and\n * an implicit derivative there is undefined. **WGSL does not merely leave it undefined,\n * it refuses to compile the module**: *\"'textureSample' must only be called from uniform\n * control flow\"*, with the note that reading `vReflectionClip` may be non-uniform.\n *\n * Bit-identical on WebGL2: `planarReflection.ts` allocates one level and filters\n * `LINEAR`, so there is no mip to select and level zero is the only thing `texture`\n * could have read.\n */\n sceneReflection +=\n textureLod(uReflectionMap, clamp(sampleUv, 0.0, 1.0), 0.0).rgb * weight;\n filterWeight += weight;\n }\n sceneReflection /= max(filterWeight, 0.001);\n reflected = mix(gradedAir, sceneReflection, reflectionValid);\n }\n // The atmosphere-colour fallback preserves the old sky reflection when a\n // quality profile disables the extra scene pass. Foam scatters rather than\n // mirroring, so it masks the reflection at breaking crests.\n float sceneWeight = float(uReflectionEnabled) * reflectionValid;\n float reflectionGain = mix(0.75, 0.94, sceneWeight);\n // Angle-honest by default, and a mirror where a body says so. See uMirror above.\n float reflectivity = mix(fresnel, 1.0, uMirror);\n float reflectedRadiance = min(\n reflectivity * reflectionGain * (1.0 - foam * 0.8),\n opacity\n );\n // With SRC_ALPHA blending, mixing by Fresnel directly would multiply the\n // reflection by opacity again. Divide it out here so the framebuffer gets\n // the intended reflected fraction while the remainder stays transmissive.\n float reflectionWeight = reflectedRadiance / max(opacity, 0.001);\n col = mix(col, reflected, reflectionWeight);\n\n /*\n * Dissolve the sheet at its own boundary.\n *\n * Fading on fog alone made the horizon a function of the daily palette: a\n * clear day never reached the threshold, so the grid's rim \u2014 corner and all \u2014\n * became the skyline. Fading on the grid coordinate cannot fail that way,\n * because alpha reaches zero at the last row of vertices by construction.\n * Chebyshev distance because the sheet is a rectangle, so the fade band\n * follows its four edges instead of cutting an ellipse out of it \u2014 and it is\n * measured per axis, or a channel's narrow sides would be one continuous fade\n * and it would have no visible edge at all.\n */\n vec2 rimFraction = abs(vGrid) / max(uGridHalf, vec2(0.001));\n float rimT = max(rimFraction.x, rimFraction.y);\n float rim = 1.0 - smoothstep(0.9, 1.0, rimT);\n // Fog still helps where it is thick, it just is no longer load-bearing.\n float edge = 1.0 - smoothstep(0.82, 1.0, fog);\n outColor = vec4(col, opacity * edge * rim * uVisibility);\n}\n";
|
|
18
|
+
export declare const WATER_FRAG = "#version 300 es\nprecision highp float;\n\nin vec3 vWorldPos;\nin vec3 vNormal;\nin float vCrest;\nin vec4 vReflectionClip;\nin vec2 vGrid;\n\nuniform vec2 uGridHalf;\nuniform float uFoamGain;\n/**\n * How much of what is beneath it a body of water hides, looked straight down.\n *\n * The *density* of the water rather than a rendering trick: a sea hides its own\n * floor, a fountain shows the tiles at the bottom of it, and a puddle is nearly\n * clear. Only the nadir end is configurable \u2014 at a grazing angle every water\n * surface goes reflective, which is Fresnel and not a property of the liquid.\n */\nuniform float uNadirOpacity;\n/** The whole surface's visibility, 0 to 1. Zero draws nothing at all. */\nuniform float uVisibility;\n/**\n * How much this body mirrors regardless of the angle it is seen at, 0 to 1.\n *\n * Zero is physics: reflection is Fresnel alone, which is right for an ocean seen\n * across its own surface and which means *almost nothing* when you stand over a\n * basin and look down into it \u2014 the angle is steep, the Fresnel term is a few\n * percent, and a reflection nobody can see is the same as no reflection: the\n * character's reflection is technically present and far too faint to perceive.\n *\n * So a body may declare itself more mirror than water. It is a lie about optics\n * and an honest one about what a small pool is *for*: you look into it to see\n * something. The sea keeps zero and behaves exactly as it always has.\n */\nuniform float uMirror;\nuniform vec3 uDeepColor;\nuniform vec3 uShallowColor;\nuniform vec3 uDirectionalDir;\nuniform vec3 uDirectionalColor;\nuniform vec3 uAmbient;\n/**\n * The scene's point lights, which water is entitled to as much as stone is.\n *\n * The surface used to answer to the sun alone, on the reasonable assumption that water\n * is a thing that sits under the sky. It is not: a flooded corridor, a cistern, an\n * indoor pool and a harbour at night are all water lit entirely by lamps, and under\n * those this shaded to flat ambient \u2014 a coloured plane, with the specular glitter that\n * is the single strongest cue that a surface *is* water switched off because no\n * directional source existed to make it. A lantern on a jetty threw nothing.\n *\n * Same slots, same weights and the surface shader's falloffs, a light's own exponent among\n * them, so a lamp reaches as far over water as over the ground beside it. **What it gives up**:\n * a spot's cone, which water does not read, so a spot lights the water around it as a lamp\n * would; and the smooth falloff's square, which the surface applies and this has never, so under\n * that falloff a lamp's pool on water is the wider of the two.\n */\n#define MAX_LIGHTS 16\nuniform int uLightCount;\nuniform vec3 uLightPos[MAX_LIGHTS];\nuniform vec3 uLightColor[MAX_LIGHTS];\nuniform float uLightRadius[MAX_LIGHTS];\nuniform float uLightWeight[MAX_LIGHTS];\nuniform int uLightFalloff;\n/* The surface shader's cone record, of which water reads z alone: a light's own falloff exponent. */\nuniform vec4 uLightCone[MAX_LIGHTS];\n\nuniform vec3 uFogColor;\n/**\n * The haze: its extinction per metre **at the camera's own height**, not at the base; its\n * reciprocal scale height per metre, 0 for a uniform medium; the camera's height, so a fragment's\n * own height is one subtraction away; and the water column's own density.\n *\n * **Four numbers in one vector rather than four uniforms**, and the reason is a phone: a scalar\n * costs a whole row of the uniform grid, and the lit stage at its eight-light rung must fit the 256\n * rows an Adreno 740 offers (uniformVectorBudget.ts). Packed, the haze's eight scalars cost two\n * rows where they cost eight, which is what paid for the second layer and the sun's glow below.\n */\nuniform vec4 uFogMedium;\n/** Where haze begins, where a linear ramp is total, and the mode: 0 a medium, 1 a ramp. */\nuniform vec4 uFogRange;\nuniform vec3 uUnderwaterColor;\nuniform float uUnderwaterFactor;\n/**\n * The second layer's extinction per metre at the camera's height and its falloff; then the least of\n * a surface the haze must leave, Atmosphere.fogMaxOpacity's complement, and the distance past which\n * it hides nothing (0 is none). **Every lane is none at zero**, and so are uFogGlow's: a program a\n * binder never wrote these into draws the haze it drew before they existed, rather than none.\n */\nuniform vec4 uFogLayer2;\n/** The sun's glow in the haze: its colour, black for none, and the exponent of its lobe. */\nuniform vec4 uFogGlow;\n/** Surface \u2192 the glow's light, and how far past the haze's start the glow begins. */\nuniform vec4 uFogGlowDir;\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 * The haze's optical depth from where it begins along the ray to a point at height pointY, dist\n * metres away: each layer's density at the eye carried over what is left of the ray, with the\n * second layer only where there is one. The fog and the sun's glow both integrate it.\n */\nfloat hazeDepth(float dist, float pointY) {\n // Where the medium begins along the ray: uFogRange.x, the linear mode's slot, which this mode\n // never read before Atmosphere.fogStart. The ray is integrated from there on, at the density\n // of that point; at a start of 0 every term is the one it always was. See render/fog.ts.\n float start = min(uFogRange.x, dist);\n float before = dist > 0.0 ? start / dist : 0.0;\n float t = (pointY - uFogMedium.z) * uFogMedium.y;\n float rest = t * (1.0 - before);\n // (1 - e^-t)/t, which tends to 1 as the ray levels out. Guarded rather than\n // branched: at t = 0 the quotient is 0/0, and 1e-4 of a scale height is 8 mm.\n float denom = abs(rest) < 1e-4 ? 1e-4 : rest;\n float depth = uFogMedium.x * exp(-t * before) * (dist - start) * (1.0 - exp(-denom)) / denom;\n if (uFogLayer2.x > 0.0) {\n float t2 = (pointY - uFogMedium.z) * uFogLayer2.y;\n float rest2 = t2 * (1.0 - before);\n float denom2 = abs(rest2) < 1e-4 ? 1e-4 : rest2;\n depth += uFogLayer2.x * exp(-t2 * before) * (dist - start) * (1.0 - exp(-denom2)) / denom2;\n }\n return depth;\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 /* Past the cutoff the air hides nothing, in either mode; the water column keeps its own law. */\n bool cut = uFogLayer2.w > 0.0 && dist > uFogLayer2.w;\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 (uFogRange.z == 1.0) {\n float span = max(uFogRange.y - uFogRange.x, 1e-4);\n float ramp = cut ? 0.0 : min(clamp((dist - uFogRange.x) / span, 0.0, 1.0), 1.0 - uFogLayer2.z);\n if (uUnderwaterFactor <= 0.0) return ramp;\n float wetLinear = uFogMedium.w * dist;\n return mix(ramp, 1.0 - exp2(-wetLinear * wetLinear * 1.442695), uUnderwaterFactor);\n }\n\n // Beer-Lambert through both layers, leaving at least uFogLayer2.z of the surface: a floor on\n // what survives rather than a ceiling on the depth, so the curve keeps its shape above it.\n float air = cut ? 0.0 : 1.0 - max(exp(-hazeDepth(dist, pointY)), uFogLayer2.z);\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 = uFogMedium.w * dist;\n return mix(air, 1.0 - exp2(-wet * wet * 1.442695), uUnderwaterFactor);\n}\n\n/**\n * The sun's light the haze scatters toward the eye along a ray to a point at height pointY, dist\n * metres away in the direction toPoint: added to a surface after its fog, never mixed. A lobe of\n * cos^exponent about the light, by as much of the ray past the glow's own start as the haze hides \u2014\n * the haze's depth per metre carried over that stretch. None in a linear ramp, past the cutoff, or\n * under water, which keeps its own medium.\n */\nvec3 mediumGlow(float dist, float pointY, vec3 toPoint) {\n if (uFogRange.z == 1.0 || max(max(uFogGlow.r, uFogGlow.g), uFogGlow.b) <= 0.0) return vec3(0.0);\n if (uFogLayer2.w > 0.0 && dist > uFogLayer2.w) return vec3(0.0);\n float span = dist - min(uFogRange.x, dist);\n if (span <= 0.0) return vec3(0.0);\n float perMetre = hazeDepth(dist, pointY) / span;\n float hidden = 1.0 - exp(-perMetre * max(span - uFogGlowDir.w, 0.0));\n float lobe = pow(clamp(dot(toPoint, uFogGlowDir.xyz), 0.0, 1.0), uFogGlow.a);\n return uFogGlow.rgb * (lobe * hidden * (1.0 - uUnderwaterFactor));\n}\n\nuniform vec3 uCameraPos;\nuniform sampler2D uReflectionMap;\nuniform int uReflectionEnabled;\nuniform vec2 uReflectionTexelSize;\nuniform int uReflectionFilterTaps;\n\n/** 0 none, 1 sRGB, 2 ACES then sRGB, 3 the highlight shoulder then sRGB. See renderQuality.ts. */\nuniform int uOutputTransform;\n/** Scales the scene into the tone curve. 1 is the reference grade. See renderQuality.ts. */\nuniform float uOutputExposure;\n\n/*\n * ACES: the reference fit, not the cheap one.\n *\n * This was Narkowicz's single rational function, on the argument that the difference from\n * the reference curve is invisible next to the gain. That argument holds for a look and\n * fails for a *match*, which is what this option is for \u2014 aces exists so a world ported\n * from three.js reads the same, and three.js applies the full fit: into AP1 through\n * ACESInputMat, the RRT and ODT together, then back out through ACESOutputMat.\n *\n * The two curves are not a shade apart. Narkowicz is markedly brighter through the\n * midtones, which is exactly where an interior scene lives, and it is a large part of a\n * measured ~1.7x that survived correcting the colour space and the missing Lambert pi.\n * An approximation of a standard is the wrong thing to hold when the standard is the\n * specification.\n */\nvec3 rrtAndOdtFit(vec3 v) {\n vec3 a = v * (v + 0.0245786) - 0.000090537;\n vec3 b = v * (0.983729 * v + 0.4329510) + 0.238081;\n return a / b;\n}\n\nvec3 acesFilmic(vec3 x) {\n /*\n * Exposure first, because a tone curve is only defined relative to one.\n *\n * The reference RRT and ODT are graded for a dark cinema. Every renderer that ships ACES\n * scales its input to suit whatever it is actually shown on, and the factor each one\n * picks is a *look*, not a standard - so this engine takes no position and reads it from\n * the caller. Neutral by default, which is the reference grade unmodified. three.js's own\n * pick is 1 / 0.6, for a consumer matching one.\n */\n x *= uOutputExposure;\n // Columns, as GLSL reads them: sRGB -> XYZ -> D65_2_D60 -> AP1 -> RRT_SAT.\n const mat3 ACES_INPUT = mat3(\n 0.59719, 0.07600, 0.02840,\n 0.35458, 0.90834, 0.13383,\n 0.04823, 0.01566, 0.83777\n );\n // ODT_SAT -> XYZ -> D60_2_D65 -> sRGB.\n const mat3 ACES_OUTPUT = mat3(\n 1.60475, -0.10208, -0.00327,\n -0.53108, 1.10813, -0.07276,\n -0.07367, -0.00605, 1.07602\n );\n return clamp(ACES_OUTPUT * rrtAndOdtFit(ACES_INPUT * x), 0.0, 1.0);\n}\n\n\n/*\n * Linear to sRGB. The piecewise form rather than a plain pow: the linear toe near black\n * is where banding lives in an 8-bit buffer, and the cheap approximation puts it in the\n * wrong place.\n */\nvec3 linearToSrgb(vec3 c) {\n vec3 low = c * 12.92;\n vec3 high = 1.055 * pow(max(c, vec3(0.0)), vec3(1.0 / 2.4)) - 0.055;\n return mix(high, low, step(c, vec3(0.0031308)));\n}\n\n\nvec3 highlightShoulder(vec3 c) {\n float m = max(c.r, max(c.g, c.b));\n if (m <= 0.8) return c;\n float e = m - 0.8;\n return c * ((0.8 + 0.2 * e / (e + 0.2)) / m);\n}\n\nvec3 applyOutputTransform(vec3 c) {\n if (uOutputTransform == 0) return c;\n if (uOutputTransform == 2) c = acesFilmic(c);\n if (uOutputTransform == 3) c = highlightShoulder(c * uOutputExposure);\n return linearToSrgb(c);\n}\n\n\nconst vec2 REFLECTION_FILTER[9] = vec2[9](\n vec2( 0.00, 0.00), vec2(-0.62, -0.31), vec2( 0.54, 0.43),\n vec2(-0.34, 0.76), vec2( 0.78, -0.57), vec2(-0.88, 0.28),\n vec2( 0.22, -0.91), vec2( 0.91, 0.08), vec2(-0.18, 0.94)\n);\n// Preserve small moving silhouettes such as a character while still spreading\n// the surrounding taps enough to make the target read as rough water.\nconst float REFLECTION_FILTER_WEIGHT[9] = float[9](\n 0.28, 0.09, 0.09, 0.09, 0.09, 0.09, 0.09, 0.09, 0.09\n);\n\nout vec4 outColor;\n\nvoid main() {\n vec3 n = normalize(vNormal);\n vec3 view = normalize(uCameraPos - vWorldPos);\n\n // Fresnel: water is nearly a mirror at grazing angles and clear straight down.\n // abs keeps the underside physically continuous with the top instead of\n // turning the entire surface into an opaque mirror below the waterline.\n float facing = clamp(abs(dot(n, view)), 0.0, 1.0);\n float fresnel = 0.02 + 0.98 * pow(1.0 - facing, 5.0);\n\n vec3 atmosphereColor = mediumColor();\n\n vec3 col = mix(uDeepColor, uShallowColor, clamp(vCrest * 1.4 + 0.4, 0.0, 1.0));\n vec3 incident = uAmbient +\n uDirectionalColor * max(dot(n, uDirectionalDir), 0.0) * 0.6;\n\n // Specular glitter along the crests \u2014 the strongest single cue that a surface\n // is water rather than a coloured plane.\n vec3 halfVec = normalize(uDirectionalDir + view);\n float spec = pow(max(dot(n, halfVec), 0.0), 220.0);\n vec3 highlight = uDirectionalColor * spec * 1.6;\n\n /*\n * And the same from every lamp in range: a light over water lights it, and glitters\n * on it. Indoors this is the whole of the surface's lighting.\n *\n * The exponent is the sun's, so a crest picks a lamp out as tightly as it picks out\n * the sky \u2014 which is what makes a strip light read as a *streak* across a pool rather\n * than a broad sheen. Weighted by presence like every other consumer of these slots,\n * so a light entering the set fades in instead of appearing.\n */\n for (int i = 0; i < MAX_LIGHTS; i++) {\n if (i >= uLightCount) break;\n vec3 toLight = uLightPos[i] - vWorldPos;\n float lightDist = length(toLight);\n float falloff;\n float exponent = uLightCone[i].z;\n if (exponent > 0.0) {\n float reach = lightDist / max(uLightRadius[i], 1e-4);\n falloff = pow(clamp(1.0 - reach * reach, 0.0, 1.0), exponent);\n } else if (uLightFalloff == 1) {\n float window = clamp(1.0 - pow(lightDist / max(uLightRadius[i], 1e-4), 4.0), 0.0, 1.0);\n falloff = window * window / max(lightDist * lightDist, 0.01);\n } else {\n falloff = clamp(1.0 - lightDist / max(uLightRadius[i], 1e-4), 0.0, 1.0);\n }\n if (falloff <= 0.0) continue;\n vec3 lightDir = toLight / max(lightDist, 1e-4);\n float shaped = falloff * uLightWeight[i];\n incident += uLightColor[i] * max(dot(n, lightDir), 0.0) * shaped * 0.6;\n vec3 lampHalf = normalize(lightDir + view);\n highlight += uLightColor[i] * pow(max(dot(n, lampHalf), 0.0), 220.0) * shaped * 1.6;\n }\n\n col *= incident;\n col += highlight;\n\n // Foam where crests peak \u2014 and only when the wind is actually breaking them.\n float foam = smoothstep(0.55, 0.85, vCrest) * uFoamGain;\n col = mix(col, vec3(0.92, 0.96, 1.0), foam * 0.5);\n\n float dist = distance(vWorldPos, uCameraPos);\n float fog = mediumFog(dist, vWorldPos.y);\n vec3 glow = mediumGlow(dist, vWorldPos.y, normalize(vWorldPos - uCameraPos));\n col = mix(col, atmosphereColor, fog) + glow;\n /*\n * **Graded here, before the mirror is mixed in**, as every pass grades what it draws when\n * nothing after it will: the mirror's own passes graded what it holds, so mixing first would put\n * the curve on it twice. The air's colour, which stands in where the mirror shows nothing, is\n * graded to meet it, as the sky behind it is.\n */\n col = applyOutputTransform(col);\n vec3 gradedAir = applyOutputTransform(atmosphereColor);\n\n // Transparency is angle-dependent, like real water: looking down you see\n // straight through to the seabed, at grazing angles it turns reflective.\n // This is computed before reflection because straight-alpha compositing must\n // not attenuate reflected radiance a second time below.\n float airOpacity = mix(uNadirOpacity, 0.96, fresnel);\n float underwaterOpacity = mix(uNadirOpacity * 0.55, 0.88, fresnel);\n float opacity = mix(airOpacity, underwaterOpacity, uUnderwaterFactor);\n opacity = mix(opacity, 0.96, foam * 0.7);\n\n // Project the displaced surface into the mirrored camera. The Gerstner\n // displacement supplies broad distortion; the analytic normal adds the\n // smaller ripples that make reflected silhouettes move with each crest.\n vec3 reflected = gradedAir;\n float reflectionValid = 0.0;\n if (uReflectionEnabled != 0 && vReflectionClip.w > 0.0) {\n vec2 reflectionUv = vReflectionClip.xy / vReflectionClip.w * 0.5 + 0.5;\n reflectionUv += n.xz * mix(0.008, 0.022, fresnel);\n vec2 border = min(reflectionUv, 1.0 - reflectionUv);\n reflectionValid = smoothstep(0.0, 0.025, min(border.x, border.y));\n // A planar target is optically sharp, which is correct for a mirror but\n // wrong for a moving water surface. Broaden it over a Poisson footprint\n // that grows gently with viewing distance; normal displacement above\n // still makes every crest steer that softened image independently.\n float blurPixels = 1.35 + min(dist * 0.022, 3.5);\n vec3 sceneReflection = vec3(0.0);\n float filterWeight = 0.0;\n for (int i = 0; i < 9; i++) {\n if (i >= uReflectionFilterTaps) break;\n vec2 sampleUv = reflectionUv +\n REFLECTION_FILTER[i] * uReflectionTexelSize * blurPixels;\n float weight = REFLECTION_FILTER_WEIGHT[i];\n /*\n * `textureLod` rather than `texture`, by the rule `AGENTS.md` records on 2026-08-07:\n * this loop is reached through a branch on `vReflectionClip`, which is not uniform, and\n * an implicit derivative there is undefined. **WGSL does not merely leave it undefined,\n * it refuses to compile the module**: *\"'textureSample' must only be called from uniform\n * control flow\"*, with the note that reading `vReflectionClip` may be non-uniform.\n *\n * Bit-identical on WebGL2: `planarReflection.ts` allocates one level and filters\n * `LINEAR`, so there is no mip to select and level zero is the only thing `texture`\n * could have read.\n */\n sceneReflection +=\n textureLod(uReflectionMap, clamp(sampleUv, 0.0, 1.0), 0.0).rgb * weight;\n filterWeight += weight;\n }\n sceneReflection /= max(filterWeight, 0.001);\n reflected = mix(gradedAir, sceneReflection, reflectionValid);\n }\n // The atmosphere-colour fallback preserves the old sky reflection when a\n // quality profile disables the extra scene pass. Foam scatters rather than\n // mirroring, so it masks the reflection at breaking crests.\n float sceneWeight = float(uReflectionEnabled) * reflectionValid;\n float reflectionGain = mix(0.75, 0.94, sceneWeight);\n // Angle-honest by default, and a mirror where a body says so. See uMirror above.\n float reflectivity = mix(fresnel, 1.0, uMirror);\n float reflectedRadiance = min(\n reflectivity * reflectionGain * (1.0 - foam * 0.8),\n opacity\n );\n // With SRC_ALPHA blending, mixing by Fresnel directly would multiply the\n // reflection by opacity again. Divide it out here so the framebuffer gets\n // the intended reflected fraction while the remainder stays transmissive.\n float reflectionWeight = reflectedRadiance / max(opacity, 0.001);\n col = mix(col, reflected, reflectionWeight);\n\n /*\n * Dissolve the sheet at its own boundary.\n *\n * Fading on fog alone made the horizon a function of the daily palette: a\n * clear day never reached the threshold, so the grid's rim \u2014 corner and all \u2014\n * became the skyline. Fading on the grid coordinate cannot fail that way,\n * because alpha reaches zero at the last row of vertices by construction.\n * Chebyshev distance because the sheet is a rectangle, so the fade band\n * follows its four edges instead of cutting an ellipse out of it \u2014 and it is\n * measured per axis, or a channel's narrow sides would be one continuous fade\n * and it would have no visible edge at all.\n */\n vec2 rimFraction = abs(vGrid) / max(uGridHalf, vec2(0.001));\n float rimT = max(rimFraction.x, rimFraction.y);\n float rim = 1.0 - smoothstep(0.9, 1.0, rimT);\n // Fog still helps where it is thick, it just is no longer load-bearing.\n float edge = 1.0 - smoothstep(0.82, 1.0, fog);\n outColor = vec4(col, opacity * edge * rim * uVisibility);\n}\n";
|
|
@@ -275,7 +275,8 @@ void main() {
|
|
|
275
275
|
|
|
276
276
|
float dist = distance(vWorldPos, uCameraPos);
|
|
277
277
|
float fog = mediumFog(dist, vWorldPos.y);
|
|
278
|
-
|
|
278
|
+
vec3 glow = mediumGlow(dist, vWorldPos.y, normalize(vWorldPos - uCameraPos));
|
|
279
|
+
col = mix(col, atmosphereColor, fog) + glow;
|
|
279
280
|
/*
|
|
280
281
|
* **Graded here, before the mirror is mixed in**, as every pass grades what it draws when
|
|
281
282
|
* nothing after it will: the mirror's own passes graded what it holds, so mixing first would put
|
|
@@ -26,11 +26,19 @@ export interface AnisotropicModel {
|
|
|
26
26
|
readonly strength: number;
|
|
27
27
|
/** Radians, turning the stretch's direction from the tangent toward the bitangent. */
|
|
28
28
|
readonly rotation: number;
|
|
29
|
+
/**
|
|
30
|
+
* How far the highlight is lifted where it is bright: the distribution `D` becomes
|
|
31
|
+
* `D + min(3, D^1.5) · lift`, which is how a brushed finish that reads as polished along its
|
|
32
|
+
* streak is authored. Where a model map is bound its alpha scales it texel by texel, so a map
|
|
33
|
+
* storing a tenth of a region's lift takes `lift: 10`. 0, the default, is none.
|
|
34
|
+
*/
|
|
35
|
+
readonly lift: number;
|
|
29
36
|
}
|
|
30
37
|
/**
|
|
31
38
|
* Hair: three lobes along the strand, which is the mesh's tangent, running from root to tip
|
|
32
|
-
* (`hairLobes.ts`). A model map's
|
|
33
|
-
* its
|
|
39
|
+
* (`hairLobes.ts`). A model map's red is the strand's specular, twice it scaling the reflected lobe
|
|
40
|
+
* (0.5 is the lobe as drawn), its green turns each strand's tilt by up to half of it either way and
|
|
41
|
+
* its blue is occlusion; `directionOffset` names a layer of it holding each strand's direction.
|
|
34
42
|
*/
|
|
35
43
|
export interface HairModel {
|
|
36
44
|
readonly kind: 'hair';
|
|
@@ -44,6 +52,15 @@ export interface HairModel {
|
|
|
44
52
|
readonly scatter: number;
|
|
45
53
|
/** How strongly light from behind glows through: the transmitted lobe. */
|
|
46
54
|
readonly backlit: number;
|
|
55
|
+
/**
|
|
56
|
+
* Where the model map holds each strand's direction: this many layers past the layer the texel's
|
|
57
|
+
* other channels are read at, so a model map of one layer with its directions in the next takes
|
|
58
|
+
* 1. The direction is in the surface's tangent frame, red along the tangent, green along the
|
|
59
|
+
* bitangent and blue out of the surface along its normal, each −1 to 1 packed as a normal map
|
|
60
|
+
* packs them, and the strand runs along it rather than along the tangent: a card's strands can
|
|
61
|
+
* lean out of its plane. 0, the default, is none.
|
|
62
|
+
*/
|
|
63
|
+
readonly directionOffset: number;
|
|
47
64
|
}
|
|
48
65
|
/** Skin: light scattered beneath the surface, reddening its terminator and its thin parts. */
|
|
49
66
|
export interface SkinModel {
|
|
@@ -102,12 +119,14 @@ export declare const MODEL_PARAM_FLOATS = 8;
|
|
|
102
119
|
export declare function anisotropicModel(options?: {
|
|
103
120
|
strength?: number;
|
|
104
121
|
rotation?: number;
|
|
122
|
+
lift?: number;
|
|
105
123
|
}): AnisotropicModel;
|
|
106
124
|
/** Hair, along the mesh's tangent from root to tip. A tilt of 3°, within the 2.5° to 5° measured. */
|
|
107
125
|
export declare function hairModel(options?: {
|
|
108
126
|
shift?: number;
|
|
109
127
|
scatter?: number;
|
|
110
128
|
backlit?: number;
|
|
129
|
+
directionOffset?: number;
|
|
111
130
|
}): HairModel;
|
|
112
131
|
/**
|
|
113
132
|
* How far Burley's normalised diffusion carries light on average, in units of its own distance
|
|
@@ -154,8 +173,8 @@ export declare function lightmapModel(options?: {
|
|
|
154
173
|
* models that read it leave free; skin, hair and the eye shade their own highlights and never do.
|
|
155
174
|
*
|
|
156
175
|
* - standard (null): − · −, −, physical, −
|
|
157
|
-
* - anisotropic: strength, cos and sin of the rotation · −, −, physical, map
|
|
158
|
-
* - hair: shift, scatter, backlit,
|
|
176
|
+
* - anisotropic: strength, cos and sin of the rotation, lift · −, −, physical, map
|
|
177
|
+
* - hair: shift, scatter, backlit, direction offset · −, −, −, map
|
|
159
178
|
* - skin: the scatter colour, radius / `SKIN_MEAN_EXIT` · transmission, profile, −, map
|
|
160
179
|
* - eye: iris radius, iris depth, ior, cornea roughness · the axis, map
|
|
161
180
|
* - lightmap: the region's scale and bias · −, −, physical, map
|
|
@@ -33,17 +33,23 @@ function unit(model, name, v) {
|
|
|
33
33
|
export function anisotropicModel(options = {}) {
|
|
34
34
|
return Object.freeze({
|
|
35
35
|
kind: 'anisotropic',
|
|
36
|
+
lift: check('anisotropicModel', 'lift', options.lift ?? 0, 0, 100),
|
|
36
37
|
strength: check('anisotropicModel', 'strength', options.strength ?? 1, 0, 1),
|
|
37
38
|
rotation: check('anisotropicModel', 'rotation', options.rotation ?? 0, -2 * Math.PI, 2 * Math.PI),
|
|
38
39
|
});
|
|
39
40
|
}
|
|
40
41
|
/** Hair, along the mesh's tangent from root to tip. A tilt of 3°, within the 2.5° to 5° measured. */
|
|
41
42
|
export function hairModel(options = {}) {
|
|
43
|
+
const directionOffset = check('hairModel', 'directionOffset', options.directionOffset ?? 0, 0, 255);
|
|
44
|
+
if (!Number.isInteger(directionOffset)) {
|
|
45
|
+
throw new Error(`hairModel: directionOffset must be a whole number of layers, not ${directionOffset}`);
|
|
46
|
+
}
|
|
42
47
|
return Object.freeze({
|
|
43
48
|
kind: 'hair',
|
|
44
49
|
shift: check('hairModel', 'shift', options.shift ?? (3 * Math.PI) / 180, -0.5, 0.5),
|
|
45
50
|
scatter: check('hairModel', 'scatter', options.scatter ?? 0.7, 0, 1),
|
|
46
51
|
backlit: check('hairModel', 'backlit', options.backlit ?? 1, 0, 4),
|
|
52
|
+
directionOffset,
|
|
47
53
|
});
|
|
48
54
|
}
|
|
49
55
|
/**
|
|
@@ -113,8 +119,8 @@ export function lightmapModel(options = {}) {
|
|
|
113
119
|
* models that read it leave free; skin, hair and the eye shade their own highlights and never do.
|
|
114
120
|
*
|
|
115
121
|
* - standard (null): − · −, −, physical, −
|
|
116
|
-
* - anisotropic: strength, cos and sin of the rotation · −, −, physical, map
|
|
117
|
-
* - hair: shift, scatter, backlit,
|
|
122
|
+
* - anisotropic: strength, cos and sin of the rotation, lift · −, −, physical, map
|
|
123
|
+
* - hair: shift, scatter, backlit, direction offset · −, −, −, map
|
|
118
124
|
* - skin: the scatter colour, radius / `SKIN_MEAN_EXIT` · transmission, profile, −, map
|
|
119
125
|
* - eye: iris radius, iris depth, ior, cornea roughness · the axis, map
|
|
120
126
|
* - lightmap: the region's scale and bias · −, −, physical, map
|
|
@@ -132,12 +138,16 @@ export function packModel(model, mapped, out, physical = false) {
|
|
|
132
138
|
out[0] = model.strength;
|
|
133
139
|
out[1] = Math.cos(model.rotation);
|
|
134
140
|
out[2] = Math.sin(model.rotation);
|
|
141
|
+
/* The lift, which the map's alpha scales texel by texel; 0, and a missing field, is none. */
|
|
142
|
+
out[3] = model.lift ?? 0;
|
|
135
143
|
out[6] = highlight;
|
|
136
144
|
return;
|
|
137
145
|
case 'hair':
|
|
138
146
|
out[0] = model.shift;
|
|
139
147
|
out[1] = model.scatter;
|
|
140
148
|
out[2] = model.backlit;
|
|
149
|
+
/* A layer offset the shader adds to the texel's own; 0 is none, and so is a missing field. */
|
|
150
|
+
out[3] = model.directionOffset ?? 0;
|
|
141
151
|
return;
|
|
142
152
|
case 'skin':
|
|
143
153
|
out[0] = model.scatterColor[0];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@driftengine/core",
|
|
3
|
-
"version": "4.13.
|
|
3
|
+
"version": "4.13.5",
|
|
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.13.
|
|
33
|
-
"@driftengine/physics": "4.13.
|
|
32
|
+
"@driftengine/drft": "4.13.5",
|
|
33
|
+
"@driftengine/physics": "4.13.5",
|
|
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.13.
|
|
71
|
-
"@driftengine/network": "4.13.
|
|
72
|
-
"@driftengine/texture": "4.13.
|
|
70
|
+
"@driftengine/nav": "4.13.5",
|
|
71
|
+
"@driftengine/network": "4.13.5",
|
|
72
|
+
"@driftengine/texture": "4.13.5"
|
|
73
73
|
}
|
|
74
74
|
}
|
package/src/index.ts
CHANGED
|
@@ -583,7 +583,12 @@ export type {
|
|
|
583
583
|
export type { StaticDrawsHandle } from './render/staticDraws.ts';
|
|
584
584
|
/* A bone animation's clip as a caller hands it over, and the handle `createBoneAnimation` returns. */
|
|
585
585
|
export type { BoneAnimationClip, BoneAnimationHandle } from './render/boneAnimation.ts';
|
|
586
|
-
export type {
|
|
586
|
+
export type {
|
|
587
|
+
Atmosphere,
|
|
588
|
+
FogLayer,
|
|
589
|
+
FogSunGlow,
|
|
590
|
+
UnderwaterAtmosphere,
|
|
591
|
+
} from './render/atmosphere.ts';
|
|
587
592
|
/*
|
|
588
593
|
* Reading an image's texels back exactly, which no canvas and no `VideoFrame` will do.
|
|
589
594
|
*
|
|
@@ -607,7 +612,12 @@ export {
|
|
|
607
612
|
WEAK_GPU_MEDIUM_STEPS,
|
|
608
613
|
resolveGlobalMedium,
|
|
609
614
|
} from './render/globalMedium.ts';
|
|
610
|
-
export type {
|
|
615
|
+
export type {
|
|
616
|
+
GlobalMediumOptions,
|
|
617
|
+
GlobalMediumSettings,
|
|
618
|
+
ResolvedGlobalMedium,
|
|
619
|
+
} from './render/globalMedium.ts';
|
|
620
|
+
export type { LocalExposureOptions } from './render/shaders/localExposure.ts';
|
|
611
621
|
/*
|
|
612
622
|
* Equirectangular to cube faces, and the roughness a prefiltered level stands for.
|
|
613
623
|
*
|
|
@@ -27,6 +27,26 @@ export function anisotropicAlphas(roughness: number, strength: number, out: Floa
|
|
|
27
27
|
* The lobe at a half vector whose components along the direction, across it and along the normal
|
|
28
28
|
* are `th`, `bh` and `nh`. 1 at the mirror direction.
|
|
29
29
|
*/
|
|
30
|
+
/**
|
|
31
|
+
* How much the lift brightens the stretched lobe at a half vector: `(D + min(3, D^1.5) · k) / D`,
|
|
32
|
+
* with D the distribution there, `1 / (π αT αB d²)`, as the shader's `anisotropicLift` has it.
|
|
33
|
+
*/
|
|
34
|
+
export function anisotropicLiftFactor(
|
|
35
|
+
th: number,
|
|
36
|
+
bh: number,
|
|
37
|
+
nh: number,
|
|
38
|
+
alphaT: number,
|
|
39
|
+
alphaB: number,
|
|
40
|
+
lift: number,
|
|
41
|
+
): number {
|
|
42
|
+
if (lift <= 0) return 1;
|
|
43
|
+
const x = th / alphaT;
|
|
44
|
+
const y = bh / alphaB;
|
|
45
|
+
const d = x * x + y * y + nh * nh;
|
|
46
|
+
const distribution = 1 / Math.max(Math.PI * alphaT * alphaB * d * d, 1e-8);
|
|
47
|
+
return 1 + lift * Math.min(3 / distribution, Math.sqrt(distribution));
|
|
48
|
+
}
|
|
49
|
+
|
|
30
50
|
export function anisotropicLobe(
|
|
31
51
|
th: number,
|
|
32
52
|
bh: number,
|