diff --git a/changes/the-ground-up-close.md b/changes/the-ground-up-close.md new file mode 100644 index 00000000..9aa4dabd --- /dev/null +++ b/changes/the-ground-up-close.md @@ -0,0 +1,26 @@ +bump: patch +type: fix +The ground keeps more of its own colour, and the rock keeps all of it. + +55% of every ground pixel was the orthophotograph, at every distance and on every +material. A survey image is a good record of WHERE the forest and the scree are and a +poor record of what a steep rock face is coloured: it is shot through kilometres of air +at an angle no player stands at, and it comes back pale. So the photograph keeps the +meadow and the forest, where it carries real variation nothing else knows, and gives the +rock back to the rock - by material and by elevation, never by distance, because a +distance fade used to live here and was removed for a good reason (the photograph has +the day's own shadows baked in, so grass grew dark patches as you backed away and they +slid as you walked). Material and height do not move when the camera does. + +The far tier's rock fallback is the maroon mean rather than a neutral grey. TFAST_3 skips +the rock sample and leaves that constant standing in for it, and the file's own rule says +the cheap tier must keep the near tier's mean colour or the boundary shows as a ring. + +Ortho-classified snow now needs altitude. Bright and unsaturated is what snow looks like +from a satellite and also what a sunlit rock face looks like, so faces were being painted +90% white wherever the photograph was pale. Gully snow is gated on the snow line too, +rather than on two absolute heights five hundred metres below where snow can lie. + +And the distance desaturation from `light-air-and-the-hour` is eased from 1.9 to 1.15. +It was tuned on a valley whose rock was grey anyway; Maroon's air is thin and dry and the +peaks are two kilometres from the lake. diff --git a/packages/ludic.render3d/daylight.ludic b/packages/ludic.render3d/daylight.ludic index 5f5b015b..7b404a56 100644 --- a/packages/ludic.render3d/daylight.ludic +++ b/packages/ludic.render3d/daylight.ludic @@ -164,7 +164,12 @@ function daylight_set(hours: int) -> void { # the glow a ridge is silhouetted against when you look into a low sun r3d_fog_inscatter = f_mul(f_add(fl(0.012), f_mul(fl(0.16), lowsun)), f_sub(F_ONE, f_mul(fl(0.7), oc))) # and distance takes colour away at every hour, harder under cloud - r3d_fog_desat = f_add(fl(1.9), f_mul(fl(0.6), oc)) + # 1.9 was tuned on a valley whose rock was grey anyway. Maroon's air is thin, dry and at + # 2900 m, and the Bells are only a couple of kilometres from the lake - in the photograph + # everybody knows, they are still plainly RED at that distance. Desaturating them to a pale + # grey-pink is physically defensible and loses the one thing the range is named for, so the + # basin's own air gets a gentler figure and overcast still takes colour away faster. + r3d_fog_desat = f_add(fl(1.15), f_mul(fl(0.7), oc)) # ---- the grade ------------------------------------------------------------------------- # The look of an hour is not only how much air is in front of the mountain; it is what colour # the light is and what the shadows are filled with. Noon is the case worth naming: direct diff --git a/packages/ludic.render3d/shaders/spv/1f95f31b.frag.spv b/packages/ludic.render3d/shaders/spv/1f95f31b.frag.spv index ab6bfa01..de1f0a03 100644 Binary files a/packages/ludic.render3d/shaders/spv/1f95f31b.frag.spv and b/packages/ludic.render3d/shaders/spv/1f95f31b.frag.spv differ diff --git a/packages/ludic.render3d/shaders/spv/2e2908c8.frag.spv b/packages/ludic.render3d/shaders/spv/2e2908c8.frag.spv index 4cee9aeb..ea3749cb 100644 Binary files a/packages/ludic.render3d/shaders/spv/2e2908c8.frag.spv and b/packages/ludic.render3d/shaders/spv/2e2908c8.frag.spv differ diff --git a/packages/ludic.render3d/shaders/spv/5e4589a1.frag.spv b/packages/ludic.render3d/shaders/spv/5e4589a1.frag.spv index 0bb4efa2..8b0741f2 100644 Binary files a/packages/ludic.render3d/shaders/spv/5e4589a1.frag.spv and b/packages/ludic.render3d/shaders/spv/5e4589a1.frag.spv differ diff --git a/packages/ludic.render3d/shaders/spv/a0f2dbac.frag.spv b/packages/ludic.render3d/shaders/spv/a0f2dbac.frag.spv index e871b15d..eaedc828 100644 Binary files a/packages/ludic.render3d/shaders/spv/a0f2dbac.frag.spv and b/packages/ludic.render3d/shaders/spv/a0f2dbac.frag.spv differ diff --git a/packages/ludic.render3d/shaders/spv/f2952361.frag.spv b/packages/ludic.render3d/shaders/spv/f2952361.frag.spv index 5ea053bf..9e959c81 100644 Binary files a/packages/ludic.render3d/shaders/spv/f2952361.frag.spv and b/packages/ludic.render3d/shaders/spv/f2952361.frag.spv differ diff --git a/packages/ludic.render3d/shaders/spv/f4382918.frag.spv b/packages/ludic.render3d/shaders/spv/f4382918.frag.spv index d6382eec..0d0808dc 100644 Binary files a/packages/ludic.render3d/shaders/spv/f4382918.frag.spv and b/packages/ludic.render3d/shaders/spv/f4382918.frag.spv differ diff --git a/packages/ludic.render3d/shaders/terrain.frag b/packages/ludic.render3d/shaders/terrain.frag index 4e7e8869..a0b923cc 100644 --- a/packages/ludic.render3d/shaders/terrain.frag +++ b/packages/ludic.render3d/shaders/terrain.frag @@ -268,18 +268,37 @@ vec3 groundShade(vec3 p, vec3 N, float slope, float dist, float viewDepth, bool float mxc = max(ocl.r, max(ocl.g, ocl.b)), mnc = min(ocl.r, min(ocl.g, ocl.b)); float greenEx = ocl.g - max(ocl.r, ocl.b); screeC = smoothstep(0.008, -0.002, greenEx) * smoothstep(0.06, 0.12, mxc) * (1.0 - smoothstep(0.55, 0.75, mxc)) * smoothstep(waterLine(p.xz) + 0.2, waterLine(p.xz) + 1.2, p.y); - snowC = smoothstep(0.08, 0.04, mxc - mnc) * smoothstep(0.55, 0.8, mxc); + // Snow, according to the photograph: bright and unsaturated. Both of which a SUNLIT ROCK + // FACE also is, seen from a satellite through several kilometres of air - so the Bells' + // own faces classified as snow and were painted 90% white, which is the real reason the + // range named for the colour of its rock rendered as a white cone. It needs the elevation + // gate the terrain's own snow has: snow does not lie below the snow line in July, however + // pale the photograph is there. + snowC = smoothstep(0.08, 0.04, mxc - mnc) * smoothstep(0.62, 0.84, mxc) + * smoothstep(u_snow_line - 260.0, u_snow_line - 40.0, p.y); } // snow lingering in the high gullies is drawn further down, but whether it can be // there at all is known now, and it is the third caller of the snow sample - float gullyGate = smoothstep(450.0, 650.0, p.y) * smoothstep(0.75, 0.35, slope); + // Snow lingering in the high gullies, and it has to be gated on THE SNOW LINE rather than on + // two absolute heights. At 450-650 m it started five hundred metres below where snow can lie, + // covered most of the upper mountain at nine tenths opacity, and is the single biggest reason + // the Bells rendered as a white cone: the mean colour of that peak measured 208/255 and + // neutral, which is not rock under any light, it is snow. Gullies hold snow NEAR the line, + // not half a kilometre under it. + float gullyGate = smoothstep(u_snow_line - 340.0, u_snow_line - 120.0, p.y) * smoothstep(0.75, 0.35, slope); // ---- samples --------------------------------------------------------------------- // World-space derivatives, taken once and in unbranched control flow: every sample // below is in a branch and takes its gradients from these. vec3 dpx = dFdx(p), dpy = dFdy(p); vec3 gA = vec3(0.3, 0.4, 0.2), gN = vec3(0.0, 0.0, 1.0), gR = vec3(1.0, 0.8, 0.0); - vec3 rA = vec3(0.3), rN = vec3(0.0, 1.0, 0.0), rR = vec3(1.0, 0.8, 0.0); + // The rock's fallback is the MAROON MEAN, not a neutral grey. The far tier (TFAST_3) skips + // the rock sample altogether and leaves this constant standing in for it, so a neutral grey + // here is what the mountain three kilometres away is actually painted - and it is the + // mountain the range is named for. The file's own rule a hundred lines up says the cheap + // tier must keep the near tier's MEAN COLOUR or the boundary shows as a ring; this is that + // rule applied to the one material where the mean is not grey. + vec3 rA = vec3(0.26, 0.13, 0.10), rN = vec3(0.0, 1.0, 0.0), rR = vec3(1.0, 0.8, 0.0); vec3 sA = vec3(0.86, 0.88, 0.92), sN = vec3(0.0, 0.0, 1.0), sR = vec3(1.0, 0.55, 0.0); vec3 pA, pN, pR; vec2 uvg = p.xz * 0.28; @@ -337,7 +356,11 @@ vec3 groundShade(vec3 p, vec3 N, float slope, float dist, float viewDepth, bool rA *= mix(1.0, 0.75 + 0.5 * smoothstep(0.35, 0.65, strata), 0.5 * smoothstep(0.3, 0.6, slope)); rN = normalize(rN + vec3(rN2.x, 0.0, rN2.y) * 0.6 * smoothstep(80.0, 400.0, dist)); // the Bells are maroon mudstone: warm red-brown rock with grey scree below - rA *= mix(vec3(0.14, 0.08, 0.06), vec3(0.24, 0.15, 0.11), fbmC(cheap, p.xz * 0.003, 2) * 0.5 + 0.5); + // The Bells are maroon mudstone: warm red-brown rock, with grey weathered scree below. + // These were tuned while 55% of a grey photograph was mixed over them, so they had to + // be dark and timid to survive it; with the rock now mostly its own colour they can be + // the colour the range is named for. + rA *= mix(vec3(0.38, 0.17, 0.13), vec3(0.58, 0.31, 0.23), fbmC(cheap, p.xz * 0.003, 2) * 0.5 + 0.5); } if (needSnow > 0.002) { sA = textureGrad(u_snow_d, p.xz * 0.25, dpx.xz * 0.25, dpy.xz * 0.25).rgb * 0.8; @@ -370,7 +393,28 @@ vec3 groundShade(vec3 p, vec3 N, float slope, float dist, float viewDepth, bool // flown, so grass that was plain up close grew dark patches as you backed away, and // those patches slid and changed shape as you walked. A constant keeps the // photograph's large-scale colour without tying any of it to the camera. - float orthoW = 0.55; + // How much of the photograph shows through, and it is NOT one number any more. + // + // The Maroon Bells are named for the colour of their mudstone and rendered grey, and + // this line was why: the maroon tint two hundred lines up is real, and then 55% of a + // hazy, sun-washed aerial photograph was mixed over the top of it everywhere. A survey + // image is a good record of WHERE the forest and the scree are and a poor record of + // what a steep rock face is coloured - it is shot through kilometres of atmosphere at + // an angle no player ever stands at. + // + // So the photograph keeps the meadow and the forest, where it carries real variation + // nothing else knows about, and gives the rock back to the rock. Deliberately NOT a + // distance fade: one used to live here and was removed because the photograph has the + // day's own shadows baked into it, so grass that was plain up close grew dark patches + // as you backed away and those patches slid as you walked. This keys off the material, + // which does not move when the camera does. + float orthoW = 0.55 * (1.0 - 0.62 * rockW); + // ...and it falls away with HEIGHT as well. A survey image is least trustworthy exactly + // where it matters most here: a high face is shot through the most air, at the most + // oblique angle, and comes back pale and grey whatever colour the rock is. Down in the + // valley the same image is the best record there is of where the meadow, the duff and the + // gravel actually are, so it keeps the valley and gives up the mountain. + orthoW *= 1.0 - 0.72 * smoothstep(280.0, 720.0, p.y); // grain at three scales so the far slopes keep structure the photograph's pixels cannot carry #ifdef TFAST_20 float grain = 0.82;