@driftengine/core 4.12.0 → 4.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.json +66 -0
- package/README.md +18 -9
- package/dist/geometry/meshBuilder.d.ts +10 -0
- package/dist/geometry/meshBuilder.js +11 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.js +3 -2
- package/dist/math/color.d.ts +25 -0
- package/dist/math/color.js +37 -0
- package/dist/render/backend/webgl2/renderer.d.ts +17 -4
- package/dist/render/backend/webgl2/renderer.js +82 -47
- package/dist/render/backend/webgl2/skinScatter.d.ts +3 -1
- package/dist/render/backend/webgl2/skinScatter.js +5 -1
- package/dist/render/backend/webgpu/boltPass.d.ts +1 -1
- package/dist/render/backend/webgpu/causticsPass.d.ts +1 -1
- package/dist/render/backend/webgpu/filmPass.d.ts +1 -1
- package/dist/render/backend/webgpu/flockPass.d.ts +1 -1
- package/dist/render/backend/webgpu/giFieldPass.d.ts +3 -1
- package/dist/render/backend/webgpu/gpuDrivenPass.d.ts +10 -1
- package/dist/render/backend/webgpu/gpuDrivenPass.js +28 -11
- package/dist/render/backend/webgpu/panelPass.d.ts +1 -1
- package/dist/render/backend/webgpu/postPass.d.ts +2 -2
- package/dist/render/backend/webgpu/renderer.d.ts +19 -8
- package/dist/render/backend/webgpu/renderer.js +94 -38
- package/dist/render/backend/webgpu/scatterPass.d.ts +1 -1
- package/dist/render/backend/webgpu/skinScatterPass.d.ts +5 -1
- package/dist/render/backend/webgpu/skinScatterPass.js +9 -3
- package/dist/render/backend/webgpu/skyPass.d.ts +11 -1
- package/dist/render/backend/webgpu/waterPass.d.ts +1 -1
- package/dist/render/backend/webgpu/windStreakPass.d.ts +1 -1
- package/dist/render/causticsRenderer.d.ts +3 -1
- package/dist/render/causticsRenderer.js +5 -1
- package/dist/render/decalPass.d.ts +3 -1
- package/dist/render/decalPass.js +4 -1
- package/dist/render/flockRenderer.d.ts +1 -1
- package/dist/render/flockRenderer.js +4 -1
- package/dist/render/globalMediumPass.d.ts +3 -1
- package/dist/render/globalMediumPass.js +5 -1
- package/dist/render/gradeColor.d.ts +21 -0
- package/dist/render/gradeColor.js +64 -0
- package/dist/render/lightMatrix.d.ts +13 -0
- package/dist/render/lightMatrix.js +17 -0
- package/dist/render/passGrade.d.ts +13 -0
- package/dist/render/passGrade.js +47 -0
- package/dist/render/plumeRenderer.d.ts +1 -1
- package/dist/render/plumeRenderer.js +4 -1
- package/dist/render/renderQuality.d.ts +57 -18
- package/dist/render/renderQuality.js +3 -3
- package/dist/render/sceneTarget.d.ts +2 -0
- package/dist/render/sceneTarget.js +1 -0
- package/dist/render/sdfTextRenderer.d.ts +1 -1
- package/dist/render/sdfTextRenderer.js +1 -1
- package/dist/render/shaders/arcane.d.ts +1 -1
- package/dist/render/shaders/arcane.js +5 -1
- package/dist/render/shaders/bolt.d.ts +1 -1
- package/dist/render/shaders/bolt.js +4 -1
- package/dist/render/shaders/caustics.d.ts +1 -1
- package/dist/render/shaders/caustics.js +4 -1
- package/dist/render/shaders/decalProject.js +12 -2
- package/dist/render/shaders/film.d.ts +1 -1
- package/dist/render/shaders/film.js +22 -9
- package/dist/render/shaders/fire.d.ts +1 -1
- package/dist/render/shaders/fire.js +4 -1
- package/dist/render/shaders/flat/layerLooks.js +11 -19
- package/dist/render/shaders/flat/layered.js +33 -18
- package/dist/render/shaders/flock.d.ts +1 -1
- package/dist/render/shaders/flock.js +4 -1
- package/dist/render/shaders/generated/arcane.wgsl.d.ts +11 -1
- package/dist/render/shaders/generated/arcane.wgsl.js +11 -1
- package/dist/render/shaders/generated/bolt.wgsl.d.ts +12 -2
- package/dist/render/shaders/generated/bolt.wgsl.js +12 -2
- package/dist/render/shaders/generated/caustics.wgsl.d.ts +12 -2
- package/dist/render/shaders/generated/caustics.wgsl.js +12 -2
- package/dist/render/shaders/generated/decalProject.wgsl.d.ts +6 -1
- package/dist/render/shaders/generated/decalProject.wgsl.js +6 -1
- package/dist/render/shaders/generated/film.wgsl.d.ts +12 -2
- package/dist/render/shaders/generated/film.wgsl.js +12 -2
- package/dist/render/shaders/generated/fire.wgsl.d.ts +11 -1
- package/dist/render/shaders/generated/fire.wgsl.js +11 -1
- package/dist/render/shaders/generated/flat.wgsl.js +23 -23
- package/dist/render/shaders/generated/flock.wgsl.d.ts +12 -2
- package/dist/render/shaders/generated/flock.wgsl.js +12 -2
- package/dist/render/shaders/generated/globalMedium.wgsl.d.ts +12 -2
- package/dist/render/shaders/generated/globalMedium.wgsl.js +12 -2
- package/dist/render/shaders/generated/lightVolume.wgsl.d.ts +36 -16
- package/dist/render/shaders/generated/lightVolume.wgsl.js +39 -19
- package/dist/render/shaders/generated/panel.wgsl.d.ts +12 -2
- package/dist/render/shaders/generated/panel.wgsl.js +12 -2
- package/dist/render/shaders/generated/rush.wgsl.d.ts +31 -26
- package/dist/render/shaders/generated/rush.wgsl.js +31 -26
- package/dist/render/shaders/generated/scatter.wgsl.d.ts +12 -2
- package/dist/render/shaders/generated/scatter.wgsl.js +12 -2
- package/dist/render/shaders/generated/skinBlur.wgsl.d.ts +11 -1
- package/dist/render/shaders/generated/skinBlur.wgsl.js +11 -1
- package/dist/render/shaders/generated/sky.wgsl.d.ts +12 -2
- package/dist/render/shaders/generated/sky.wgsl.js +12 -2
- package/dist/render/shaders/generated/smoke.wgsl.d.ts +11 -1
- package/dist/render/shaders/generated/smoke.wgsl.js +11 -1
- package/dist/render/shaders/generated/text.wgsl.d.ts +11 -1
- package/dist/render/shaders/generated/text.wgsl.js +11 -1
- package/dist/render/shaders/generated/water.wgsl.d.ts +12 -2
- package/dist/render/shaders/generated/water.wgsl.js +12 -2
- package/dist/render/shaders/generated/windStreaks.wgsl.d.ts +12 -2
- package/dist/render/shaders/generated/windStreaks.wgsl.js +12 -2
- package/dist/render/shaders/globalMedium.js +14 -1
- package/dist/render/shaders/gpudriven/present.wgsl.d.ts +15 -6
- package/dist/render/shaders/gpudriven/present.wgsl.js +71 -8
- package/dist/render/shaders/lightVolume.js +4 -1
- package/dist/render/shaders/line.d.ts +1 -1
- package/dist/render/shaders/outputTransform.d.ts +8 -2
- package/dist/render/shaders/outputTransform.js +18 -11
- package/dist/render/shaders/panel.d.ts +1 -1
- package/dist/render/shaders/panel.js +5 -1
- package/dist/render/shaders/particle.d.ts +3 -3
- package/dist/render/shaders/rush.js +8 -0
- package/dist/render/shaders/scatter.d.ts +1 -1
- package/dist/render/shaders/scatter.js +4 -1
- package/dist/render/shaders/sdfText.d.ts +1 -1
- package/dist/render/shaders/skinBlur.js +5 -1
- package/dist/render/shaders/sky.d.ts +1 -1
- package/dist/render/shaders/sky.js +5 -1
- package/dist/render/shaders/smoke.d.ts +1 -1
- package/dist/render/shaders/smoke.js +4 -1
- package/dist/render/shaders/text.d.ts +1 -1
- package/dist/render/shaders/text.js +4 -1
- package/dist/render/shaders/water.d.ts +1 -1
- package/dist/render/shaders/water.js +12 -2
- package/dist/render/shaders/windStreaks.d.ts +1 -1
- package/dist/render/shaders/windStreaks.js +4 -1
- package/dist/render/surfaceLayerLooks.d.ts +16 -4
- package/dist/render/surfaceLayerLooks.js +33 -14
- package/dist/render/surfaceLayers.d.ts +2 -2
- package/dist/render/textRenderer.d.ts +3 -1
- package/dist/render/textRenderer.js +7 -1
- package/dist/render/vertexDefaults.d.ts +1 -1
- package/dist/render/vertexDefaults.js +1 -1
- package/dist/render/waterRenderer.d.ts +1 -1
- package/dist/render/waterRenderer.js +4 -1
- package/dist/render/windStreakRenderer.d.ts +3 -1
- package/dist/render/windStreakRenderer.js +5 -1
- package/package.json +6 -6
- package/src/geometry/meshBuilder.ts +21 -2
- package/src/index.ts +11 -2
- package/src/math/color.ts +39 -0
- package/src/render/backend/webgl2/renderer.ts +134 -44
- package/src/render/backend/webgl2/skinScatter.ts +5 -0
- package/src/render/backend/webgpu/giFieldPass.ts +3 -1
- package/src/render/backend/webgpu/gpuDrivenPass.ts +29 -13
- package/src/render/backend/webgpu/renderer.ts +104 -37
- package/src/render/backend/webgpu/skinScatterPass.ts +17 -1
- package/src/render/causticsRenderer.ts +5 -0
- package/src/render/decalPass.ts +3 -0
- package/src/render/flockRenderer.ts +5 -0
- package/src/render/globalMediumPass.ts +10 -1
- package/src/render/gradeColor.ts +90 -0
- package/src/render/lightMatrix.ts +32 -0
- package/src/render/passGrade.ts +58 -0
- package/src/render/plumeRenderer.ts +5 -0
- package/src/render/renderQuality.ts +60 -21
- package/src/render/sceneTarget.ts +3 -0
- package/src/render/sdfTextRenderer.ts +1 -1
- package/src/render/shaders/arcane.ts +5 -1
- package/src/render/shaders/bolt.ts +4 -1
- package/src/render/shaders/caustics.ts +4 -1
- package/src/render/shaders/decalProject.ts +12 -2
- package/src/render/shaders/film.ts +22 -9
- package/src/render/shaders/fire.ts +4 -1
- package/src/render/shaders/flat/layerLooks.ts +11 -19
- package/src/render/shaders/flat/layered.ts +33 -18
- package/src/render/shaders/flock.ts +4 -1
- package/src/render/shaders/generated/arcane.wgsl.ts +11 -1
- package/src/render/shaders/generated/bolt.wgsl.ts +12 -2
- package/src/render/shaders/generated/caustics.wgsl.ts +12 -2
- package/src/render/shaders/generated/decalProject.wgsl.ts +6 -1
- package/src/render/shaders/generated/film.wgsl.ts +12 -2
- package/src/render/shaders/generated/fire.wgsl.ts +11 -1
- package/src/render/shaders/generated/flat.wgsl.ts +23 -23
- package/src/render/shaders/generated/flock.wgsl.ts +12 -2
- package/src/render/shaders/generated/globalMedium.wgsl.ts +12 -2
- package/src/render/shaders/generated/lightVolume.wgsl.ts +39 -19
- package/src/render/shaders/generated/panel.wgsl.ts +12 -2
- package/src/render/shaders/generated/rush.wgsl.ts +31 -26
- package/src/render/shaders/generated/scatter.wgsl.ts +12 -2
- package/src/render/shaders/generated/skinBlur.wgsl.ts +11 -1
- package/src/render/shaders/generated/sky.wgsl.ts +12 -2
- package/src/render/shaders/generated/smoke.wgsl.ts +11 -1
- package/src/render/shaders/generated/text.wgsl.ts +11 -1
- package/src/render/shaders/generated/water.wgsl.ts +12 -2
- package/src/render/shaders/generated/windStreaks.wgsl.ts +12 -2
- package/src/render/shaders/globalMedium.ts +14 -1
- package/src/render/shaders/gpudriven/present.wgsl.ts +72 -8
- package/src/render/shaders/lightVolume.ts +4 -1
- package/src/render/shaders/outputTransform.ts +19 -11
- package/src/render/shaders/panel.ts +5 -1
- package/src/render/shaders/rush.ts +8 -0
- package/src/render/shaders/scatter.ts +4 -1
- package/src/render/shaders/skinBlur.ts +5 -1
- package/src/render/shaders/sky.ts +5 -1
- package/src/render/shaders/smoke.ts +4 -1
- package/src/render/shaders/text.ts +4 -1
- package/src/render/shaders/water.ts +12 -2
- package/src/render/shaders/windStreaks.ts +4 -1
- package/src/render/surfaceLayerLooks.ts +49 -18
- package/src/render/surfaceLayers.ts +2 -2
- package/src/render/textRenderer.ts +6 -0
- package/src/render/vertexDefaults.ts +1 -1
- package/src/render/waterRenderer.ts +5 -0
- package/src/render/windStreakRenderer.ts +5 -0
|
@@ -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\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 outColor = vec4(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/** 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";
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { FOG_GLSL } from './fog.js';
|
|
2
|
+
import { OUTPUT_TRANSFORM_GLSL } from './outputTransform.js';
|
|
2
3
|
/**
|
|
3
4
|
* Smoke: a column that rises, expands and dissipates. Shares the plume
|
|
4
5
|
* billboard vertex shader with fire — only the fragment behaviour differs.
|
|
@@ -21,6 +22,7 @@ uniform vec4 uClipPlane;
|
|
|
21
22
|
uniform int uClipEnabled;
|
|
22
23
|
|
|
23
24
|
out vec4 outColor;
|
|
25
|
+
${OUTPUT_TRANSFORM_GLSL}
|
|
24
26
|
|
|
25
27
|
float hash21(vec2 p) {
|
|
26
28
|
return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123);
|
|
@@ -83,6 +85,7 @@ void main() {
|
|
|
83
85
|
float fog = mediumFog(vDistance, vWorldPos.y);
|
|
84
86
|
col = mix(col, atmosphereColor, fog);
|
|
85
87
|
float alpha = clamp(density * 0.95, 0.0, 1.0) * emerge * dissipate * 0.13;
|
|
86
|
-
|
|
88
|
+
/* Graded where nothing after this will grade it, as every pass is. The alpha is coverage. */
|
|
89
|
+
outColor = vec4(applyOutputTransform(col), alpha * (1.0 - fog));
|
|
87
90
|
}
|
|
88
91
|
`;
|
|
@@ -14,4 +14,4 @@
|
|
|
14
14
|
* letter shears instead of turning.
|
|
15
15
|
*/
|
|
16
16
|
export declare const TEXT_VERT = "#version 300 es\nprecision highp float;\n\nlayout(location = 0) in vec3 aCorner;\nlayout(location = 1) in vec3 aNormal;\n/** Cell position in pixels, relative to the string's origin. */\nlayout(location = 2) in vec2 aCell;\n/** Which character this cell belongs to, for staggered animation. */\nlayout(location = 3) in float aCharIndex;\n\nuniform vec2 uViewport;\n/** Where the string's baseline-left sits, in pixels from the top left. */\nuniform vec2 uOrigin;\nuniform float uCellSize;\n/** How far each cell is seen from, head-on; larger flattens the perspective. */\nuniform float uDepth;\n\n/** 0 before a character has arrived, 1 once it has settled. */\nuniform float uReveal;\n/** How many characters the stagger spans \u2014 the whole line, not this piece. */\nuniform float uCharCount;\n/**\n * Where this piece starts within that line.\n *\n * A line drawn in several pieces \u2014 words around a keycap \u2014 must stagger as one\n * sentence. Without this each piece restarts at zero, so the second word begins\n * arriving before the first has finished and they sit at different heights\n * through the whole animation.\n */\nuniform float uCharOffset;\n/** Radians of entry rotation, decaying to zero as a character settles. */\nuniform float uSpin;\n/** Extra scale at the moment of arrival \u2014 the overshoot. */\nuniform float uPunch;\n/** Vertical bob amplitude in pixels, and its phase. */\nuniform float uBob;\nuniform float uTime;\n/**\n * Clip space, as the backend drawing this defines it. Identity on WebGL2.\n *\n * This draw builds its own clip position from pixels and never multiplies by a camera, so\n * the correction every other draw carries in its view-projection cannot reach it \u2014 the same\n * gap that left the sky unprojecting through the wrong space. Two things are wrong without\n * it on WebGPU, and only one of them is visible: the generated vertex shader negates Y, so\n * the message renders upside down; and z here is a narrow slice about zero, which is inside\n * OpenGL's [-1, 1] and *outside* WebGPU's [0, 1], so half of every glyph is clipped away.\n */\nuniform mat4 uClipCorrection;\n\nout vec3 vNormal;\nout float vDepth;\nout float vChar;\n\n/**\n * Spring settle: overshoots once and comes back.\n *\n * A character that eases into place reads as an element appearing; one that\n * overshoots and settles reads as an object arriving. That difference is most\n * of what makes text feel like part of a game rather than part of a page.\n */\nfloat settle(float t) {\n if (t <= 0.0) return 0.0;\n if (t >= 1.0) return 1.0;\n float d = 1.0 - t;\n return 1.0 - d * d * cos(t * 12.0) ;\n}\n\nmat2 rot(float a) {\n float s = sin(a);\n float c = cos(a);\n return mat2(c, -s, s, c);\n}\n\nvoid main() {\n // Stagger: each character starts a fraction of the reveal later than the\n // one before it, so the message assembles left to right.\n float span = max(uCharCount, 1.0);\n float start = ((aCharIndex + uCharOffset) / span) * 0.55;\n float local = clamp((uReveal - start) / max(0.45, 1.0 - start), 0.0, 1.0);\n float eased = settle(local);\n\n // Arrives spinning about its own horizontal axis and dropping into place.\n float spin = uSpin * (1.0 - eased);\n float rise = (1.0 - eased) * uCellSize * 6.0;\n float punch = 1.0 + uPunch * (1.0 - abs(eased * 2.0 - 1.0));\n\n vec3 local3 = aCorner * uCellSize * 0.5 * punch;\n local3.yz = rot(spin) * local3.yz;\n\n vec2 cell = aCell * uCellSize;\n float bob = sin(uTime * 3.1 + aCharIndex * 0.7) * uBob;\n\n // The cell's centre in pixels, y down from the top-left corner of the viewport.\n vec2 centre = uOrigin + vec2(cell.x, -cell.y) + vec2(0.0, rise - bob);\n\n // A hand-built perspective divide about the cell's own centre, so every cube\n // is seen head-on: its front face at depth zero is exactly its cell, and at\n // rest its sides are behind it wherever on the screen it is drawn.\n //\n // It divided about the viewport's top-left corner until 2026-09-30, which\n // shifted a front face by its distance from that corner times z / uDepth and\n // showed the side it uncovered. Near the corner that was nothing; across the\n // screen it widened every stroke by up to a pixel either side and closed the\n // one-cell gaps a 5x7 face reads by. What this gives up is the extruded look\n // text far from the corner had at rest; what would make it wrong is a caller\n // wanting that look, which is then one offset for every cell, not a position.\n float invDepth = uDepth / max(0.001, uDepth - local3.z);\n vec2 screen = centre + local3.xy * invDepth;\n vec2 ndc = (screen / uViewport) * 2.0 - 1.0;\n // Depth from the cube's own extrusion, mapped into a narrow slice of the\n // range so a message sorts against itself without ever reaching the scene.\n float depth = clamp(-local3.z / max(1.0, uCellSize * 2.0), -1.0, 1.0) * 0.4;\n gl_Position = uClipCorrection * vec4(ndc.x, -ndc.y, depth, 1.0);\n\n vec3 n = aNormal;\n n.yz = rot(spin) * n.yz;\n vNormal = n;\n vDepth = local3.z;\n vChar = eased;\n}";
|
|
17
|
-
export declare const TEXT_FRAG = "#version 300 es\nprecision highp float;\n\nin vec3 vNormal;\nin float vDepth;\nin float vChar;\n\nuniform vec3 uColor;\n/** Self-illumination on top of the shaded colour. */\nuniform float uGlow;\nuniform float uAlpha;\n\nout vec4 fragColor;\n\nvoid main() {\n /*\n * A fixed key light rather than the world's. Text has to stay legible at\n * midnight and at noon, and lighting it with the scene would make a\n * notification unreadable on exactly the night runs where it matters most.\n * The light only exists to give the cubes an edge.\n */\n vec3 key = normalize(vec3(-0.35, 0.7, 0.6));\n float lambert = 0.55 + 0.45 * max(dot(normalize(vNormal), key), 0.0);\n\n vec3 lit = uColor * lambert;\n // The glow rides the arrival: brightest as a character lands, settling after.\n vec3 emissive = uColor * uGlow * (0.45 + 0.55 * vChar);\n fragColor = vec4(lit + emissive, uAlpha);\n}";
|
|
17
|
+
export declare const TEXT_FRAG = "#version 300 es\nprecision highp float;\n\nin vec3 vNormal;\nin float vDepth;\nin float vChar;\n\nuniform vec3 uColor;\n/** Self-illumination on top of the shaded colour. */\nuniform float uGlow;\nuniform float uAlpha;\n\nout vec4 fragColor;\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 /*\n * A fixed key light rather than the world's. Text has to stay legible at\n * midnight and at noon, and lighting it with the scene would make a\n * notification unreadable on exactly the night runs where it matters most.\n * The light only exists to give the cubes an edge.\n */\n vec3 key = normalize(vec3(-0.35, 0.7, 0.6));\n float lambert = 0.55 + 0.45 * max(dot(normalize(vNormal), key), 0.0);\n\n vec3 lit = uColor * lambert;\n // The glow rides the arrival: brightest as a character lands, settling after.\n vec3 emissive = uColor * uGlow * (0.45 + 0.55 * vChar);\n /* Graded where nothing after this will grade it, as every pass is. The alpha is coverage. */\n fragColor = vec4(applyOutputTransform(lit + emissive), uAlpha);\n}";
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { OUTPUT_TRANSFORM_GLSL } from './outputTransform.js';
|
|
1
2
|
/**
|
|
2
3
|
* Screen-space 3D text.
|
|
3
4
|
*
|
|
@@ -146,6 +147,7 @@ uniform float uGlow;
|
|
|
146
147
|
uniform float uAlpha;
|
|
147
148
|
|
|
148
149
|
out vec4 fragColor;
|
|
150
|
+
${OUTPUT_TRANSFORM_GLSL}
|
|
149
151
|
|
|
150
152
|
void main() {
|
|
151
153
|
/*
|
|
@@ -160,5 +162,6 @@ void main() {
|
|
|
160
162
|
vec3 lit = uColor * lambert;
|
|
161
163
|
// The glow rides the arrival: brightest as a character lands, settling after.
|
|
162
164
|
vec3 emissive = uColor * uGlow * (0.45 + 0.55 * vChar);
|
|
163
|
-
|
|
165
|
+
/* Graded where nothing after this will grade it, as every pass is. The alpha is coverage. */
|
|
166
|
+
fragColor = vec4(applyOutputTransform(lit + emissive), uAlpha);
|
|
164
167
|
}`;
|
|
@@ -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\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 // 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 = atmosphereColor;\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(atmosphereColor, 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/** 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";
|
|
@@ -2,6 +2,7 @@ import { MAX_POINT_LIGHTS } from '../lightBudget.js';
|
|
|
2
2
|
import { MAX_WATER_REFLECTION_FILTER_TAPS } from '../renderQuality.js';
|
|
3
3
|
import { GERSTNER_GLSL } from './gerstner.js';
|
|
4
4
|
import { FOG_GLSL } from './fog.js';
|
|
5
|
+
import { OUTPUT_TRANSFORM_GLSL } from './outputTransform.js';
|
|
5
6
|
/**
|
|
6
7
|
* The sky-ocean, as an actual wave simulation rather than a tinted plane.
|
|
7
8
|
*
|
|
@@ -196,6 +197,7 @@ uniform sampler2D uReflectionMap;
|
|
|
196
197
|
uniform int uReflectionEnabled;
|
|
197
198
|
uniform vec2 uReflectionTexelSize;
|
|
198
199
|
uniform int uReflectionFilterTaps;
|
|
200
|
+
${OUTPUT_TRANSFORM_GLSL}
|
|
199
201
|
|
|
200
202
|
const vec2 REFLECTION_FILTER[${MAX_WATER_REFLECTION_FILTER_TAPS}] = vec2[${MAX_WATER_REFLECTION_FILTER_TAPS}](
|
|
201
203
|
vec2( 0.00, 0.00), vec2(-0.62, -0.31), vec2( 0.54, 0.43),
|
|
@@ -274,6 +276,14 @@ void main() {
|
|
|
274
276
|
float dist = distance(vWorldPos, uCameraPos);
|
|
275
277
|
float fog = mediumFog(dist, vWorldPos.y);
|
|
276
278
|
col = mix(col, atmosphereColor, fog);
|
|
279
|
+
/*
|
|
280
|
+
* **Graded here, before the mirror is mixed in**, as every pass grades what it draws when
|
|
281
|
+
* nothing after it will: the mirror's own passes graded what it holds, so mixing first would put
|
|
282
|
+
* the curve on it twice. The air's colour, which stands in where the mirror shows nothing, is
|
|
283
|
+
* graded to meet it, as the sky behind it is.
|
|
284
|
+
*/
|
|
285
|
+
col = applyOutputTransform(col);
|
|
286
|
+
vec3 gradedAir = applyOutputTransform(atmosphereColor);
|
|
277
287
|
|
|
278
288
|
// Transparency is angle-dependent, like real water: looking down you see
|
|
279
289
|
// straight through to the seabed, at grazing angles it turns reflective.
|
|
@@ -287,7 +297,7 @@ void main() {
|
|
|
287
297
|
// Project the displaced surface into the mirrored camera. The Gerstner
|
|
288
298
|
// displacement supplies broad distortion; the analytic normal adds the
|
|
289
299
|
// smaller ripples that make reflected silhouettes move with each crest.
|
|
290
|
-
vec3 reflected =
|
|
300
|
+
vec3 reflected = gradedAir;
|
|
291
301
|
float reflectionValid = 0.0;
|
|
292
302
|
if (uReflectionEnabled != 0 && vReflectionClip.w > 0.0) {
|
|
293
303
|
vec2 reflectionUv = vReflectionClip.xy / vReflectionClip.w * 0.5 + 0.5;
|
|
@@ -322,7 +332,7 @@ void main() {
|
|
|
322
332
|
filterWeight += weight;
|
|
323
333
|
}
|
|
324
334
|
sceneReflection /= max(filterWeight, 0.001);
|
|
325
|
-
reflected = mix(
|
|
335
|
+
reflected = mix(gradedAir, sceneReflection, reflectionValid);
|
|
326
336
|
}
|
|
327
337
|
// The atmosphere-colour fallback preserves the old sky reflection when a
|
|
328
338
|
// quality profile disables the extra scene pass. Foam scatters rather than
|
|
@@ -11,4 +11,4 @@
|
|
|
11
11
|
* bookkeeping that could disagree with the wind everything else is using.
|
|
12
12
|
*/
|
|
13
13
|
export declare const WIND_STREAK_VERT = "#version 300 es\nlayout(location = 0) in vec2 aCorner;\nlayout(location = 1) in float aIndex;\n\nuniform mat4 uViewProj;\nuniform vec3 uCameraPos;\n/*\n * No uCameraRight. It was declared here and read nowhere, which GLSL answers by optimising the\n * location away \u2014 so the WebGL2 path never bound it and nothing was wrong. A uniform block is\n * laid out whole, though, so the generated WGSL carried sixteen bytes of it at offset 80 and\n * pushed everything after it along, leaving a field in the layout with no writer. That is the\n * shape AGENTS.md 2026-08-13 is about, arriving as dead weight rather than as a bug.\n */\nuniform vec2 uWind;\nuniform vec2 uDrift;\nuniform float uSpeed;\nuniform float uStrength;\nuniform float uCount;\nuniform float uCellSize;\nuniform float uTime;\n\nout float vFade;\nout float vAlong;\n\nvoid main() {\n float id = aIndex;\n float rx = fract(sin(id * 12.9898) * 43758.5453);\n float ry = fract(sin(id * 39.3467) * 24634.6345);\n float rz = fract(sin(id * 78.2330) * 12934.1234);\n\n /*\n * Scatter through a box that follows the camera, wrapped so a particle\n * leaving one side re-enters the other. That wrap is what makes a finite\n * count look like endless weather, and why nothing ever needs respawning.\n */\n /*\n * Biased low. You do not see air \u2014 you see what the wind has picked up, and\n * it picks things up from surfaces. Spreading debris evenly through a volume\n * puts streaks at head height in open sky, which is the single thing that\n * makes this read as an effect rather than as weather.\n */\n float lowBias = ry * ry;\n vec3 cell = vec3(rx * uCellSize, lowBias * uCellSize * 0.42, rz * uCellSize);\n vec3 base = vec3(\n uCameraPos.x + cell.x - uCellSize * 0.5 - uDrift.x,\n uCameraPos.y + cell.y - uCellSize * 0.3,\n uCameraPos.z + cell.z - uCellSize * 0.5 - uDrift.y\n );\n vec3 rel = base - uCameraPos;\n rel.x = mod(rel.x + uCellSize * 0.5, uCellSize) - uCellSize * 0.5;\n rel.z = mod(rel.z + uCellSize * 0.5, uCellSize) - uCellSize * 0.5;\n vec3 centre = uCameraPos + rel;\n\n // Streaks lie along the wind and lengthen with it: a smear is how speed\n // reads when the thing moving is too small to resolve.\n vec2 alongXZ = uSpeed > 0.001 ? uWind / uSpeed : vec2(1.0, 0.0);\n vec3 along = vec3(alongXZ.x, 0.0, alongXZ.y);\n vec3 across = normalize(cross(along, vec3(0.0, 1.0, 0.0)));\n\n /*\n * Turbulent, not ruled. A dead-straight line is the giveaway that something\n * was drawn rather than blown, so each streak curls on its own slow phase and\n * varies in length \u2014 a gust is made of eddies, and eddies are what the eye is\n * actually reading when it decides air is moving.\n */\n float curl = sin(uTime * 1.7 + id * 2.3) * 0.35 + sin(uTime * 0.9 + id) * 0.2;\n float length = (0.18 + uSpeed * 0.09) * (0.5 + rz);\n vec3 world = centre\n + along * (aCorner.y * length)\n + across * (aCorner.x * 0.012 + aCorner.y * curl * length * 0.35);\n\n // Fade with distance from the camera so the lattice edge is never a visible\n // boundary, and with strength so a calm day shows nothing at all.\n float dist = distance(centre, uCameraPos);\n /*\n * Three fades, all of which have to hold for a streak to be visible at all:\n * distance, so the lattice edge is never a boundary; a per-streak weight, so\n * they are not a uniform field; and a near fade, because debris a metre from\n * the eye is a smear across the screen rather than a cue.\n */\n /*\n * Fades spread across the whole volume rather than clipped into a band.\n *\n * Narrow near and far fades carve a spherical shell out of the lattice, and a\n * shell centred on the camera is seen as a curtain hanging in front of the\n * player \u2014 every particle at roughly the same distance, moving together. The\n * cure is depth: a long ramp at both ends, so density falls away gradually\n * and no distance is privileged.\n */\n float far = 1.0 - smoothstep(uCellSize * 0.12, uCellSize * 0.5, dist);\n float near = smoothstep(2.0, 16.0, dist);\n // Most particles are faint; a few carry the effect. A uniform field is a veil.\n float weight = rx * rx * rx;\n vFade = uStrength * far * near * weight;\n vAlong = aCorner.y * 0.5 + 0.5;\n\n gl_Position = uViewProj * vec4(world, 1.0);\n}\n";
|
|
14
|
-
export declare const WIND_STREAK_FRAG = "#version 300 es\nprecision highp float;\n\nin float vFade;\nin float vAlong;\nuniform vec3 uTint;\nout vec4 outColor;\n\nvoid main() {\n // Tapered at both ends, so a streak reads as motion rather than as a stick.\n // Squared taper: a streak should be a smear that thins to nothing, not a\n // capsule with visible ends.\n float taper = sin(vAlong * 3.14159);\n float alpha = vFade * taper * taper * 0.13;\n if (alpha <= 0.002) discard;\n outColor = vec4(uTint, alpha);\n}\n";
|
|
14
|
+
export declare const WIND_STREAK_FRAG = "#version 300 es\nprecision highp float;\n\nin float vFade;\nin float vAlong;\nuniform vec3 uTint;\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 // Tapered at both ends, so a streak reads as motion rather than as a stick.\n // Squared taper: a streak should be a smear that thins to nothing, not a\n // capsule with visible ends.\n float taper = sin(vAlong * 3.14159);\n float alpha = vFade * taper * taper * 0.13;\n if (alpha <= 0.002) discard;\n /* Graded where nothing after this will grade it, as every pass is. The alpha is coverage. */\n outColor = vec4(applyOutputTransform(uTint), alpha);\n}\n";
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { OUTPUT_TRANSFORM_GLSL } from './outputTransform.js';
|
|
1
2
|
/**
|
|
2
3
|
* Visible wind: streaks of dust and debris carried past the camera.
|
|
3
4
|
*
|
|
@@ -116,6 +117,7 @@ in float vFade;
|
|
|
116
117
|
in float vAlong;
|
|
117
118
|
uniform vec3 uTint;
|
|
118
119
|
out vec4 outColor;
|
|
120
|
+
${OUTPUT_TRANSFORM_GLSL}
|
|
119
121
|
|
|
120
122
|
void main() {
|
|
121
123
|
// Tapered at both ends, so a streak reads as motion rather than as a stick.
|
|
@@ -124,6 +126,7 @@ void main() {
|
|
|
124
126
|
float taper = sin(vAlong * 3.14159);
|
|
125
127
|
float alpha = vFade * taper * taper * 0.13;
|
|
126
128
|
if (alpha <= 0.002) discard;
|
|
127
|
-
|
|
129
|
+
/* Graded where nothing after this will grade it, as every pass is. The alpha is coverage. */
|
|
130
|
+
outColor = vec4(applyOutputTransform(uTint), alpha);
|
|
128
131
|
}
|
|
129
132
|
`;
|
|
@@ -40,7 +40,7 @@ export interface SurfaceLayerLook {
|
|
|
40
40
|
}
|
|
41
41
|
/** What an occlusion read at the mesh's own coordinates darkens, and how far. */
|
|
42
42
|
export interface SurfaceMeshOcclusion {
|
|
43
|
-
/** 0 to 1:
|
|
43
|
+
/** 0 to 1: what `into` names times `mix(1, occlusion, strength)`. Absent: 1. */
|
|
44
44
|
readonly strength?: number;
|
|
45
45
|
/**
|
|
46
46
|
* `'color'`, the default, darkens the blended colour, and so every light on it, the sun's too.
|
|
@@ -48,6 +48,18 @@ export interface SurfaceMeshOcclusion {
|
|
|
48
48
|
* does, leaving the sun, the lamps and the reflections as they were.
|
|
49
49
|
*/
|
|
50
50
|
readonly into?: 'color' | 'ambient';
|
|
51
|
+
/**
|
|
52
|
+
* The colour's own strength, from 0 up and past 1: the blended colour times
|
|
53
|
+
* `max(0, 1 − colorStrength × (1 − occlusion))`, which up to 1 is the `mix` above and past it
|
|
54
|
+
* darkens faster than the occlusion does. Absent: `strength` where `into` is the colour, and 0
|
|
55
|
+
* where it is the ambient light — so named beside `into: 'ambient'` it darkens both, each at its
|
|
56
|
+
* own strength, as a material taking its ambient occlusion and a darker colour from one map does.
|
|
57
|
+
*
|
|
58
|
+
* **What it gives up**: past 1 the colour is black wherever `1 − occlusion` reaches
|
|
59
|
+
* `1 / colorStrength`, and black under every light, the sun's too, which is what such a material
|
|
60
|
+
* asks for and what an ambient occlusion alone would never do.
|
|
61
|
+
*/
|
|
62
|
+
readonly colorStrength?: number;
|
|
51
63
|
/** The range the occlusion's red is spread over, from its 0 to its 1. Absent: `[0, 1]`. */
|
|
52
64
|
readonly range?: readonly [number, number];
|
|
53
65
|
}
|
|
@@ -65,15 +77,15 @@ export declare function asksLayerLooks<Texture>(layers: SurfaceLayers<Texture> |
|
|
|
65
77
|
* `LAYER_LOOKS` on, so it must draw exactly as it did before.
|
|
66
78
|
*
|
|
67
79
|
* - **0 to 7**: the array layer each layer reads, base first (layer `i` where not given); where the
|
|
68
|
-
* maps beyond the layers start (the layer count where not given); the mesh occlusion's strength
|
|
69
|
-
*
|
|
80
|
+
* maps beyond the layers start (the layer count where not given); the mesh occlusion's strength
|
|
81
|
+
* on the colour, from 0 up, and on the ambient light, 0 to 1.
|
|
70
82
|
* - **8 to 47**: two vectors a layer, base first: its tint and its normal strength, then its
|
|
71
83
|
* roughness range and its metalness range.
|
|
72
84
|
* - **48 to 59**: each layer's specular (−1 for the surface's own), each layer's repeat down (its
|
|
73
85
|
* repeat across where it gave one number), and the range the occlusion is spread over.
|
|
74
86
|
*
|
|
75
87
|
* An array layer that is not a whole number at or above 0 reads layer `i`; a start that is not one
|
|
76
|
-
* is the count; a strength is held to 0..1; a tint channel, a normal strength or a specular that is
|
|
88
|
+
* is the count; a strength is held to 0..1, but for the colour's own, which is held at or above 0; a tint channel, a normal strength or a specular that is
|
|
77
89
|
* not a number at or above 0 is the identity; a repeat down that is not a positive number is the
|
|
78
90
|
* repeat across; a range end that is not a number is the identity's end.
|
|
79
91
|
*/
|
|
@@ -15,7 +15,8 @@ export function asksLayerLooks(layers) {
|
|
|
15
15
|
layers.extrasAt !== undefined ||
|
|
16
16
|
layers.looks !== undefined ||
|
|
17
17
|
layers.repeats.some((repeat) => typeof repeat !== 'number') ||
|
|
18
|
-
|
|
18
|
+
colorOcclusion(layers.meshOcclusion) > 0 ||
|
|
19
|
+
ambientOcclusion(layers.meshOcclusion) > 0);
|
|
19
20
|
}
|
|
20
21
|
/**
|
|
21
22
|
* `layers` into `out`, fifteen vectors, the identity wherever a material says nothing — which is
|
|
@@ -23,15 +24,15 @@ export function asksLayerLooks(layers) {
|
|
|
23
24
|
* `LAYER_LOOKS` on, so it must draw exactly as it did before.
|
|
24
25
|
*
|
|
25
26
|
* - **0 to 7**: the array layer each layer reads, base first (layer `i` where not given); where the
|
|
26
|
-
* maps beyond the layers start (the layer count where not given); the mesh occlusion's strength
|
|
27
|
-
*
|
|
27
|
+
* maps beyond the layers start (the layer count where not given); the mesh occlusion's strength
|
|
28
|
+
* on the colour, from 0 up, and on the ambient light, 0 to 1.
|
|
28
29
|
* - **8 to 47**: two vectors a layer, base first: its tint and its normal strength, then its
|
|
29
30
|
* roughness range and its metalness range.
|
|
30
31
|
* - **48 to 59**: each layer's specular (−1 for the surface's own), each layer's repeat down (its
|
|
31
32
|
* repeat across where it gave one number), and the range the occlusion is spread over.
|
|
32
33
|
*
|
|
33
34
|
* An array layer that is not a whole number at or above 0 reads layer `i`; a start that is not one
|
|
34
|
-
* is the count; a strength is held to 0..1; a tint channel, a normal strength or a specular that is
|
|
35
|
+
* is the count; a strength is held to 0..1, but for the colour's own, which is held at or above 0; a tint channel, a normal strength or a specular that is
|
|
35
36
|
* not a number at or above 0 is the identity; a repeat down that is not a positive number is the
|
|
36
37
|
* repeat across; a range end that is not a number is the identity's end.
|
|
37
38
|
*/
|
|
@@ -41,9 +42,9 @@ export function packLayerLooks(layers, out) {
|
|
|
41
42
|
for (let i = 0; i < MAX_SURFACE_LAYERS; i++)
|
|
42
43
|
out[i] = arrayLayer(picks?.[i], i);
|
|
43
44
|
out[5] = arrayLayer(layers?.extrasAt, count);
|
|
44
|
-
const occlusion =
|
|
45
|
-
out[6] = occlusion
|
|
46
|
-
out[7] = occlusion
|
|
45
|
+
const occlusion = layers?.meshOcclusion;
|
|
46
|
+
out[6] = colorOcclusion(occlusion);
|
|
47
|
+
out[7] = ambientOcclusion(occlusion);
|
|
47
48
|
for (let i = 0; i < MAX_SURFACE_LAYERS; i++) {
|
|
48
49
|
const look = layers?.looks?.[i] ?? null;
|
|
49
50
|
const at = 8 + i * 8;
|
|
@@ -60,14 +61,32 @@ export function packLayerLooks(layers, out) {
|
|
|
60
61
|
const acrossIt = positiveOr(across(repeat), 1);
|
|
61
62
|
out[53 + i] = typeof repeat === 'object' ? positiveOr(repeat[1], acrossIt) : acrossIt;
|
|
62
63
|
}
|
|
63
|
-
|
|
64
|
-
out[
|
|
64
|
+
const range = typeof occlusion === 'object' ? occlusion.range : undefined;
|
|
65
|
+
out[58] = finiteOr(range?.[0], 0);
|
|
66
|
+
out[59] = finiteOr(range?.[1], 1);
|
|
65
67
|
}
|
|
66
|
-
/** The
|
|
67
|
-
function
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
68
|
+
/** The strength `meshOcclusion` names, held to 0..1: a number's own, an object's or 1, and 0 for none. */
|
|
69
|
+
function occlusionStrength(occlusion) {
|
|
70
|
+
const strength = typeof occlusion === 'number'
|
|
71
|
+
? occlusion
|
|
72
|
+
: occlusion === undefined
|
|
73
|
+
? 0
|
|
74
|
+
: (occlusion.strength ?? 1);
|
|
75
|
+
return Number.isFinite(strength) ? Math.min(Math.max(strength, 0), 1) : 0;
|
|
76
|
+
}
|
|
77
|
+
/** How hard the occlusion darkens the colour: its own strength where named, else `into`'s. */
|
|
78
|
+
function colorOcclusion(occlusion) {
|
|
79
|
+
if (typeof occlusion === 'object' && occlusion.colorStrength !== undefined)
|
|
80
|
+
return atLeastZero(occlusion.colorStrength, 0);
|
|
81
|
+
return typeof occlusion === 'object' && occlusion.into === 'ambient'
|
|
82
|
+
? 0
|
|
83
|
+
: occlusionStrength(occlusion);
|
|
84
|
+
}
|
|
85
|
+
/** How hard the occlusion darkens the ambient light: its strength where that is its target. */
|
|
86
|
+
function ambientOcclusion(occlusion) {
|
|
87
|
+
return typeof occlusion === 'object' && occlusion.into === 'ambient'
|
|
88
|
+
? occlusionStrength(occlusion)
|
|
89
|
+
: 0;
|
|
71
90
|
}
|
|
72
91
|
function arrayLayer(value, otherwise) {
|
|
73
92
|
return value !== undefined && Number.isInteger(value) && value >= 0 ? value : otherwise;
|
|
@@ -110,8 +110,8 @@ export interface SurfaceLayers<Texture> {
|
|
|
110
110
|
* layers and any mask or added mask the array carries — darkening the surface by a strength from 0
|
|
111
111
|
* to 1: the large shading of a cliff that layers repeating across it cannot carry, as
|
|
112
112
|
* `meshNormal` carries its shape. A number is that strength, darkening the blended colour; the
|
|
113
|
-
* object form also chooses what it darkens
|
|
114
|
-
* none.
|
|
113
|
+
* object form also chooses what it darkens, the colour and the ambient light at strengths of their
|
|
114
|
+
* own, and the range its red is spread over. Absent or 0: none.
|
|
115
115
|
*/
|
|
116
116
|
readonly meshOcclusion?: number | SurfaceMeshOcclusion;
|
|
117
117
|
}
|
|
@@ -25,7 +25,9 @@ export declare class TextRenderer {
|
|
|
25
25
|
*
|
|
26
26
|
* Answers whether it drew, which the renderer counts on its frame budget.
|
|
27
27
|
*/
|
|
28
|
-
draw(viewportWidth: number, viewportHeight: number, originX: number, originY: number, style: TextStyle, timeSec: number
|
|
28
|
+
draw(viewportWidth: number, viewportHeight: number, originX: number, originY: number, style: TextStyle, timeSec: number,
|
|
29
|
+
/** The renderer's `passGrade`: the frame's, but none in a probe's face or a capture. */
|
|
30
|
+
outputTransform?: number, outputExposure?: number): boolean;
|
|
29
31
|
dispose(): void;
|
|
30
32
|
/** Both instance buffers, from the arrays the layout filled in place. */
|
|
31
33
|
private upload;
|
|
@@ -67,11 +67,17 @@ export class TextRenderer {
|
|
|
67
67
|
*
|
|
68
68
|
* Answers whether it drew, which the renderer counts on its frame budget.
|
|
69
69
|
*/
|
|
70
|
-
draw(viewportWidth, viewportHeight, originX, originY, style, timeSec
|
|
70
|
+
draw(viewportWidth, viewportHeight, originX, originY, style, timeSec,
|
|
71
|
+
/** The renderer's `passGrade`: the frame's, but none in a probe's face or a capture. */
|
|
72
|
+
outputTransform = 0, outputExposure = 1) {
|
|
71
73
|
if (this.layout.instanceCount === 0 || style.alpha <= 0)
|
|
72
74
|
return false;
|
|
73
75
|
const gl = this.gl;
|
|
74
76
|
gl.useProgram(this.program);
|
|
77
|
+
const grade = this.uniforms['uOutputTransform'];
|
|
78
|
+
if (grade !== undefined)
|
|
79
|
+
gl.uniform1i(grade, outputTransform);
|
|
80
|
+
setFloat(gl, this.uniforms['uOutputExposure'], outputExposure);
|
|
75
81
|
gl.bindVertexArray(this.vao);
|
|
76
82
|
/*
|
|
77
83
|
* Text gets a fresh depth buffer rather than no depth test at all.
|
|
@@ -41,7 +41,7 @@ export declare const OUTPUT_TRANSFORM_CODE: Readonly<Record<string, number>>;
|
|
|
41
41
|
/**
|
|
42
42
|
* The code a **forward** pass grades with: the transform's own, except `filmic`, which only the
|
|
43
43
|
* composite carries the constants for, and which a pass grading itself therefore draws as `aces`.
|
|
44
|
-
*
|
|
44
|
+
* `passGrade.ts` reads this for both backends, so neither can hand a forward stage a code it lacks.
|
|
45
45
|
*/
|
|
46
46
|
export declare function forwardTransformCode(transform: string): number;
|
|
47
47
|
/**
|
|
@@ -115,7 +115,7 @@ export const OUTPUT_TRANSFORM_CODE = {
|
|
|
115
115
|
/**
|
|
116
116
|
* The code a **forward** pass grades with: the transform's own, except `filmic`, which only the
|
|
117
117
|
* composite carries the constants for, and which a pass grading itself therefore draws as `aces`.
|
|
118
|
-
*
|
|
118
|
+
* `passGrade.ts` reads this for both backends, so neither can hand a forward stage a code it lacks.
|
|
119
119
|
*/
|
|
120
120
|
export function forwardTransformCode(transform) {
|
|
121
121
|
return transform === 'filmic' ? 2 : (OUTPUT_TRANSFORM_CODE[transform] ?? 0);
|
|
@@ -158,6 +158,6 @@ export declare class WaterRenderer {
|
|
|
158
158
|
* by geometry in front of it, but its far edge fades out, and that fade needs
|
|
159
159
|
* the sky already behind it to blend into.
|
|
160
160
|
*/
|
|
161
|
-
draw(gl: WebGL2RenderingContext, camera: FrameView, timeSeconds: number, settings: WaterBody, directionalDir: Vec3, directionalColor: Vec3, ambient: Vec3, atmosphere: Atmosphere, underwaterEnabled: boolean, reflection: PlanarReflection | null, lights?: PointLightSet | null, lightFalloff?: 'smooth' | 'inverseSquare', windX?: number, windZ?: number): void;
|
|
161
|
+
draw(gl: WebGL2RenderingContext, camera: FrameView, timeSeconds: number, settings: WaterBody, directionalDir: Vec3, directionalColor: Vec3, ambient: Vec3, atmosphere: Atmosphere, underwaterEnabled: boolean, reflection: PlanarReflection | null, lights?: PointLightSet | null, lightFalloff?: 'smooth' | 'inverseSquare', windX?: number, windZ?: number, outputTransform?: number, outputExposure?: number): void;
|
|
162
162
|
dispose(gl: WebGL2RenderingContext): void;
|
|
163
163
|
}
|
|
@@ -95,10 +95,13 @@ export class WaterRenderer {
|
|
|
95
95
|
* by geometry in front of it, but its far edge fades out, and that fade needs
|
|
96
96
|
* the sky already behind it to blend into.
|
|
97
97
|
*/
|
|
98
|
-
draw(gl, camera, timeSeconds, settings, directionalDir, directionalColor, ambient, atmosphere, underwaterEnabled, reflection, lights = null, lightFalloff = 'smooth', windX = 0, windZ = 0) {
|
|
98
|
+
draw(gl, camera, timeSeconds, settings, directionalDir, directionalColor, ambient, atmosphere, underwaterEnabled, reflection, lights = null, lightFalloff = 'smooth', windX = 0, windZ = 0, outputTransform = 0, outputExposure = 1) {
|
|
99
99
|
const u = this.uniforms;
|
|
100
100
|
const reflectionReady = reflection?.isReadyFor(settings.level) ?? false;
|
|
101
101
|
gl.useProgram(this.program);
|
|
102
|
+
/* The renderer's `passGrade`: the frame's, but none in a probe's face or a capture. */
|
|
103
|
+
gl.uniform1i(u['uOutputTransform'] ?? null, outputTransform);
|
|
104
|
+
gl.uniform1f(u['uOutputExposure'] ?? null, outputExposure);
|
|
102
105
|
gl.uniformMatrix4fv(u['uViewProj'] ?? null, false, camera.viewProjection);
|
|
103
106
|
gl.uniformMatrix4fv(u['uReflectionViewProj'] ?? null, false, reflection?.viewProjection ?? camera.viewProjection);
|
|
104
107
|
gl.uniform3fv(u['uCameraPos'] ?? null, camera.position);
|
|
@@ -33,6 +33,8 @@ export declare class WindStreakRenderer {
|
|
|
33
33
|
/** Answers whether it drew, which the renderer counts on its frame budget. */
|
|
34
34
|
draw(gl: WebGL2RenderingContext, camera: FrameView, wind: WindField, timeSeconds: number, tint: Vec3,
|
|
35
35
|
/** True when the camera is below the waterline. */
|
|
36
|
-
submerged?: boolean
|
|
36
|
+
submerged?: boolean,
|
|
37
|
+
/** The renderer's `passGrade`: the frame's, but none in a probe's face or a capture. */
|
|
38
|
+
outputTransform?: number, outputExposure?: number): boolean;
|
|
37
39
|
dispose(gl: WebGL2RenderingContext): void;
|
|
38
40
|
}
|
|
@@ -51,7 +51,9 @@ export class WindStreakRenderer {
|
|
|
51
51
|
/** Answers whether it drew, which the renderer counts on its frame budget. */
|
|
52
52
|
draw(gl, camera, wind, timeSeconds, tint,
|
|
53
53
|
/** True when the camera is below the waterline. */
|
|
54
|
-
submerged = false
|
|
54
|
+
submerged = false,
|
|
55
|
+
/** The renderer's `passGrade`: the frame's, but none in a probe's face or a capture. */
|
|
56
|
+
outputTransform = 0, outputExposure = 1) {
|
|
55
57
|
/* Chosen in `windStreakDraw.ts` so both backends show the same weather. */
|
|
56
58
|
const settled = resolveWindStreaks(wind.speed, wind.driftX, wind.driftZ, this.onsetSpeed, this.fullSpeed, submerged, this.resolved);
|
|
57
59
|
if (!settled.visible)
|
|
@@ -68,6 +70,8 @@ export class WindStreakRenderer {
|
|
|
68
70
|
gl.uniform1f(u['uCellSize'] ?? null, this.cellSize);
|
|
69
71
|
gl.uniform1f(u['uTime'] ?? null, timeSeconds);
|
|
70
72
|
gl.uniform3fv(u['uTint'] ?? null, tint);
|
|
73
|
+
gl.uniform1i(u['uOutputTransform'] ?? null, outputTransform);
|
|
74
|
+
gl.uniform1f(u['uOutputExposure'] ?? null, outputExposure);
|
|
71
75
|
gl.enable(gl.BLEND);
|
|
72
76
|
blendCovering(gl, gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
|
|
73
77
|
gl.depthMask(false);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@driftengine/core",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.13.0",
|
|
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.
|
|
33
|
-
"@driftengine/physics": "4.
|
|
32
|
+
"@driftengine/drft": "4.13.0",
|
|
33
|
+
"@driftengine/physics": "4.13.0",
|
|
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.
|
|
71
|
-
"@driftengine/network": "4.
|
|
72
|
-
"@driftengine/texture": "4.
|
|
70
|
+
"@driftengine/nav": "4.13.0",
|
|
71
|
+
"@driftengine/network": "4.13.0",
|
|
72
|
+
"@driftengine/texture": "4.13.0"
|
|
73
73
|
}
|
|
74
74
|
}
|