From 98af66f567f70bd0158cff777209052917a0926f Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 11 Oct 2026 10:31:34 +0000 Subject: [PATCH] Etna 0.8.1: water shade lines up with the bottom; far water less white waterBodyShadow follows the straight line of sight into the water instead of the refracted ray. The bottom shows through the water unbent (the blend lays the water over it), so the shade in the water now lands on the shadow seen on the bottom. Before, the two sat side by side: shaded water over a lit bottom looked colourless and see-through, lit water over a shaded bottom like a patch of colour. Far water seen nearly edge on: the waves a pixel no longer resolves still tilt the facets it sees towards the eye (HIDDEN_TILT, 0.08), so they mirror sky from a little above the horizon and less of it than a flat mirror (waterSeenCos, waterSeenMirror in water_waves.glsl, used by the terrain and by the reflections pass alike). From a block or two above the water, far water mirrored almost only the whitish horizon. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01Xzo7zbTpyU7a3y3XQNbw1n --- .../etna/shaders/include/terrain_water.glsl | 14 ++++++++----- .../etna/shaders/include/water_waves.glsl | 20 +++++++++++++++++++ .../assets/etna/shaders/post/water_ssr.fsh | 4 ++-- 3 files changed, 31 insertions(+), 7 deletions(-) diff --git a/src/main/resources/assets/etna/shaders/include/terrain_water.glsl b/src/main/resources/assets/etna/shaders/include/terrain_water.glsl index 485f915..a0ab93c 100644 --- a/src/main/resources/assets/etna/shaders/include/terrain_water.glsl +++ b/src/main/resources/assets/etna/shaders/include/terrain_water.glsl @@ -587,8 +587,12 @@ float waterDepthBelow(out float trust) { // the bottom seen through it, and on murky or deep water a soft shadow in the water itself. Before 0.8.1 // the surface took the sharp shadow of its own point, a second copy of every shadow next to the one on // the bottom (shifted from it by the depth, the more the lower the sun), and the two crossed each other out -// in strange shapes. Four points down the view's way into the water, each at an equal share of the light -// scattered back above the bottom, each the bilinear blend of four texels (shadowSoftAt). A point counts +// in strange shapes. Four points down the line of sight into the water, each at an equal share of the light +// scattered back above the bottom, each the bilinear blend of four texels (shadowSoftAt). The line of sight +// goes straight on, unbent: the bottom shows through the water unbent too (the blend just lays the water over +// it), so the shade in the water lands on the shadow seen on the bottom. Up to 0.9.0 it followed the +// refracted ray, which runs steeper, and the two shadows sat side by side: shaded water over a lit bottom +// looked colourless and see-through, lit water over a shaded bottom like a patch of colour. A point counts // as shaded only by what stands above the water's surface on its way to the sun: it is looked up where // that way crosses the surface (the same texels of the map, a shallower depth), so a point that ends up // inside a bottom rising ahead of the view (a beach stepping up out of a lake) doesn't take the bottom over @@ -602,7 +606,7 @@ float waterBodyShadow(vec3 normal) { if (trust <= 0.0) { return shadowVisibility(normal, false); } - vec3 down = refract(normalize(etnaViewPos), vec3(0.0, 1.0, 0.0), 1.0 / WATER_IOR); + vec3 down = normalize(etnaViewPos); float cosDown = max(-down.y, 0.1); // Light scattered back from z blocks down went z / sin(sun) blocks through the water on its way down // and comes z / cos(view) blocks back up: green's extinction over both, per block of depth. @@ -981,8 +985,8 @@ vec4 shadeWater(vec4 color, vec3 normal) { // would only shimmer. vec3 n = normalize(vec3(-waves.seen.x, 1.0, -waves.seen.y)); n.y *= sign(normal.y); - float fresnel = waterFresnel(dot(toEye, n)); - vec3 skyReflection = etnaWaterSky(reflect(-toEye, n)) * open; + float fresnel = waterFresnel(waterSeenCos(dot(toEye, n), etnaPixelFootprint)); + vec3 skyReflection = etnaWaterSky(waterSeenMirror(reflect(-toEye, n), etnaPixelFootprint)) * open; float sun = sunGlint(waves, toEye, h, light) + waterGlitter(p, t, etnaPixelFootprint, waves.seen, target, light); #ifdef ETNA_SHADOWS // No glint and no glitter in the shade of trees and cliffs. diff --git a/src/main/resources/assets/etna/shaders/include/water_waves.glsl b/src/main/resources/assets/etna/shaders/include/water_waves.glsl index c4ddfdd..5fee4fd 100644 --- a/src/main/resources/assets/etna/shaders/include/water_waves.glsl +++ b/src/main/resources/assets/etna/shaders/include/water_waves.glsl @@ -71,6 +71,26 @@ vec2 swellSeen(vec2 p, float t, float footprint) { return slope; } +// Far water seen nearly edge on. The waves a pixel no longer resolves (waveSeen) still tilt the facets it +// sees towards the eye, by about HIDDEN_TILT (the swell's and the ripples' slopes), which only matters when +// the view is about as flat as that: those facets mirror sky from above the horizon, and less of it than a +// flat mirror would. Without them, water far away seen from a block or two above it mirrored almost only +// the whitish horizon and looked white. How far the facets tilt, for a pixel footprint blocks wide. +const float HIDDEN_TILT = 0.08; +float waterHiddenTilt(float footprint) { + return HIDDEN_TILT * (1.0 - waveSeen(length(SWELL_WAVES[0].xy), footprint)); +} +// The cosine between the view and the facets the pixel sees, from cosView, the one to the resolved surface. +float waterSeenCos(float cosView, float footprint) { + float tilt = waterHiddenTilt(footprint); + return sqrt(cosView * cosView + tilt * tilt); +} +// Where those facets mirror the sky from, for `mirrored`, the reflection off the resolved surface. +vec3 waterSeenMirror(vec3 mirrored, float footprint) { + float lift = 2.0 * waterHiddenTilt(footprint); + float up = max(mirrored.y, 0.0); + return normalize(vec3(mirrored.x, sqrt(up * up + lift * lift), mirrored.z)); +} // Schlick's Fresnel for water seen from the air, at the cosine between the view and the surface's normal: // the share of the light the surface mirrors (4 % straight down, all of it at grazing angles). float waterFresnel(float cosView) { diff --git a/src/main/resources/assets/etna/shaders/post/water_ssr.fsh b/src/main/resources/assets/etna/shaders/post/water_ssr.fsh index 829d597..01a44e9 100644 --- a/src/main/resources/assets/etna/shaders/post/water_ssr.fsh +++ b/src/main/resources/assets/etna/shaders/post/water_ssr.fsh @@ -275,9 +275,9 @@ void main() { vec3 toEye = -position / dist; // The sky's reflection the water drew, and how strong, came off the full swell; the ray follows a // calmer one. - vec3 dir = reflect(-toEye, n); + vec3 dir = waterSeenMirror(reflect(-toEye, n), footprint); vec3 ray = reflect(-toEye, normalize(vec3(-slope.x * TRACE_WAVES, 1.0, -slope.y * TRACE_WAVES))); - float mirror = waterFresnel(dot(toEye, n)) * WaterSsr.x; + float mirror = waterFresnel(waterSeenCos(dot(toEye, n), footprint)) * WaterSsr.x; // The haze and the render-distance fog the water shader's colour went through (aerialHaze in // terrain_dark.glsl, calculateFinalColor in terrain.fsh), for the light the reflection adds or takes away. float openSky = Air.w > 0.5 ? 1.0 - Shafts.z : 0.0;