diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f80d65a..43b0c73 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -53,12 +53,12 @@ jobs: # Engine/ is a tracked symlink to ../SleakEngine, so the two repos must # sit side by side in the workspace for it to resolve. - name: Check out the game - uses: actions/checkout@v4 + uses: actions/checkout@v7 with: path: SleakCraft - name: Check out the engine - uses: actions/checkout@v4 + uses: actions/checkout@v7 with: repository: CanReader/SleakEngine ref: ${{ env.ENGINE_REF }} @@ -102,7 +102,7 @@ jobs: - name: Cache compiler output if: runner.os == 'Linux' - uses: actions/cache@v4 + uses: actions/cache@v6 with: path: ~/.ccache key: ccache-craft-${{ runner.os }}-${{ matrix.name }}-${{ github.sha }} @@ -168,12 +168,12 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out the game - uses: actions/checkout@v4 + uses: actions/checkout@v7 with: path: SleakCraft - name: Check out the engine - uses: actions/checkout@v4 + uses: actions/checkout@v7 with: repository: CanReader/SleakEngine ref: ${{ env.ENGINE_REF }} @@ -225,12 +225,12 @@ jobs: timeout-minutes: 90 steps: - name: Check out the game - uses: actions/checkout@v4 + uses: actions/checkout@v7 with: path: SleakCraft - name: Check out the engine - uses: actions/checkout@v4 + uses: actions/checkout@v7 with: repository: CanReader/SleakEngine ref: ${{ env.ENGINE_REF }} @@ -251,7 +251,7 @@ jobs: libasound2-dev libpulse-dev libudev-dev libdbus-1-dev \ glslc || sudo apt-get install -y glslang-tools - - uses: actions/cache@v4 + - uses: actions/cache@v6 with: path: ~/.ccache key: ccache-craft-smoke-${{ github.sha }} diff --git a/.github/workflows/dependabot-auto-merge.yml b/.github/workflows/dependabot-auto-merge.yml index 63386e8..810458a 100644 --- a/.github/workflows/dependabot-auto-merge.yml +++ b/.github/workflows/dependabot-auto-merge.yml @@ -16,7 +16,7 @@ jobs: steps: - name: Fetch update metadata id: meta - uses: dependabot/fetch-metadata@v2 + uses: dependabot/fetch-metadata@v3 with: github-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/engine-update.yml b/.github/workflows/engine-update.yml index 5ecc55d..ebff28d 100644 --- a/.github/workflows/engine-update.yml +++ b/.github/workflows/engine-update.yml @@ -22,12 +22,12 @@ jobs: timeout-minutes: 90 steps: - name: Check out the game - uses: actions/checkout@v4 + uses: actions/checkout@v7 with: path: SleakCraft - name: Check out the engine - uses: actions/checkout@v4 + uses: actions/checkout@v7 with: repository: CanReader/SleakEngine ref: ${{ github.event.client_payload.sha || 'main' }} @@ -47,7 +47,7 @@ jobs: glslc || sudo apt-get install -y glslang-tools - name: Cache compiler output - uses: actions/cache@v4 + uses: actions/cache@v6 with: path: ~/.ccache key: ccache-craft-engineupdate-${{ github.run_id }} @@ -62,7 +62,7 @@ jobs: - name: Open an issue if the engine broke the game if: failure() - uses: actions/github-script@v7 + uses: actions/github-script@v9 with: script: | const sha = context.payload.client_payload?.sha || 'main'; diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 17576a6..103b463 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -19,12 +19,12 @@ jobs: os: [ubuntu-latest, windows-latest] steps: - name: Check out the game - uses: actions/checkout@v4 + uses: actions/checkout@v7 with: path: SleakCraft - name: Check out the engine - uses: actions/checkout@v4 + uses: actions/checkout@v7 with: repository: CanReader/SleakEngine submodules: recursive @@ -85,7 +85,7 @@ jobs: ls -la - name: Attach to the release - uses: softprops/action-gh-release@v2 + uses: softprops/action-gh-release@v3 with: files: | SleakCraft/dist/*.tar.gz diff --git a/Client/src/main.cpp b/Client/src/main.cpp index 5d9bd28..66ecd91 100644 --- a/Client/src/main.cpp +++ b/Client/src/main.cpp @@ -2,6 +2,8 @@ #include #include #include +#include +#include #include #include @@ -30,8 +32,15 @@ static void PrintHelp(const char* exe) { << " --no-vsync Disable VSync on launch\n" << "\nBenchmark\n" << " --bench Start benchmark recording immediately\n" + << " --autofly Autopilot flight speed (Debug builds)\n" + << " -campose x,y,z,yaw,pitch\n" + << " Pin the camera, degrees (Debug builds)\n" + << " --gpuprofile Log per-pass GPU timings (Vulkan)\n" + << " -capture Write frame N as a PPM, then exit (Vulkan)\n" + << " -capture-frame Frame index for -capture (default: 600)\n" << "\nDebug\n" - << " --validate Enable Vulkan validation layer\n" + << " --validate Enable Vulkan validation (Debug: always on)\n" + << " --syncval Enable Vulkan synchronization validation\n" << "\nMisc\n" << " --help Show this message\n\n"; } @@ -56,6 +65,24 @@ int main(int argc, char** argv) { }; Game* game = new Game(); +#ifdef DEBUG + const std::string autoFly = defaults.CommandLineArgs["--autofly"]; + if (!autoFly.empty()) { + try { + game->SetAutoFlySpeed(std::stof(autoFly)); + } catch (...) { + } + } + // Raw argv: CommandLine drops values that start with '-' + for (int i = 1; i + 1 < argc; ++i) { + CameraPose pose; + if (std::strcmp(argv[i], "-campose") == 0 && + std::sscanf(argv[i + 1], "%f,%f,%f,%f,%f", &pose.x, &pose.y, + &pose.z, &pose.yawDeg, + &pose.pitchDeg) == CAMERA_POSE_FIELD_COUNT) + game->SetCameraPose(pose); + } +#endif Sleak::Application app(defaults); return app.Run(game); diff --git a/Game/assets/shaders/water_shader.frag b/Game/assets/shaders/water_shader.frag index b29c149..5621827 100644 --- a/Game/assets/shaders/water_shader.frag +++ b/Game/assets/shaders/water_shader.frag @@ -2,14 +2,11 @@ layout(location = 0) in vec3 fragWorldPos; layout(location = 1) in vec3 fragNormal; -layout(location = 2) in vec4 fragColor; layout(location = 3) in vec4 fragShadowCoord; layout(location = 4) in float fragTime; layout(location = 0) out vec4 outColor; -layout(set = 0, binding = 0) uniform sampler2D diffuseTexture; - // NOTE: see water_shader.vert — no MaterialUBO in the Vulkan layout. // Time comes from uCameraPos.w (interpolated via fragTime). layout(set = 2, binding = 0) uniform ShadowLightUBO { @@ -35,6 +32,44 @@ layout(set = 2, binding = 0) uniform ShadowLightUBO { layout(set = 3, binding = 0) uniform sampler2DShadow shadowMap; +// Scene read (VertexLayoutDesc::transparentReadsScene) +layout(set = 3, binding = 2) uniform sampler2D sceneColor; // linear HDR +layout(set = 3, binding = 3) uniform sampler2D sceneDepth; +layout(set = 3, binding = 4) uniform samplerCube skyCube; +layout(set = 3, binding = 5) uniform SceneReadUBO { + mat4 uViewProj; + mat4 uInvViewProj; + vec4 uScreenSize; // w, h, 1/w, 1/h + vec4 uSceneCamera; // xy = TAA jitter in NDC, z = near, w = far + vec4 uSceneFlags; // x = scene textures valid, y = sky valid +}; + +#define EPSILON 1e-4 + +float SignedToUnit(float v) { return v * 0.5 + 0.5; } +vec2 SignedToUnit(vec2 v) { return v * 0.5 + 0.5; } +vec2 UnitToSigned(vec2 v) { return v * 2.0 - 1.0; } +bool IsSet(float flag) { return flag > 0.5; } + +// ============================================================ +// Backend conventions (Vulkan: UV y down, depth = NDC z in [0,1]) +// ============================================================ +vec3 ProjectToScreen(vec4 clip) { + vec3 ndc = clip.xyz / clip.w; + vec2 uv = SignedToUnit(vec2(ndc.x, -ndc.y) + uSceneCamera.xy); + return vec3(uv, ndc.z); +} +vec3 Reconstruct(vec2 uv, float depth) { + vec2 ndc = UnitToSigned(uv) - uSceneCamera.xy; + vec4 w = uInvViewProj * vec4(ndc.x, -ndc.y, depth, 1.0); + return w.xyz / w.w; +} +float DepthToNdc(float depth) { return depth; } +vec3 SceneToLinear(vec3 c) { return c; } +vec3 SkyRadiance(vec3 dir, float lod) { + return textureLod(skyCube, dir, lod).rgb; +} + // ============================================================ // Shadow — 16-sample PCF with IGN rotation (matches GL/DX11/DX12) // ============================================================ @@ -61,142 +96,462 @@ float CalcShadow(vec4 sc) { float rad = uShadowTexelSize * uLightSize * 6.0; float s = 0.0; for (int i = 0; i < 16; i++) - s += texture(shadowMap, vec3(p.xy + rot * disk[i] * rad, p.z - uShadowBias)); + s += textureLod(shadowMap, vec3(p.xy + rot * disk[i] * rad, p.z - uShadowBias), 0.0); return mix(1.0, s / 16.0, uShadowStrength * fc.x * fc.y); } // ============================================================ -// Procedural wave height field (BSL-style: 2 wind layers, finite-diff normals) +// Lighting inputs // ============================================================ -float WaterHeight(vec2 xz, float time) { - vec2 w1 = vec2( time * 0.45, time * 0.30); - vec2 w2 = vec2(-time * 0.35, time * 0.50); +vec3 SunDirection() { return normalize(-uLightDir.xyz); } +vec3 SunRadiance() { return uLightColor.rgb * uLightColor.a; } +vec3 AmbientRadiance() { return uAmbient.rgb * uAmbient.a; } +vec3 CameraPosition() { return uCameraPos.xyz; } + +// Fog terms of lighting_pass ApplyFog, split so the snapshot can be unfogged +float FogAmount(vec3 p) { + if (uFogEnd <= 0.0) return 0.0; + float dist = length(p.xz - uCameraPos.xz); + float distFog = clamp((dist - uFogStart) / max(uFogEnd - uFogStart, EPSILON), 0.0, 1.0); + float heightFog = 0.0; + if (IsSet(uHeightFogEnabled)) { + float h = max(uHeightFogTop - p.y, 0.0); + heightFog = clamp(uHeightFogDensity * (1.0 - exp(-h * uHeightFogFalloff)), + 0.0, 1.0); + } + return 1.0 - (1.0 - distFog) * (1.0 - heightFog); +} +vec3 FogColorAt(vec3 p) { + vec3 viewDir = normalize(p - uCameraPos.xyz); + float t = smoothstep(0.0, 1.0, clamp(SignedToUnit(viewDir.y), 0.0, 1.0)); + return mix(uFogColor.rgb, uFogZenithColor.rgb, t); +} + +// ==== SHARED WATER BODY (keep identical in water_shader_gl.frag) ==== - float h = 0.0; - h += sin(xz.x * 0.24 + w1.x) * sin(xz.y * 0.19 + w1.y); - h += sin(xz.x * 0.33 + w2.x - xz.y * 0.11) * 0.65; - h += sin(xz.x * 0.80 + w1.x * 1.9 + xz.y * 0.58) * 0.40; - h += sin(xz.x * 1.10 - w2.x * 2.1) * sin(xz.y * 0.88 + w2.y * 1.4) * 0.35; - h += sin(xz.x * 2.20 + w1.x * 3.3) * sin(xz.y * 1.85 - w1.y * 2.7) * 0.15; - h += sin(xz.x * 3.00 - w2.x * 4.2 + xz.y * 2.30) * 0.10; - return h; +// ---- Physical constants ---- +#define PI 3.14159265 +#define TWO_PI (2.0 * PI) +#define GRAVITY 9.81 // m/s^2 +#define WATER_IOR 1.333 +#define AIR_IOR 1.0 +#define FRESNEL_F0 pow((WATER_IOR - AIR_IOR) / (WATER_IOR + AIR_IOR), 2.0) +// Cosine of the total internal reflection angle seen from below +#define TIR_COS sqrt(1.0 - pow(AIR_IOR / WATER_IOR, 2.0)) +#define TIR_SOFTEN 0.04 + +// ---- Waves: octaves derived from a few parameters ---- +#define WAVE_COUNT 12 +#define WAVE_BASE_LENGTH 13.0 // m, longest octave +#define WAVE_LENGTH_RATIO (1.0 / sqrt(2.0)) // irrational octave spacing +#define WAVE_LENGTH_JITTER 0.12 // +- fraction added to each octave step +#define WAVE_BASE_STEEPNESS 0.075 // slope amplitude of the longest octave +#define WAVE_STEEPNESS_DECAY 0.8946 // per-octave steepness falloff +#define WIND_ANGLE 0.2 // rad, mean travel direction in xz +#define WIND_SPREAD_LONG 0.9 // rad, long octaves stay nearer the wind +#define WIND_SPREAD_SHORT 1.4 // rad, short chop spreads wider +#define WAVE_DIR_SEED 17.0 // hash streams per octave index +#define WAVE_PHASE_SEED 31.0 +#define WAVE_LENGTH_SEED 47.0 +#define WAVE_SPEED_SCALE 0.55 // fraction of deep-water phase speed +#define WAVE_FADE_START 0.08 // pixel footprint / wavelength +#define WAVE_FADE_END 0.25 // 4 pixels per wavelength + +// ---- Wave field variation ---- +#define WARP_SCALE (1.0 / 55.0) // 1/m, domain warp noise +#define WARP_AMPLITUDE 2.5 // m the wave coordinates bend +#define WARP_SPEED 0.03 // noise units per second +#define WARP_CHANNEL_OFFSET vec2(19.3, 7.7) // decorrelates warp x and y +#define SWELL_SCALE (1.0 / 90.0) // 1/m, long-octave amplitude patches +#define SWELL_RANGE vec2(0.65, 1.15) // min, max amplitude +#define SWELL_DRIFT vec2(0.011, 0.007) // noise units per second +#define GUST_SCALE (1.0 / 24.0) // 1/m, short-octave gust patches +#define GUST_RANGE vec2(0.35, 1.3) +#define GUST_DRIFT vec2(-0.03, 0.02) +#define CALM_START 30.0 // m from the camera where the surface settles +#define CALM_END 100.0 +#define FAR_SLOPE_SCALE 0.35 // remaining wave slope far away + +// ---- Optics ---- +#define BASE_ROUGHNESS 0.035 +#define SIGMA_T vec3(0.46, 0.105, 0.075) // absorption + scattering, 1/m +#define SCATTER_ALBEDO vec3(0.014, 0.080, 0.150) +#define MIN_TRANSMIT_COS 0.2 // caps the slant path at grazing angles +#define SPEC_MAX 30.0 +#define SUN_SHADOW_FADE vec2(-0.1, 0.2) // N.L range where the sun fades in +#define MAX_UNFOG 0.9 +#define BOTTOM_TRUST_FOG 0.4 // fog amount where the bottom color starts to fade out + +// ---- Sky ---- +#define SKY_MIN_Y 0.07 // stay above the skybox's own horizon band +#define SKY_MAX_RADIANCE 16.0 +#define SKY_LOD_PER_SLOPE 10.0 +#define SKY_HORIZON vec3(0.70, 0.82, 0.95) +#define SKY_ZENITH vec3(0.22, 0.42, 0.82) +#define SKY_HORIZON_TINT 0.30 +#define SKY_ZENITH_TINT 0.25 +#define SKY_ZENITH_DIM 0.65 +#define SKY_GRADIENT_POW 0.45 +#define SKY_SUN_POW 60.0 +#define SKY_SUN_GAIN 0.5 + +// ---- Refraction and bottom ---- +#define REFRACT_SCALE 1.2 // m of bottom shift per unit slope and m depth +#define REFRACT_MAX_DEPTH 2.5 +#define REFRACT_DEPTH_BIAS 0.05 // m the bent sample must sit behind the surface +#define ABSENT_DEPTH 1e3 // depth used when nothing lies behind the water +#define BLUR_PER_DEPTH 0.06 // m of blur radius per m of depth +#define BLUR_MIN_PX 0.5 +#define BLUR_MAX_PX 6.0 +#define BLUR_TAPS 8 // fixed Vogel disk, no per-pixel noise to boil under motion +#define GOLDEN_ANGLE 2.39996323 +#define VOGEL_RING_OFFSET 0.5 // centres each tap in its equal-area ring +#define CAUSTIC_SCALE 0.9 +#define CAUSTIC_STRENGTH 1.2 +#define CAUSTIC_FALLOFF 0.35 // 1/m +#define CAUSTIC_FADE_IN vec2(0.05, 0.5) +#define CAUSTIC_SHARPNESS 5.0 +#define MIN_SUN_ELEVATION 0.2 + +// ---- Screen-space reflection ---- +#define SSR_MIN_FRESNEL 0.04 +#define SSR_STEPS 16 +#define SSR_BATCH 4 // depth fetches issued together +#define SSR_REFINE_STEPS 4 +#define SSR_START 0.5 // m, fixed so reflections stay still under motion +#define SSR_GROWTH 1.38 +#define SSR_STEP_ADD 0.1 // m +#define SSR_MAX_DIST 90.0 // m +#define SSR_MIN_THICKNESS 0.75 // m +#define SSR_THICKNESS_STEPS 2.0 // hit tolerance in step lengths +#define SSR_EDGE_FADE 0.07 // UV +#define SSR_DIST_FADE_START 0.6 // fraction of SSR_MAX_DIST +#define SSR_ORIGIN_OFFSET 0.02 // m along the normal +#define SSR_MIN_DIR_Y 0.015 + +// ---- Shoreline ---- +#define SHORE_FADE_DEPTH 0.12 // m of depth over which the contact line fades in +#define FOAM_MAX_DEPTH 0.7 +#define FOAM_BAND 0.6 +#define FOAM_SCALE 3.0 // 1/m +#define FOAM_DRIFT vec2(0.21, -0.17) +#define FOAM_DETAIL_SCALE 2.3 +#define FOAM_DETAIL_OFFSET 7.1 +#define FOAM_DETAIL_WEIGHT 0.35 +#define FOAM_THRESHOLD vec2(0.5, 0.85) +#define FOAM_NOISE_WEIGHT 0.7 +#define FOAM_SHORE_WEIGHT 0.55 +#define FOAM_OPACITY 0.8 +#define FOAM_ALBEDO 0.85 + +// ---- Faces ---- +#define TOP_FACE_MIN_Y 0.5 // normal.y above which a face is the water top + +// ---- Hash (Dave Hoskins, hash without sine) ---- +#define HASH_SCALE 0.1031 +#define HASH_OFFSET 33.33 + +// ---- Caustics: two warped sine layers (x = layer A, y = layer B) ---- +#define CAUSTIC_FREQ vec2(1.7, 1.9) +#define CAUSTIC_WARP_FREQ vec2(1.3, 1.1) +#define CAUSTIC_WARP_AMOUNT vec2(1.2, 1.3) +#define CAUSTIC_WARP_SPEED vec2(0.8, -0.7) +#define CAUSTIC_DRIFT vec2(0.6, -0.5) + +float Hash12(vec2 p) { + vec3 p3 = fract(vec3(p.xyx) * HASH_SCALE); + p3 += dot(p3, p3.yzx + HASH_OFFSET); + return fract((p3.x + p3.y) * p3.z); +} +float ValueNoise(vec2 p) { + vec2 i = floor(p), f = fract(p); + vec2 u = f * f * (3.0 - 2.0 * f); + return mix(mix(Hash12(i), Hash12(i + vec2(1.0, 0.0)), u.x), + mix(Hash12(i + vec2(0.0, 1.0)), Hash12(i + vec2(1.0, 1.0)), u.x), u.y); } -// Finite-difference normal — purely fragment-shader, no vertex displacement -vec3 WaveNormal(vec3 wp, float time) { - if (fragNormal.y < 0.5) return fragNormal; // side faces keep flat normal +float SignedNoise(vec2 p) { return ValueNoise(p) * 2.0 - 1.0; } - const float d = 0.15; - float h1 = WaterHeight(wp.xz + vec2( d, 0.0), time); - float h2 = WaterHeight(wp.xz - vec2( d, 0.0), time); - float h3 = WaterHeight(wp.xz + vec2(0.0, d), time); - float h4 = WaterHeight(wp.xz - vec2(0.0, d), time); +// xyz = normal, w = slope variance removed by the filter and the far calm +vec4 WaveNormal(vec2 xz, float t, float footprint, float viewDist) { + // Slow domain warp keeps crests from lining up into periodic rows + vec2 wp = xz * WARP_SCALE + t * WARP_SPEED; + vec2 p = xz + vec2(SignedNoise(wp), SignedNoise(wp + WARP_CHANNEL_OFFSET)) * WARP_AMPLITUDE; + // Patches of swell for the long octaves and gusts for the short ones + float swell = mix(SWELL_RANGE.x, SWELL_RANGE.y, ValueNoise(xz * SWELL_SCALE + t * SWELL_DRIFT)); + float gust = mix(GUST_RANGE.x, GUST_RANGE.y, ValueNoise(xz * GUST_SCALE + t * GUST_DRIFT)); + // Distant water settles toward a mirror; the removed slope widens the glint + float calm = mix(1.0, FAR_SLOPE_SCALE, smoothstep(CALM_START, CALM_END, viewDist)); - float xd = (h2 - h1) / (2.0 * d); - float zd = (h4 - h3) / (2.0 * d); - return normalize(vec3(xd * 0.30, 1.0, zd * 0.30)); + vec2 g = vec2(0.0); + float lost = 0.0; + float wavelength = WAVE_BASE_LENGTH; + float steepness = WAVE_BASE_STEEPNESS; + for (int i = 0; i < WAVE_COUNT; ++i) { + float octave = float(i) / float(WAVE_COUNT - 1); + float s = steepness * mix(swell, gust, octave); + float fade = (1.0 - smoothstep(WAVE_FADE_START, WAVE_FADE_END, footprint / wavelength)) * calm; + lost += s * s * (1.0 - fade * fade); + if (fade > 0.0) { + float spread = mix(WIND_SPREAD_LONG, WIND_SPREAD_SHORT, octave); + float angle = WIND_ANGLE + spread * (2.0 * Hash12(vec2(i, WAVE_DIR_SEED)) - 1.0); + vec2 dir = vec2(cos(angle), sin(angle)); + float k = TWO_PI / wavelength; + float omega = sqrt(GRAVITY * k) * WAVE_SPEED_SCALE; + float phase = Hash12(vec2(i, WAVE_PHASE_SEED)) * TWO_PI; + g += dir * (s * fade * cos(dot(dir, p) * k - omega * t + phase)); + } + float jitter = 2.0 * Hash12(vec2(i, WAVE_LENGTH_SEED)) - 1.0; + wavelength *= WAVE_LENGTH_RATIO * (1.0 + WAVE_LENGTH_JITTER * jitter); + steepness *= WAVE_STEEPNESS_DECAY; + } + // Mean of cos^2 is one half + return vec4(normalize(vec3(-g.x, 1.0, -g.y)), 0.5 * lost); } -// ============================================================ -// Sky reflection — matches game sky gradient, tinted by ambient -// ============================================================ -vec3 SampleSky(vec3 dir, vec3 sunDir, vec3 sunColor, vec3 amb) { - float y = max(dir.y, 0.0); - vec3 horiz = mix(vec3(0.70, 0.82, 0.95), amb, 0.30); - vec3 zenith = mix(vec3(0.22, 0.42, 0.82), amb * 0.65, 0.25); - vec3 sky = mix(horiz, zenith, pow(y, 0.45)); +float FresnelSchlick(float c) { + return FRESNEL_F0 + (1.0 - FRESNEL_F0) * pow(clamp(1.0 - c, 0.0, 1.0), 5.0); +} + +vec3 SkyColor(vec3 dir, float lod, vec3 sunDir, vec3 sunCol, vec3 amb) { + dir = normalize(vec3(dir.x, max(dir.y, SKY_MIN_Y), dir.z)); + if (IsSet(uSceneFlags.y)) return min(SkyRadiance(dir, lod), vec3(SKY_MAX_RADIANCE)); + vec3 horiz = mix(SKY_HORIZON, amb, SKY_HORIZON_TINT); + vec3 zenith = mix(SKY_ZENITH, amb * SKY_ZENITH_DIM, SKY_ZENITH_TINT); + vec3 sky = mix(horiz, zenith, pow(dir.y, SKY_GRADIENT_POW)); float sd = max(dot(dir, sunDir), 0.0); - sky += sunColor * pow(sd, 500.0) * 5.0 + sunColor * pow(sd, 60.0) * 0.5; - if (dir.y < 0.0) sky = mix(horiz * 0.5, vec3(0.02, 0.04, 0.08), clamp(-dir.y * 4.0, 0.0, 1.0)); - return sky; + return sky + sunCol * pow(sd, SKY_SUN_POW) * SKY_SUN_GAIN; } -// ============================================================ -// Helpers -// ============================================================ -vec3 ACESFilm(vec3 x) { return clamp((x*(2.51*x+0.03))/(x*(2.43*x+0.59)+0.14), vec3(0), vec3(1)); } -float FresnelSchlick(float c) { return 0.02 + 0.98 * pow(clamp(1.0 - c, 0.0, 1.0), 5.0); } -float GGX(float NdotH, float r) { float a=r*r*r*r; float d=NdotH*NdotH*(a-1.0)+1.0; return a/(3.14159265*d*d); } - -// Sky-matched gradient fog + exponential height fog. Mirrors the ApplyFog -// helper in lighting_pass.frag so water blends seamlessly into the fogged sky. -vec3 ApplyFog(vec3 color, vec3 worldPos) { - if (uFogEnd <= 0.0) return color; - vec3 toFrag = worldPos - uCameraPos.xyz; - float dist = length(toFrag.xz); - float distFog = clamp((dist - uFogStart) / max(uFogEnd - uFogStart, 1e-4), 0.0, 1.0); - float heightFog = 0.0; - if (uHeightFogEnabled > 0.5) { - float h = max(uHeightFogTop - worldPos.y, 0.0); - heightFog = clamp(uHeightFogDensity * (1.0 - exp(-h * uHeightFogFalloff)), - 0.0, 1.0); +float Caustics(vec2 p, float t) { + vec2 warp = sin(p.yx * CAUSTIC_WARP_FREQ + t * CAUSTIC_WARP_SPEED) * CAUSTIC_WARP_AMOUNT; + vec2 layers = abs(sin(p * CAUSTIC_FREQ + warp + t * CAUSTIC_DRIFT)); + float a = layers.x, b = layers.y; + // Bright where both layers cross zero + return pow(clamp(1.0 - 0.5 * (a + b), 0.0, 1.0), CAUSTIC_SHARPNESS); +} + +vec3 Unfog(vec3 c, vec3 p, float fog) { + float f = min(fog, MAX_UNFOG); + return max((c - FogColorAt(p) * f) / (1.0 - f), vec3(0.0)); +} + +float LinearDepth(float depth) { + float n = uSceneCamera.z, f = uSceneCamera.w; + return n * f / max(f - DepthToNdc(depth) * (f - n), EPSILON); +} + +bool OnScreen(vec2 uv) { + return all(greaterThan(uv, vec2(0.0))) && all(lessThan(uv, vec2(1.0))); +} + +// rgb = scene color at the hit, a = confidence +vec4 TraceReflection(vec3 origin, vec3 dir) { + float prevT = 0.0; + float t = SSR_START; + for (int i = 0; i < SSR_STEPS; i += SSR_BATCH) { + // Issue a batch of depth fetches before testing any of them + float ts[SSR_BATCH]; + vec4 clips[SSR_BATCH]; + vec2 uvs[SSR_BATCH]; + float depths[SSR_BATCH]; + for (int j = 0; j < SSR_BATCH; ++j) { + ts[j] = t; + clips[j] = uViewProj * vec4(origin + dir * t, 1.0); + uvs[j] = ProjectToScreen(clips[j]).xy; + depths[j] = textureLod(sceneDepth, uvs[j], 0.0).r; + t = min(t * SSR_GROWTH + SSR_STEP_ADD, SSR_MAX_DIST); + } + for (int j = 0; j < SSR_BATCH; ++j) { + if (clips[j].w <= uSceneCamera.z || !OnScreen(uvs[j])) return vec4(0.0); + float diff = clips[j].w - LinearDepth(depths[j]); + float tolerance = max(SSR_MIN_THICKNESS, (ts[j] - prevT) * SSR_THICKNESS_STEPS); + if (depths[j] < 1.0 && diff > 0.0 && diff < tolerance) { + float lo = prevT, hi = ts[j]; + vec2 uv = uvs[j]; + for (int k = 0; k < SSR_REFINE_STEPS; ++k) { + float mid = 0.5 * (lo + hi); + vec4 c = uViewProj * vec4(origin + dir * mid, 1.0); + vec3 m = ProjectToScreen(c); + float md = textureLod(sceneDepth, m.xy, 0.0).r; + if (c.w > LinearDepth(md)) { hi = mid; uv = m.xy; } else { lo = mid; } + } + vec2 edge = min(uv, 1.0 - uv); + float conf = smoothstep(0.0, SSR_EDGE_FADE, min(edge.x, edge.y)) + * (1.0 - smoothstep(SSR_DIST_FADE_START * SSR_MAX_DIST, SSR_MAX_DIST, hi)); + return vec4(textureLod(sceneColor, uv, 0.0).rgb, conf); + } + prevT = ts[j]; + } } - float fogAmount = 1.0 - (1.0 - distFog) * (1.0 - heightFog); - vec3 viewDir = normalize(toFrag); - float t = smoothstep(0.0, 1.0, clamp(viewDir.y * 0.5 + 0.5, 0.0, 1.0)); - vec3 fogColor = mix(uFogColor.rgb, uFogZenithColor.rgb, t); - return mix(color, fogColor, fogAmount); + return vec4(0.0); } -// ============================================================ -// Main -// ============================================================ -void main() { - float time = fragTime; - vec3 N = WaveNormal(fragWorldPos, time); - vec3 V = normalize(uCameraPos.xyz - fragWorldPos); - vec3 sunDir = normalize(-uLightDir.xyz); - vec3 sunCol = uLightColor.rgb * uLightColor.a; - vec3 amb = uAmbient.rgb * uAmbient.a; - - float NdotV = max(dot(N, V), 0.001); +struct WaterResult { + vec3 color; // composited water, linear, before fog + vec3 backdrop; // unrefracted scene behind the surface, as stored + float edge; // 0 at the contact line, 1 once the water has depth +}; + +WaterResult ShadeWater(vec3 P, vec3 Nface, float t, float footprint) { + WaterResult r; + vec3 sunDir = SunDirection(); + vec3 sunCol = SunRadiance(); + vec3 amb = AmbientRadiance(); + vec3 toCam = CameraPosition() - P; + vec3 V = normalize(toCam); + bool top = Nface.y > TOP_FACE_MIN_Y; + bool below = dot(Nface, V) < 0.0; + bool scene = IsSet(uSceneFlags.x); + + // Waves + vec4 wave = top ? WaveNormal(P.xz, t, footprint, length(toCam)) : vec4(Nface, 0.0); + vec3 N = below ? -wave.xyz : wave.xyz; + float a2 = BASE_ROUGHNESS * BASE_ROUGHNESS + wave.w; + + float NdotV = max(dot(N, V), 0.0); float fresnel = FresnelSchlick(NdotV); - // ---- Shadow & diffuse ---- - float shadow = CalcShadow(fragShadowCoord); - shadow *= smoothstep(-0.1, 0.2, dot(N, sunDir)); - float NdotL = clamp(dot(N, sunDir) * 0.65 + 0.35, 0.0, 1.0); - vec3 diffuse = sunCol * NdotL * shadow; - - // ---- BSL water color: R=64 G=160 B=255 I=0.35, then squared ---- - // waterColorSqrt = vec3(64,160,255)/255 * 0.35 → squared → *3 to lift brightness - vec3 wcSqrt = vec3(64.0, 160.0, 255.0) / 255.0 * 0.35; - vec3 wColor = wcSqrt * wcSqrt * 3.0; // ≈ (0.023, 0.145, 0.368) - vec3 waterBody = wColor * (amb * 1.8 + diffuse * 0.9); - - // Subsurface forward scatter (light passing through shallow water) - float sss = pow(max(dot(V, -sunDir), 0.0), 4.0) * (1.0 - NdotV) * 0.35; - waterBody += vec3(0.0, 0.08, 0.14) * sunCol * sss; - - // ---- Sky reflection ---- - vec3 R = reflect(-V, N); - vec3 refl = SampleSky(R, sunDir, sunCol, amb); - - // ---- GGX specular ---- - vec3 H = normalize(V + sunDir); - float NdotH = max(dot(N, H), 0.0); - float spec = GGX(NdotH, 0.07) * fresnel * shadow; - vec3 specC = sunCol * min(spec, 40.0); - - // ---- Compose: water body at normal incidence, sky at grazing ---- - vec3 finalColor = mix(waterBody, refl, fresnel) + specC; - - // ---- Caustic shimmer (2-layer, power-curved — matches GL/DX) ---- - if (fragNormal.y > 0.5) { - vec2 cp = fragWorldPos.xz * 0.65; - float t = time * 0.9; - float c = sin(cp.x*2.7+t) * sin(cp.y*2.7-t*0.75) * 0.5 + 0.5; - c += sin(cp.x*1.9-t*0.55+cp.y*1.6) * sin(cp.y*2.4+t*0.35) * 0.5 + 0.5; - c = clamp(pow(c * 0.5, 1.6) * 1.5, 0.0, 1.0); - finalColor += sunCol * c * 0.05 * shadow; + // Sun visibility at the surface + float NdotS = dot(N, sunDir); + float sunVis = 0.0; + if (!below && NdotS > SUN_SHADOW_FADE.x) + sunVis = CalcShadow(fragShadowCoord) * smoothstep(SUN_SHADOW_FADE.x, SUN_SHADOW_FADE.y, NdotS); + vec3 lightIn = amb + sunCol * (sunVis * max(sunDir.y, 0.0)); + vec3 inscatter = SCATTER_ALBEDO * lightIn; + + // Scene behind the surface + vec2 uv0 = gl_FragCoord.xy * uScreenSize.zw; + vec3 backdrop = vec3(0.0); + vec3 under = inscatter; + float depth0 = ABSENT_DEPTH; + if (scene) { + float d0 = textureLod(sceneDepth, uv0, 0.0).r; + vec3 B0 = Reconstruct(uv0, d0); + backdrop = textureLod(sceneColor, uv0, 0.0).rgb; + if (d0 < 1.0) depth0 = top ? max(P.y - B0.y, 0.0) : distance(P, B0); + + // Refraction: bend the bottom lookup by the wave slope, keep it behind the surface + vec2 uv1 = uv0; + vec3 B1 = B0; + float d1 = d0; + if (top && !below && d0 < 1.0) { + vec3 bent = B0 + vec3(N.x, 0.0, N.z) * (REFRACT_SCALE * min(depth0, REFRACT_MAX_DEPTH)); + vec4 bc = uViewProj * vec4(bent, 1.0); + if (bc.w > uSceneCamera.z) { + vec3 s = ProjectToScreen(bc); + float sd = textureLod(sceneDepth, s.xy, 0.0).r; + float surfZ = (uViewProj * vec4(P, 1.0)).w; + if (OnScreen(s.xy) && LinearDepth(sd) > surfZ + REFRACT_DEPTH_BIAS) { + uv1 = s.xy; + d1 = sd; + B1 = Reconstruct(uv1, sd); + } + } + } + float depth1 = d1 < 1.0 ? (top ? max(P.y - B1.y, 0.0) : distance(P, B1)) : ABSENT_DEPTH; + + // Beer-Lambert along the refracted view ray plus the sunlight's way down + float cosV = clamp(abs(dot(Nface, V)), 0.0, 1.0); + float cosT = sqrt(1.0 - (1.0 - cosV * cosV) / (WATER_IOR * WATER_IOR)); + float path = below ? length(toCam) : depth1 * (1.0 / max(cosT, MIN_TRANSMIT_COS) + 1.0); + vec3 trans = exp(-SIGMA_T * path); + + // Scattering softens the bottom with depth + vec3 bottom = textureLod(sceneColor, uv1, 0.0).rgb; + float blurPx = clamp(depth1 * BLUR_PER_DEPTH / max(footprint, EPSILON), 0.0, BLUR_MAX_PX); + if (blurPx > BLUR_MIN_PX) { + vec2 radius = blurPx * uScreenSize.zw; + for (int i = 0; i < BLUR_TAPS; ++i) { + float r = sqrt((float(i) + VOGEL_RING_OFFSET) / float(BLUR_TAPS)); + float a = float(i) * GOLDEN_ANGLE; + bottom += textureLod(sceneColor, uv1 + vec2(cos(a), sin(a)) * r * radius, 0.0).rgb; + } + bottom /= float(BLUR_TAPS + 1); + } + float bottomFog = FogAmount(B1); + bottom = SceneToLinear(Unfog(bottom, B1, bottomFog)); + if (top && !below && d1 < 1.0) { + vec2 cp = B1.xz + sunDir.xz * (depth1 / max(sunDir.y, MIN_SUN_ELEVATION)); + float c = Caustics(cp * CAUSTIC_SCALE, t); + bottom *= 1.0 + c * CAUSTIC_STRENGTH * sunVis * exp(-CAUSTIC_FALLOFF * depth1) + * smoothstep(CAUSTIC_FADE_IN.x, CAUSTIC_FADE_IN.y, depth1); + } + // A heavily fogged bottom carries no usable color; it sinks into the water body + float bottomTrust = 1.0 - smoothstep(BOTTOM_TRUST_FOG, MAX_UNFOG, bottomFog); + under = inscatter + (bottom - inscatter) * trans * bottomTrust; + } + + // Reflection: screen-space hit over the sky cubemap + vec3 R = reflect(-V, N); + vec3 refl = SkyColor(R, sqrt(wave.w) * SKY_LOD_PER_SLOPE, sunDir, sunCol, amb); + if (scene && !below && fresnel > SSR_MIN_FRESNEL) { + vec3 dir = normalize(vec3(R.x, max(R.y, SSR_MIN_DIR_Y), R.z)); + vec4 hit = TraceReflection(P + N * SSR_ORIGIN_OFFSET, dir); + refl = mix(refl, SceneToLinear(hit.rgb), hit.a); + } + if (below) { + refl = inscatter; + fresnel = max(fresnel, 1.0 - smoothstep(TIR_COS - TIR_SOFTEN, TIR_COS + TIR_SOFTEN, NdotV)); + } + + // Sun glint (GGX, widened by the filtered-out wave slopes) + vec3 spec = vec3(0.0); + if (!below && NdotS > 0.0) { + vec3 H = normalize(V + sunDir); + float NdotH = max(dot(N, H), 0.0); + float dd = NdotH * NdotH * (a2 - 1.0) + 1.0; + float D = a2 / (PI * dd * dd); + float smithV = NdotS * sqrt(NdotV * NdotV * (1.0 - a2) + a2); + float smithL = NdotV * sqrt(NdotS * NdotS * (1.0 - a2) + a2); + float vis = 0.5 / max(smithV + smithL, EPSILON); + float F = FresnelSchlick(max(dot(V, H), 0.0)); + spec = sunCol * min(D * vis * F * NdotS * sunVis, SPEC_MAX); + } + + vec3 color = mix(under, refl, fresnel) + spec; + + // Shoreline foam + float foam = 0.0; + if (scene && top && !below && depth0 < FOAM_MAX_DEPTH) { + float shore = 1.0 - smoothstep(0.0, FOAM_BAND, depth0); + vec2 fp = P.xz * FOAM_SCALE + FOAM_DRIFT * t; + float n = mix(ValueNoise(fp), ValueNoise(fp * FOAM_DETAIL_SCALE + FOAM_DETAIL_OFFSET), + FOAM_DETAIL_WEIGHT); + foam = smoothstep(FOAM_THRESHOLD.x, FOAM_THRESHOLD.y, + n * FOAM_NOISE_WEIGHT + shore * FOAM_SHORE_WEIGHT) * shore * FOAM_OPACITY; + color = mix(color, FOAM_ALBEDO * lightIn, foam); } - // ---- ACES + fog ---- - finalColor = ACESFilm(finalColor); - finalColor = ApplyFog(finalColor, fragWorldPos); + r.color = color; + r.backdrop = backdrop; + r.edge = (scene && !below) ? max(smoothstep(0.0, SHORE_FADE_DEPTH, depth0), foam) : 1.0; + return r; +} - // ---- Alpha: BSL waterAlpha=0.70 base, Fresnel boosts edges to ~0.95 ---- - float alpha = mix(0.72, 0.97, pow(clamp(1.0 - NdotV, 0.0, 1.0), 2.0)); +// Opacity of the plain blended fallback when the scene cannot be read +#define FALLBACK_ALPHA vec2(0.80, 0.97) +#define FALLBACK_ALPHA_POW 2.0 + +float FallbackAlpha(vec3 P, vec3 Nface) { + float NdotV = abs(dot(Nface, normalize(CameraPosition() - P))); + return mix(FALLBACK_ALPHA.x, FALLBACK_ALPHA.y, pow(1.0 - NdotV, FALLBACK_ALPHA_POW)); +} + +// ==== END SHARED WATER BODY ==== + +#define MAX_HDR 1000.0 + +void main() { + vec3 P = fragWorldPos; + float footprint = max(length(dFdx(P.xz)), length(dFdy(P.xz))); + WaterResult w = ShadeWater(P, fragNormal, fragTime, footprint); - outColor = vec4(finalColor, alpha); + vec3 color = mix(w.color, FogColorAt(P), FogAmount(P)); + float alpha = 1.0; + if (IsSet(uSceneFlags.x)) + color = mix(w.backdrop, color, w.edge); + else + alpha = FallbackAlpha(P, fragNormal); + outColor = vec4(clamp(color, vec3(0.0), vec3(MAX_HDR)), alpha); } diff --git a/Game/assets/shaders/water_shader.frag.spv b/Game/assets/shaders/water_shader.frag.spv index d1058da..425cc8f 100644 Binary files a/Game/assets/shaders/water_shader.frag.spv and b/Game/assets/shaders/water_shader.frag.spv differ diff --git a/Game/assets/shaders/water_shader.hlsl b/Game/assets/shaders/water_shader.hlsl index 1d23ebf..d2b97c0 100644 --- a/Game/assets/shaders/water_shader.hlsl +++ b/Game/assets/shaders/water_shader.hlsl @@ -69,38 +69,111 @@ float CalcShadow(float4 sc, float2 sp) { return lerp(1.0, s/16.0, ShadowStrength * fc.x * fc.y); } -// ---- Wave height field ---- -float WaterHeight(float2 xz, float time) { - float2 w1 = float2( time*0.45, time*0.30); - float2 w2 = float2(-time*0.35, time*0.50); - float h = 0.0; - h += sin(xz.x*0.24+w1.x) * sin(xz.y*0.19+w1.y); - h += sin(xz.x*0.33+w2.x-xz.y*0.11) * 0.65; - h += sin(xz.x*0.80+w1.x*1.9+xz.y*0.58) * 0.40; - h += sin(xz.x*1.10-w2.x*2.1) * sin(xz.y*0.88+w2.y*1.4) * 0.35; - h += sin(xz.x*2.20+w1.x*3.3) * sin(xz.y*1.85-w1.y*2.7) * 0.15; - h += sin(xz.x*3.00-w2.x*4.2+xz.y*2.30) * 0.10; - return h; +// ---- Waves (matches water_shader.frag): octaves derived from a few +// parameters, each faded out once its wavelength drops under a few pixels ---- +#define PI 3.14159265 +#define TWO_PI (2.0 * PI) +#define GRAVITY 9.81 // m/s^2 +#define WAVE_COUNT 12 +#define WAVE_BASE_LENGTH 13.0 // m, longest octave +#define WAVE_LENGTH_RATIO (1.0 / sqrt(2.0)) // irrational octave spacing +#define WAVE_LENGTH_JITTER 0.12 // +- fraction added to each octave step +#define WAVE_BASE_STEEPNESS 0.075 // slope amplitude of the longest octave +#define WAVE_STEEPNESS_DECAY 0.8946 // per-octave steepness falloff +#define WIND_ANGLE 0.2 // rad, mean travel direction in xz +#define WIND_SPREAD_LONG 0.9 // rad, long octaves stay nearer the wind +#define WIND_SPREAD_SHORT 1.4 // rad, short chop spreads wider +#define WAVE_DIR_SEED 17.0 // hash streams per octave index +#define WAVE_PHASE_SEED 31.0 +#define WAVE_LENGTH_SEED 47.0 +#define WAVE_SPEED_SCALE 0.55 // fraction of deep-water phase speed +#define WAVE_FADE_START 0.08 // pixel footprint / wavelength +#define WAVE_FADE_END 0.25 // 4 pixels per wavelength +#define TOP_FACE_MIN_Y 0.5 // normal.y above which a face is the water top + +// ---- Wave field variation ---- +#define WARP_SCALE (1.0 / 55.0) // 1/m, domain warp noise +#define WARP_AMPLITUDE 2.5 // m the wave coordinates bend +#define WARP_SPEED 0.03 // noise units per second +#define WARP_CHANNEL_OFFSET float2(19.3, 7.7) // decorrelates warp x and y +#define SWELL_SCALE (1.0 / 90.0) // 1/m, long-octave amplitude patches +#define SWELL_RANGE float2(0.65, 1.15) // min, max amplitude +#define SWELL_DRIFT float2(0.011, 0.007) // noise units per second +#define GUST_SCALE (1.0 / 24.0) // 1/m, short-octave gust patches +#define GUST_RANGE float2(0.35, 1.3) +#define GUST_DRIFT float2(-0.03, 0.02) +#define CALM_START 30.0 // m from the camera where the surface settles +#define CALM_END 100.0 +#define FAR_SLOPE_SCALE 0.35 // remaining wave slope far away + +// ---- Hash (Dave Hoskins, hash without sine) ---- +#define HASH_SCALE 0.1031 +#define HASH_OFFSET 33.33 + +float Hash12(float2 p) { + float3 p3 = frac(float3(p.xyx) * HASH_SCALE); + p3 += dot(p3, p3.yzx + HASH_OFFSET); + return frac((p3.x + p3.y) * p3.z); } -float3 WaveNormal(float3 wp, float3 origNorm, float time) { - if (origNorm.y < 0.5) return origNorm; - float d = 0.15; - float xd = (WaterHeight(wp.xz - float2(d,0),time) - WaterHeight(wp.xz + float2(d,0),time)) / (2.0*d); - float zd = (WaterHeight(wp.xz - float2(0,d),time) - WaterHeight(wp.xz + float2(0,d),time)) / (2.0*d); - return normalize(float3(xd*0.30, 1.0, zd*0.30)); +float ValueNoise(float2 p) { + float2 i = floor(p), f = frac(p); + float2 u = f * f * (3.0 - 2.0 * f); + return lerp(lerp(Hash12(i), Hash12(i + float2(1.0, 0.0)), u.x), + lerp(Hash12(i + float2(0.0, 1.0)), Hash12(i + float2(1.0, 1.0)), u.x), u.y); } +float SignedNoise(float2 p) { return ValueNoise(p) * 2.0 - 1.0; } + +// xyz = normal, w = slope variance removed by the filter and the far calm +float4 WaveNormal(float3 wp, float3 origNorm, float time, float footprint, float viewDist) { + if (origNorm.y < TOP_FACE_MIN_Y) return float4(origNorm, 0.0); + float2 xz = wp.xz; + // Slow domain warp keeps crests from lining up into periodic rows + float2 wn = xz * WARP_SCALE + time * WARP_SPEED; + float2 p = xz + float2(SignedNoise(wn), SignedNoise(wn + WARP_CHANNEL_OFFSET)) * WARP_AMPLITUDE; + // Patches of swell for the long octaves and gusts for the short ones + float swell = lerp(SWELL_RANGE.x, SWELL_RANGE.y, ValueNoise(xz * SWELL_SCALE + time * SWELL_DRIFT)); + float gust = lerp(GUST_RANGE.x, GUST_RANGE.y, ValueNoise(xz * GUST_SCALE + time * GUST_DRIFT)); + // Distant water settles toward a mirror; the removed slope widens the glint + float calm = lerp(1.0, FAR_SLOPE_SCALE, smoothstep(CALM_START, CALM_END, viewDist)); + + float2 g = float2(0.0, 0.0); + float lost = 0.0; + float wavelength = WAVE_BASE_LENGTH; + float steepness = WAVE_BASE_STEEPNESS; + [unroll] for (int k = 0; k < WAVE_COUNT; k++) { + float octave = (float)k / (float)(WAVE_COUNT - 1); + float s = steepness * lerp(swell, gust, octave); + float fade = (1.0 - smoothstep(WAVE_FADE_START, WAVE_FADE_END, footprint / wavelength)) * calm; + lost += s * s * (1.0 - fade * fade); + float spread = lerp(WIND_SPREAD_LONG, WIND_SPREAD_SHORT, octave); + float angle = WIND_ANGLE + spread * (2.0 * Hash12(float2(k, WAVE_DIR_SEED)) - 1.0); + float2 dir = float2(cos(angle), sin(angle)); + float wavenumber = TWO_PI / wavelength; + float omega = sqrt(GRAVITY * wavenumber) * WAVE_SPEED_SCALE; + float phase = Hash12(float2(k, WAVE_PHASE_SEED)) * TWO_PI; + g += dir * (s * fade * cos(dot(dir, p) * wavenumber - omega * time + phase)); + float jitter = 2.0 * Hash12(float2(k, WAVE_LENGTH_SEED)) - 1.0; + wavelength *= WAVE_LENGTH_RATIO * (1.0 + WAVE_LENGTH_JITTER * jitter); + steepness *= WAVE_STEEPNESS_DECAY; + } + // Mean of cos^2 is one half + return float4(normalize(float3(-g.x, 1.0, -g.y)), 0.5 * lost); +} + +// ---- Sky: directions below the horizon reuse the horizon, never black ---- +#define SKY_MIN_Y 0.07 -// ---- Sky ---- float3 SampleSky(float3 dir, float3 sunDir, float3 sunCol, float3 amb) { - float y = max(dir.y, 0.0); + dir = normalize(float3(dir.x, max(dir.y, SKY_MIN_Y), dir.z)); float3 horiz = lerp(float3(0.70,0.82,0.95), amb, 0.30); float3 zenith = lerp(float3(0.22,0.42,0.82), amb*0.65, 0.25); - float3 sky = lerp(horiz, zenith, pow(y, 0.45)); + float3 sky = lerp(horiz, zenith, pow(dir.y, 0.45)); float sd = max(dot(dir,sunDir), 0.0); sky += sunCol*pow(sd,500.0)*5.0 + sunCol*pow(sd,60.0)*0.5; - if (dir.y < 0.0) sky = lerp(horiz*0.5, float3(0.02,0.04,0.08), saturate(-dir.y*4.0)); return sky; } +#define SUN_ROUGHNESS 0.07 + float3 ACESFilm(float3 x) { return saturate((x*(2.51*x+0.03))/(x*(2.43*x+0.59)+0.14)); } float Fresnel(float c) { return 0.02 + 0.98*pow(saturate(1.0-c),5.0); } float GGX(float NdotH, float r) { float a=r*r*r*r; float d=NdotH*NdotH*(a-1.0)+1.0; return a/(3.14159265*d*d); } @@ -139,7 +212,9 @@ VS_OUTPUT VS_Main(VS_INPUT i) { // ---- PS ---- float4 PS_Main(VS_OUTPUT i) : SV_Target { float time = i.Time; - float3 N = WaveNormal(i.WorldPos, i.Normal, time); + float footprint = max(length(ddx(i.WorldPos.xz)), length(ddy(i.WorldPos.xz))); + float4 wave = WaveNormal(i.WorldPos, i.Normal, time, footprint, length(CameraPos - i.WorldPos)); + float3 N = wave.xyz; float3 V = normalize(CameraPos - i.WorldPos); float3 sunDir = float3(0,1,0), sunCol = float3(1,1,1); @@ -166,7 +241,8 @@ float4 PS_Main(VS_OUTPUT i) : SV_Target { float3 refl = SampleSky(R, sunDir, sunCol, amb); float NdotH = max(dot(N,normalize(V+sunDir)),0.0); - float spec = GGX(NdotH,0.07)*fresnel*shadow; + // GGX here takes r with alpha = r^2; filtered wave slopes add to alpha^2 + float spec = GGX(NdotH, pow(pow(SUN_ROUGHNESS, 4.0) + wave.w, 0.25))*fresnel*shadow; float3 specC = sunCol * min(spec, 40.0); float3 final = lerp(waterBody, refl, fresnel) + specC; diff --git a/Game/assets/shaders/water_shader.vert b/Game/assets/shaders/water_shader.vert index 2308dd4..6bbb803 100644 --- a/Game/assets/shaders/water_shader.vert +++ b/Game/assets/shaders/water_shader.vert @@ -29,9 +29,17 @@ layout(set = 2, binding = 0) uniform ShadowLightUBO { vec2 _fogPad; // std140: must be vec2 (8B) not float[2] (stride-16 = 32B) }; +// Scene read block (VertexLayoutDesc::transparentReadsScene) +layout(set = 3, binding = 5) uniform SceneReadUBO { + mat4 uViewProj; + mat4 uInvViewProj; + vec4 uScreenSize; + vec4 uSceneCamera; // xy = TAA jitter in NDC, z = near, w = far + vec4 uSceneFlags; +}; + layout(location = 0) out vec3 fragWorldPos; layout(location = 1) out vec3 fragNormal; -layout(location = 2) out vec4 fragColor; layout(location = 3) out vec4 fragShadowCoord; layout(location = 4) out float fragTime; @@ -40,10 +48,11 @@ void main() { // via procedural normal mapping (BSL approach). gl_Position = WVP * vec4(inPosition, 1.0); gl_Position.y = -gl_Position.y; // Vulkan NDC y-flip + // Same sub-pixel jitter as the opaque GBuffer geometry + gl_Position.xy += uSceneCamera.xy * gl_Position.w; fragWorldPos = inPosition; fragNormal = inNormal; - fragColor = inColor; fragTime = uCameraPos.w; // scene clock stashed in cam.w by LightManager float normalBias = uLightDir.w; diff --git a/Game/assets/shaders/water_shader.vert.spv b/Game/assets/shaders/water_shader.vert.spv index dda3d08..73123eb 100644 Binary files a/Game/assets/shaders/water_shader.vert.spv and b/Game/assets/shaders/water_shader.vert.spv differ diff --git a/Game/assets/shaders/water_shader_dx12.hlsl b/Game/assets/shaders/water_shader_dx12.hlsl index 4c648ab..ec6f702 100644 --- a/Game/assets/shaders/water_shader_dx12.hlsl +++ b/Game/assets/shaders/water_shader_dx12.hlsl @@ -69,38 +69,111 @@ float CalcShadow(float4 sc, float2 sp) { return lerp(1.0, s/16.0, ShadowStrength * fc.x * fc.y); } -// ---- Wave height field ---- -float WaterHeight(float2 xz, float time) { - float2 w1 = float2( time*0.45, time*0.30); - float2 w2 = float2(-time*0.35, time*0.50); - float h = 0.0; - h += sin(xz.x*0.24+w1.x) * sin(xz.y*0.19+w1.y); - h += sin(xz.x*0.33+w2.x-xz.y*0.11) * 0.65; - h += sin(xz.x*0.80+w1.x*1.9+xz.y*0.58) * 0.40; - h += sin(xz.x*1.10-w2.x*2.1) * sin(xz.y*0.88+w2.y*1.4) * 0.35; - h += sin(xz.x*2.20+w1.x*3.3) * sin(xz.y*1.85-w1.y*2.7) * 0.15; - h += sin(xz.x*3.00-w2.x*4.2+xz.y*2.30) * 0.10; - return h; +// ---- Waves (matches water_shader.frag): octaves derived from a few +// parameters, each faded out once its wavelength drops under a few pixels ---- +#define PI 3.14159265 +#define TWO_PI (2.0 * PI) +#define GRAVITY 9.81 // m/s^2 +#define WAVE_COUNT 12 +#define WAVE_BASE_LENGTH 13.0 // m, longest octave +#define WAVE_LENGTH_RATIO (1.0 / sqrt(2.0)) // irrational octave spacing +#define WAVE_LENGTH_JITTER 0.12 // +- fraction added to each octave step +#define WAVE_BASE_STEEPNESS 0.075 // slope amplitude of the longest octave +#define WAVE_STEEPNESS_DECAY 0.8946 // per-octave steepness falloff +#define WIND_ANGLE 0.2 // rad, mean travel direction in xz +#define WIND_SPREAD_LONG 0.9 // rad, long octaves stay nearer the wind +#define WIND_SPREAD_SHORT 1.4 // rad, short chop spreads wider +#define WAVE_DIR_SEED 17.0 // hash streams per octave index +#define WAVE_PHASE_SEED 31.0 +#define WAVE_LENGTH_SEED 47.0 +#define WAVE_SPEED_SCALE 0.55 // fraction of deep-water phase speed +#define WAVE_FADE_START 0.08 // pixel footprint / wavelength +#define WAVE_FADE_END 0.25 // 4 pixels per wavelength +#define TOP_FACE_MIN_Y 0.5 // normal.y above which a face is the water top + +// ---- Wave field variation ---- +#define WARP_SCALE (1.0 / 55.0) // 1/m, domain warp noise +#define WARP_AMPLITUDE 2.5 // m the wave coordinates bend +#define WARP_SPEED 0.03 // noise units per second +#define WARP_CHANNEL_OFFSET float2(19.3, 7.7) // decorrelates warp x and y +#define SWELL_SCALE (1.0 / 90.0) // 1/m, long-octave amplitude patches +#define SWELL_RANGE float2(0.65, 1.15) // min, max amplitude +#define SWELL_DRIFT float2(0.011, 0.007) // noise units per second +#define GUST_SCALE (1.0 / 24.0) // 1/m, short-octave gust patches +#define GUST_RANGE float2(0.35, 1.3) +#define GUST_DRIFT float2(-0.03, 0.02) +#define CALM_START 30.0 // m from the camera where the surface settles +#define CALM_END 100.0 +#define FAR_SLOPE_SCALE 0.35 // remaining wave slope far away + +// ---- Hash (Dave Hoskins, hash without sine) ---- +#define HASH_SCALE 0.1031 +#define HASH_OFFSET 33.33 + +float Hash12(float2 p) { + float3 p3 = frac(float3(p.xyx) * HASH_SCALE); + p3 += dot(p3, p3.yzx + HASH_OFFSET); + return frac((p3.x + p3.y) * p3.z); } -float3 WaveNormal(float3 wp, float3 origNorm, float time) { - if (origNorm.y < 0.5) return origNorm; - float d = 0.15; - float xd = (WaterHeight(wp.xz - float2(d,0),time) - WaterHeight(wp.xz + float2(d,0),time)) / (2.0*d); - float zd = (WaterHeight(wp.xz - float2(0,d),time) - WaterHeight(wp.xz + float2(0,d),time)) / (2.0*d); - return normalize(float3(xd*0.30, 1.0, zd*0.30)); +float ValueNoise(float2 p) { + float2 i = floor(p), f = frac(p); + float2 u = f * f * (3.0 - 2.0 * f); + return lerp(lerp(Hash12(i), Hash12(i + float2(1.0, 0.0)), u.x), + lerp(Hash12(i + float2(0.0, 1.0)), Hash12(i + float2(1.0, 1.0)), u.x), u.y); } +float SignedNoise(float2 p) { return ValueNoise(p) * 2.0 - 1.0; } + +// xyz = normal, w = slope variance removed by the filter and the far calm +float4 WaveNormal(float3 wp, float3 origNorm, float time, float footprint, float viewDist) { + if (origNorm.y < TOP_FACE_MIN_Y) return float4(origNorm, 0.0); + float2 xz = wp.xz; + // Slow domain warp keeps crests from lining up into periodic rows + float2 wn = xz * WARP_SCALE + time * WARP_SPEED; + float2 p = xz + float2(SignedNoise(wn), SignedNoise(wn + WARP_CHANNEL_OFFSET)) * WARP_AMPLITUDE; + // Patches of swell for the long octaves and gusts for the short ones + float swell = lerp(SWELL_RANGE.x, SWELL_RANGE.y, ValueNoise(xz * SWELL_SCALE + time * SWELL_DRIFT)); + float gust = lerp(GUST_RANGE.x, GUST_RANGE.y, ValueNoise(xz * GUST_SCALE + time * GUST_DRIFT)); + // Distant water settles toward a mirror; the removed slope widens the glint + float calm = lerp(1.0, FAR_SLOPE_SCALE, smoothstep(CALM_START, CALM_END, viewDist)); + + float2 g = float2(0.0, 0.0); + float lost = 0.0; + float wavelength = WAVE_BASE_LENGTH; + float steepness = WAVE_BASE_STEEPNESS; + [unroll] for (int k = 0; k < WAVE_COUNT; k++) { + float octave = (float)k / (float)(WAVE_COUNT - 1); + float s = steepness * lerp(swell, gust, octave); + float fade = (1.0 - smoothstep(WAVE_FADE_START, WAVE_FADE_END, footprint / wavelength)) * calm; + lost += s * s * (1.0 - fade * fade); + float spread = lerp(WIND_SPREAD_LONG, WIND_SPREAD_SHORT, octave); + float angle = WIND_ANGLE + spread * (2.0 * Hash12(float2(k, WAVE_DIR_SEED)) - 1.0); + float2 dir = float2(cos(angle), sin(angle)); + float wavenumber = TWO_PI / wavelength; + float omega = sqrt(GRAVITY * wavenumber) * WAVE_SPEED_SCALE; + float phase = Hash12(float2(k, WAVE_PHASE_SEED)) * TWO_PI; + g += dir * (s * fade * cos(dot(dir, p) * wavenumber - omega * time + phase)); + float jitter = 2.0 * Hash12(float2(k, WAVE_LENGTH_SEED)) - 1.0; + wavelength *= WAVE_LENGTH_RATIO * (1.0 + WAVE_LENGTH_JITTER * jitter); + steepness *= WAVE_STEEPNESS_DECAY; + } + // Mean of cos^2 is one half + return float4(normalize(float3(-g.x, 1.0, -g.y)), 0.5 * lost); +} + +// ---- Sky: directions below the horizon reuse the horizon, never black ---- +#define SKY_MIN_Y 0.07 -// ---- Sky ---- float3 SampleSky(float3 dir, float3 sunDir, float3 sunCol, float3 amb) { - float y = max(dir.y, 0.0); + dir = normalize(float3(dir.x, max(dir.y, SKY_MIN_Y), dir.z)); float3 horiz = lerp(float3(0.70,0.82,0.95), amb, 0.30); float3 zenith = lerp(float3(0.22,0.42,0.82), amb*0.65, 0.25); - float3 sky = lerp(horiz, zenith, pow(y, 0.45)); + float3 sky = lerp(horiz, zenith, pow(dir.y, 0.45)); float sd = max(dot(dir,sunDir), 0.0); sky += sunCol*pow(sd,500.0)*5.0 + sunCol*pow(sd,60.0)*0.5; - if (dir.y < 0.0) sky = lerp(horiz*0.5, float3(0.02,0.04,0.08), saturate(-dir.y*4.0)); return sky; } +#define SUN_ROUGHNESS 0.07 + float3 ACESFilm(float3 x) { return saturate((x*(2.51*x+0.03))/(x*(2.43*x+0.59)+0.14)); } float Fresnel(float c) { return 0.02 + 0.98*pow(saturate(1.0-c),5.0); } float GGX(float NdotH, float r) { float a=r*r*r*r; float d=NdotH*NdotH*(a-1.0)+1.0; return a/(3.14159265*d*d); } @@ -139,7 +212,9 @@ VS_OUTPUT VS_Main(VS_INPUT i) { // ---- PS ---- float4 PS_Main(VS_OUTPUT i) : SV_Target { float time = i.Time; - float3 N = WaveNormal(i.WorldPos, i.Normal, time); + float footprint = max(length(ddx(i.WorldPos.xz)), length(ddy(i.WorldPos.xz))); + float4 wave = WaveNormal(i.WorldPos, i.Normal, time, footprint, length(CameraPos.xyz - i.WorldPos)); + float3 N = wave.xyz; float3 V = normalize(CameraPos.xyz - i.WorldPos); float3 sunDir = normalize(-LightDir.xyz); @@ -164,7 +239,8 @@ float4 PS_Main(VS_OUTPUT i) : SV_Target { float3 refl = SampleSky(R, sunDir, sunCol, amb); float NdotH = max(dot(N,normalize(V+sunDir)),0.0); - float spec = GGX(NdotH,0.07)*fresnel*shadow; + // GGX here takes r with alpha = r^2; filtered wave slopes add to alpha^2 + float spec = GGX(NdotH, pow(pow(SUN_ROUGHNESS, 4.0) + wave.w, 0.25))*fresnel*shadow; float3 specC = sunCol * min(spec, 40.0); float3 final = lerp(waterBody, refl, fresnel) + specC; diff --git a/Game/assets/shaders/water_shader_gl.frag b/Game/assets/shaders/water_shader_gl.frag index 6f99269..696b055 100644 --- a/Game/assets/shaders/water_shader_gl.frag +++ b/Game/assets/shaders/water_shader_gl.frag @@ -16,17 +16,6 @@ struct LightData { float AreaWidth; float AreaHeight; }; -layout(std140, binding = 1) uniform MaterialUBO { - uint HasDiffuseMap, HasNormalMap, HasSpecularMap, HasRoughnessMap; - uint HasMetallicMap, HasAOMap, HasEmissiveMap, _matPad0; - vec4 MatDiffuseColor; - vec3 MatSpecularColor; float MatShininess; - vec3 MatEmissiveColor; float MatEmissiveIntensity; - float MatMetallic, MatRoughness, MatAO, MatNormalIntensity; - vec2 MatTiling; vec2 MatOffset; - float MatOpacity; float MatAlphaCutoff; float _matPad1, _matPad2; -}; - layout(std140, binding = 2) uniform LightUBO { vec3 CameraPos; uint NumActiveLights; @@ -56,9 +45,65 @@ layout(std140, binding = 5) uniform ShadowUBO { float _shadowPad0, _shadowPad1; }; -layout(binding = 0) uniform sampler2D diffuseTexture; layout(binding = 3) uniform sampler2DShadow shadowMap; +// Scene read (VertexLayoutDesc::transparentReadsScene) +layout(binding = 4) uniform sampler2D sceneColor; // tonemapped + fogged +layout(binding = 5) uniform sampler2D sceneDepth; +layout(binding = 6) uniform samplerCube skyCube; // HDR +layout(std140, binding = 4) uniform SceneReadUBO { + mat4 uViewProj; + mat4 uInvViewProj; + vec4 uScreenSize; // w, h, 1/w, 1/h + vec4 uSceneCamera; // xy = jitter (unused on GL), z = near, w = far + vec4 uSceneFlags; // x = scene textures valid, y = sky valid +}; + +#define EPSILON 1e-4 + +float SignedToUnit(float v) { return v * 0.5 + 0.5; } +vec2 SignedToUnit(vec2 v) { return v * 0.5 + 0.5; } +vec2 UnitToSigned(vec2 v) { return v * 2.0 - 1.0; } +bool IsSet(float flag) { return flag > 0.5; } + +// ============================================================ +// Backend conventions (GL: UV y up, window depth in [0,1]) +// ============================================================ +vec3 ProjectToScreen(vec4 clip) { + vec3 ndc = clip.xyz / clip.w; + return vec3(SignedToUnit(ndc.xy), SignedToUnit(ndc.z)); +} +float DepthToNdc(float depth) { return depth * 2.0 - 1.0; } +vec3 Reconstruct(vec2 uv, float depth) { + vec4 w = uInvViewProj * vec4(UnitToSigned(uv), DepthToNdc(depth), 1.0); + return w.xyz / w.w; +} + +// ACES fit used by the GL lighting and skybox passes: (x(Ax+B)) / (x(Cx+D)+E) +#define ACES_A 2.51 +#define ACES_B 0.03 +#define ACES_C 2.43 +#define ACES_D 0.59 +#define ACES_E 0.14 +// Largest stored value inverted; the fit only reaches 1 at infinity +#define ACES_INVERSE_MAX 0.985 + +vec3 ACESFilm(vec3 x) { + return clamp((x * (ACES_A * x + ACES_B)) / (x * (ACES_C * x + ACES_D) + ACES_E), + vec3(0.0), vec3(1.0)); +} +// Positive root of the ACES fit solved for x +vec3 SceneToLinear(vec3 c) { + vec3 y = clamp(c, vec3(0.0), vec3(ACES_INVERSE_MAX)); + vec3 a = ACES_C * y - ACES_A; + vec3 b = ACES_D * y - ACES_B; + vec3 k = ACES_E * y; + return (-b - sqrt(max(b * b - 4.0 * a * k, vec3(0.0)))) / (2.0 * a); +} +vec3 SkyRadiance(vec3 dir, float lod) { + return textureLod(skyCube, dir, lod).rgb; +} + // ============================================================ // Shadow — 16-sample PCF with IGN rotation (identical to Vulkan) // ============================================================ @@ -89,121 +134,469 @@ float CalcShadow(vec4 sc) { } // ============================================================ -// Procedural wave height field -// ============================================================ -float WaterHeight(vec2 xz, float time) { - vec2 w1 = vec2( time * 0.45, time * 0.30); - vec2 w2 = vec2(-time * 0.35, time * 0.50); - float h = 0.0; - h += sin(xz.x * 0.24 + w1.x) * sin(xz.y * 0.19 + w1.y); - h += sin(xz.x * 0.33 + w2.x - xz.y * 0.11) * 0.65; - h += sin(xz.x * 0.80 + w1.x * 1.9 + xz.y * 0.58) * 0.40; - h += sin(xz.x * 1.10 - w2.x * 2.1) * sin(xz.y * 0.88 + w2.y * 1.4) * 0.35; - h += sin(xz.x * 2.20 + w1.x * 3.3) * sin(xz.y * 1.85 - w1.y * 2.7) * 0.15; - h += sin(xz.x * 3.00 - w2.x * 4.2 + xz.y * 2.30) * 0.10; - return h; -} -vec3 WaveNormal(vec3 wp, float time) { - if (fragNormal.y < 0.5) return fragNormal; - const float d = 0.15; - float xd = (WaterHeight(wp.xz - vec2(d,0), time) - WaterHeight(wp.xz + vec2(d,0), time)) / (2.0*d); - float zd = (WaterHeight(wp.xz - vec2(0,d), time) - WaterHeight(wp.xz + vec2(0,d), time)) / (2.0*d); - return normalize(vec3(xd * 0.30, 1.0, zd * 0.30)); -} - -// ============================================================ -// Sky reflection (identical to Vulkan) +// Lighting inputs // ============================================================ -vec3 SampleSky(vec3 dir, vec3 sunDir, vec3 sunColor, vec3 amb) { - float y = max(dir.y, 0.0); - vec3 horiz = mix(vec3(0.70, 0.82, 0.95), amb, 0.30); - vec3 zenith = mix(vec3(0.22, 0.42, 0.82), amb * 0.65, 0.25); - vec3 sky = mix(horiz, zenith, pow(y, 0.45)); - float sd = max(dot(dir, sunDir), 0.0); - sky += sunColor * pow(sd, 500.0) * 5.0 + sunColor * pow(sd, 60.0) * 0.5; - if (dir.y < 0.0) sky = mix(horiz * 0.5, vec3(0.02, 0.04, 0.08), clamp(-dir.y * 4.0, 0.0, 1.0)); - return sky; +vec3 gSunDir = vec3(0.0, 1.0, 0.0); +vec3 gSunCol = vec3(1.0); +void FindSun() { + for (uint i = 0u; i < NumActiveLights; i++) { + if (Lights[i].Type == 0u) { + gSunDir = normalize(-Lights[i].Direction); + gSunCol = Lights[i].Color * Lights[i].Intensity; + break; + } + } } +vec3 SunDirection() { return gSunDir; } +vec3 SunRadiance() { return gSunCol; } +vec3 AmbientRadiance() { return AmbientColor * AmbientIntensity; } +vec3 CameraPosition() { return CameraPos; } -vec3 ACESFilm(vec3 x) { return clamp((x*(2.51*x+0.03))/(x*(2.43*x+0.59)+0.14), vec3(0), vec3(1)); } -float FresnelSchlick(float c) { return 0.02 + 0.98 * pow(clamp(1.0-c,0.0,1.0), 5.0); } -float GGX(float NdotH, float r) { float a=r*r*r*r; float d=NdotH*NdotH*(a-1.0)+1.0; return a/(3.14159265*d*d); } - -// Sky-matched gradient fog + exponential height fog (mirrors lighting_pass_gl.frag). -vec3 ApplyFog(vec3 color, vec3 worldPos) { - if (FogEnd <= 0.0) return color; - vec3 toFrag = worldPos - CameraPos; - float dist = length(toFrag.xz); - float distFog = clamp((dist - FogStart) / max(FogEnd - FogStart, 1e-4), 0.0, 1.0); +// Fog terms of lighting_pass_gl ApplyFog, split so the snapshot can be unfogged +float FogAmount(vec3 p) { + if (FogEnd <= 0.0) return 0.0; + float dist = length(p.xz - CameraPos.xz); + float distFog = clamp((dist - FogStart) / max(FogEnd - FogStart, EPSILON), 0.0, 1.0); float heightFog = 0.0; - if (HeightFogEnabled > 0.5) { - float h = max(HeightFogTop - worldPos.y, 0.0); + if (IsSet(HeightFogEnabled)) { + float h = max(HeightFogTop - p.y, 0.0); heightFog = clamp(HeightFogDensity * (1.0 - exp(-h * HeightFogFalloff)), 0.0, 1.0); } - float fogAmount = 1.0 - (1.0 - distFog) * (1.0 - heightFog); - vec3 viewDir = normalize(toFrag); - float t = smoothstep(0.0, 1.0, clamp(viewDir.y * 0.5 + 0.5, 0.0, 1.0)); - vec3 fogColor = mix(FogColor.rgb, FogColorZenith.rgb, t); - return mix(color, fogColor, fogAmount); + return 1.0 - (1.0 - distFog) * (1.0 - heightFog); +} +vec3 FogColorAt(vec3 p) { + vec3 viewDir = normalize(p - CameraPos); + float t = smoothstep(0.0, 1.0, clamp(SignedToUnit(viewDir.y), 0.0, 1.0)); + return mix(FogColor.rgb, FogColorZenith.rgb, t); } -void main() { - float time = fragTime; - vec3 N = WaveNormal(fragWorldPos, time); - vec3 V = normalize(CameraPos - fragWorldPos); +// ==== SHARED WATER BODY (keep identical in water_shader_gl.frag) ==== - // Find directional light - vec3 sunDir = vec3(0.0, 1.0, 0.0); - vec3 sunCol = vec3(1.0); - for (uint i = 0u; i < NumActiveLights; i++) { - if (Lights[i].Type == 0u) { - sunDir = normalize(-Lights[i].Direction); - sunCol = Lights[i].Color * Lights[i].Intensity; - break; +// ---- Physical constants ---- +#define PI 3.14159265 +#define TWO_PI (2.0 * PI) +#define GRAVITY 9.81 // m/s^2 +#define WATER_IOR 1.333 +#define AIR_IOR 1.0 +#define FRESNEL_F0 pow((WATER_IOR - AIR_IOR) / (WATER_IOR + AIR_IOR), 2.0) +// Cosine of the total internal reflection angle seen from below +#define TIR_COS sqrt(1.0 - pow(AIR_IOR / WATER_IOR, 2.0)) +#define TIR_SOFTEN 0.04 + +// ---- Waves: octaves derived from a few parameters ---- +#define WAVE_COUNT 12 +#define WAVE_BASE_LENGTH 13.0 // m, longest octave +#define WAVE_LENGTH_RATIO (1.0 / sqrt(2.0)) // irrational octave spacing +#define WAVE_LENGTH_JITTER 0.12 // +- fraction added to each octave step +#define WAVE_BASE_STEEPNESS 0.075 // slope amplitude of the longest octave +#define WAVE_STEEPNESS_DECAY 0.8946 // per-octave steepness falloff +#define WIND_ANGLE 0.2 // rad, mean travel direction in xz +#define WIND_SPREAD_LONG 0.9 // rad, long octaves stay nearer the wind +#define WIND_SPREAD_SHORT 1.4 // rad, short chop spreads wider +#define WAVE_DIR_SEED 17.0 // hash streams per octave index +#define WAVE_PHASE_SEED 31.0 +#define WAVE_LENGTH_SEED 47.0 +#define WAVE_SPEED_SCALE 0.55 // fraction of deep-water phase speed +#define WAVE_FADE_START 0.08 // pixel footprint / wavelength +#define WAVE_FADE_END 0.25 // 4 pixels per wavelength + +// ---- Wave field variation ---- +#define WARP_SCALE (1.0 / 55.0) // 1/m, domain warp noise +#define WARP_AMPLITUDE 2.5 // m the wave coordinates bend +#define WARP_SPEED 0.03 // noise units per second +#define WARP_CHANNEL_OFFSET vec2(19.3, 7.7) // decorrelates warp x and y +#define SWELL_SCALE (1.0 / 90.0) // 1/m, long-octave amplitude patches +#define SWELL_RANGE vec2(0.65, 1.15) // min, max amplitude +#define SWELL_DRIFT vec2(0.011, 0.007) // noise units per second +#define GUST_SCALE (1.0 / 24.0) // 1/m, short-octave gust patches +#define GUST_RANGE vec2(0.35, 1.3) +#define GUST_DRIFT vec2(-0.03, 0.02) +#define CALM_START 30.0 // m from the camera where the surface settles +#define CALM_END 100.0 +#define FAR_SLOPE_SCALE 0.35 // remaining wave slope far away + +// ---- Optics ---- +#define BASE_ROUGHNESS 0.035 +#define SIGMA_T vec3(0.46, 0.105, 0.075) // absorption + scattering, 1/m +#define SCATTER_ALBEDO vec3(0.014, 0.080, 0.150) +#define MIN_TRANSMIT_COS 0.2 // caps the slant path at grazing angles +#define SPEC_MAX 30.0 +#define SUN_SHADOW_FADE vec2(-0.1, 0.2) // N.L range where the sun fades in +#define MAX_UNFOG 0.9 +#define BOTTOM_TRUST_FOG 0.4 // fog amount where the bottom color starts to fade out + +// ---- Sky ---- +#define SKY_MIN_Y 0.07 // stay above the skybox's own horizon band +#define SKY_MAX_RADIANCE 16.0 +#define SKY_LOD_PER_SLOPE 10.0 +#define SKY_HORIZON vec3(0.70, 0.82, 0.95) +#define SKY_ZENITH vec3(0.22, 0.42, 0.82) +#define SKY_HORIZON_TINT 0.30 +#define SKY_ZENITH_TINT 0.25 +#define SKY_ZENITH_DIM 0.65 +#define SKY_GRADIENT_POW 0.45 +#define SKY_SUN_POW 60.0 +#define SKY_SUN_GAIN 0.5 + +// ---- Refraction and bottom ---- +#define REFRACT_SCALE 1.2 // m of bottom shift per unit slope and m depth +#define REFRACT_MAX_DEPTH 2.5 +#define REFRACT_DEPTH_BIAS 0.05 // m the bent sample must sit behind the surface +#define ABSENT_DEPTH 1e3 // depth used when nothing lies behind the water +#define BLUR_PER_DEPTH 0.06 // m of blur radius per m of depth +#define BLUR_MIN_PX 0.5 +#define BLUR_MAX_PX 6.0 +#define BLUR_TAPS 8 // fixed Vogel disk, no per-pixel noise to boil under motion +#define GOLDEN_ANGLE 2.39996323 +#define VOGEL_RING_OFFSET 0.5 // centres each tap in its equal-area ring +#define CAUSTIC_SCALE 0.9 +#define CAUSTIC_STRENGTH 1.2 +#define CAUSTIC_FALLOFF 0.35 // 1/m +#define CAUSTIC_FADE_IN vec2(0.05, 0.5) +#define CAUSTIC_SHARPNESS 5.0 +#define MIN_SUN_ELEVATION 0.2 + +// ---- Screen-space reflection ---- +#define SSR_MIN_FRESNEL 0.04 +#define SSR_STEPS 16 +#define SSR_BATCH 4 // depth fetches issued together +#define SSR_REFINE_STEPS 4 +#define SSR_START 0.5 // m, fixed so reflections stay still under motion +#define SSR_GROWTH 1.38 +#define SSR_STEP_ADD 0.1 // m +#define SSR_MAX_DIST 90.0 // m +#define SSR_MIN_THICKNESS 0.75 // m +#define SSR_THICKNESS_STEPS 2.0 // hit tolerance in step lengths +#define SSR_EDGE_FADE 0.07 // UV +#define SSR_DIST_FADE_START 0.6 // fraction of SSR_MAX_DIST +#define SSR_ORIGIN_OFFSET 0.02 // m along the normal +#define SSR_MIN_DIR_Y 0.015 + +// ---- Shoreline ---- +#define SHORE_FADE_DEPTH 0.12 // m of depth over which the contact line fades in +#define FOAM_MAX_DEPTH 0.7 +#define FOAM_BAND 0.6 +#define FOAM_SCALE 3.0 // 1/m +#define FOAM_DRIFT vec2(0.21, -0.17) +#define FOAM_DETAIL_SCALE 2.3 +#define FOAM_DETAIL_OFFSET 7.1 +#define FOAM_DETAIL_WEIGHT 0.35 +#define FOAM_THRESHOLD vec2(0.5, 0.85) +#define FOAM_NOISE_WEIGHT 0.7 +#define FOAM_SHORE_WEIGHT 0.55 +#define FOAM_OPACITY 0.8 +#define FOAM_ALBEDO 0.85 + +// ---- Faces ---- +#define TOP_FACE_MIN_Y 0.5 // normal.y above which a face is the water top + +// ---- Hash (Dave Hoskins, hash without sine) ---- +#define HASH_SCALE 0.1031 +#define HASH_OFFSET 33.33 + +// ---- Caustics: two warped sine layers (x = layer A, y = layer B) ---- +#define CAUSTIC_FREQ vec2(1.7, 1.9) +#define CAUSTIC_WARP_FREQ vec2(1.3, 1.1) +#define CAUSTIC_WARP_AMOUNT vec2(1.2, 1.3) +#define CAUSTIC_WARP_SPEED vec2(0.8, -0.7) +#define CAUSTIC_DRIFT vec2(0.6, -0.5) + +float Hash12(vec2 p) { + vec3 p3 = fract(vec3(p.xyx) * HASH_SCALE); + p3 += dot(p3, p3.yzx + HASH_OFFSET); + return fract((p3.x + p3.y) * p3.z); +} +float ValueNoise(vec2 p) { + vec2 i = floor(p), f = fract(p); + vec2 u = f * f * (3.0 - 2.0 * f); + return mix(mix(Hash12(i), Hash12(i + vec2(1.0, 0.0)), u.x), + mix(Hash12(i + vec2(0.0, 1.0)), Hash12(i + vec2(1.0, 1.0)), u.x), u.y); +} + +float SignedNoise(vec2 p) { return ValueNoise(p) * 2.0 - 1.0; } + +// xyz = normal, w = slope variance removed by the filter and the far calm +vec4 WaveNormal(vec2 xz, float t, float footprint, float viewDist) { + // Slow domain warp keeps crests from lining up into periodic rows + vec2 wp = xz * WARP_SCALE + t * WARP_SPEED; + vec2 p = xz + vec2(SignedNoise(wp), SignedNoise(wp + WARP_CHANNEL_OFFSET)) * WARP_AMPLITUDE; + // Patches of swell for the long octaves and gusts for the short ones + float swell = mix(SWELL_RANGE.x, SWELL_RANGE.y, ValueNoise(xz * SWELL_SCALE + t * SWELL_DRIFT)); + float gust = mix(GUST_RANGE.x, GUST_RANGE.y, ValueNoise(xz * GUST_SCALE + t * GUST_DRIFT)); + // Distant water settles toward a mirror; the removed slope widens the glint + float calm = mix(1.0, FAR_SLOPE_SCALE, smoothstep(CALM_START, CALM_END, viewDist)); + + vec2 g = vec2(0.0); + float lost = 0.0; + float wavelength = WAVE_BASE_LENGTH; + float steepness = WAVE_BASE_STEEPNESS; + for (int i = 0; i < WAVE_COUNT; ++i) { + float octave = float(i) / float(WAVE_COUNT - 1); + float s = steepness * mix(swell, gust, octave); + float fade = (1.0 - smoothstep(WAVE_FADE_START, WAVE_FADE_END, footprint / wavelength)) * calm; + lost += s * s * (1.0 - fade * fade); + if (fade > 0.0) { + float spread = mix(WIND_SPREAD_LONG, WIND_SPREAD_SHORT, octave); + float angle = WIND_ANGLE + spread * (2.0 * Hash12(vec2(i, WAVE_DIR_SEED)) - 1.0); + vec2 dir = vec2(cos(angle), sin(angle)); + float k = TWO_PI / wavelength; + float omega = sqrt(GRAVITY * k) * WAVE_SPEED_SCALE; + float phase = Hash12(vec2(i, WAVE_PHASE_SEED)) * TWO_PI; + g += dir * (s * fade * cos(dot(dir, p) * k - omega * t + phase)); } + float jitter = 2.0 * Hash12(vec2(i, WAVE_LENGTH_SEED)) - 1.0; + wavelength *= WAVE_LENGTH_RATIO * (1.0 + WAVE_LENGTH_JITTER * jitter); + steepness *= WAVE_STEEPNESS_DECAY; } - vec3 amb = AmbientColor * AmbientIntensity; + // Mean of cos^2 is one half + return vec4(normalize(vec3(-g.x, 1.0, -g.y)), 0.5 * lost); +} + +float FresnelSchlick(float c) { + return FRESNEL_F0 + (1.0 - FRESNEL_F0) * pow(clamp(1.0 - c, 0.0, 1.0), 5.0); +} + +vec3 SkyColor(vec3 dir, float lod, vec3 sunDir, vec3 sunCol, vec3 amb) { + dir = normalize(vec3(dir.x, max(dir.y, SKY_MIN_Y), dir.z)); + if (IsSet(uSceneFlags.y)) return min(SkyRadiance(dir, lod), vec3(SKY_MAX_RADIANCE)); + vec3 horiz = mix(SKY_HORIZON, amb, SKY_HORIZON_TINT); + vec3 zenith = mix(SKY_ZENITH, amb * SKY_ZENITH_DIM, SKY_ZENITH_TINT); + vec3 sky = mix(horiz, zenith, pow(dir.y, SKY_GRADIENT_POW)); + float sd = max(dot(dir, sunDir), 0.0); + return sky + sunCol * pow(sd, SKY_SUN_POW) * SKY_SUN_GAIN; +} + +float Caustics(vec2 p, float t) { + vec2 warp = sin(p.yx * CAUSTIC_WARP_FREQ + t * CAUSTIC_WARP_SPEED) * CAUSTIC_WARP_AMOUNT; + vec2 layers = abs(sin(p * CAUSTIC_FREQ + warp + t * CAUSTIC_DRIFT)); + float a = layers.x, b = layers.y; + // Bright where both layers cross zero + return pow(clamp(1.0 - 0.5 * (a + b), 0.0, 1.0), CAUSTIC_SHARPNESS); +} + +vec3 Unfog(vec3 c, vec3 p, float fog) { + float f = min(fog, MAX_UNFOG); + return max((c - FogColorAt(p) * f) / (1.0 - f), vec3(0.0)); +} + +float LinearDepth(float depth) { + float n = uSceneCamera.z, f = uSceneCamera.w; + return n * f / max(f - DepthToNdc(depth) * (f - n), EPSILON); +} + +bool OnScreen(vec2 uv) { + return all(greaterThan(uv, vec2(0.0))) && all(lessThan(uv, vec2(1.0))); +} + +// rgb = scene color at the hit, a = confidence +vec4 TraceReflection(vec3 origin, vec3 dir) { + float prevT = 0.0; + float t = SSR_START; + for (int i = 0; i < SSR_STEPS; i += SSR_BATCH) { + // Issue a batch of depth fetches before testing any of them + float ts[SSR_BATCH]; + vec4 clips[SSR_BATCH]; + vec2 uvs[SSR_BATCH]; + float depths[SSR_BATCH]; + for (int j = 0; j < SSR_BATCH; ++j) { + ts[j] = t; + clips[j] = uViewProj * vec4(origin + dir * t, 1.0); + uvs[j] = ProjectToScreen(clips[j]).xy; + depths[j] = textureLod(sceneDepth, uvs[j], 0.0).r; + t = min(t * SSR_GROWTH + SSR_STEP_ADD, SSR_MAX_DIST); + } + for (int j = 0; j < SSR_BATCH; ++j) { + if (clips[j].w <= uSceneCamera.z || !OnScreen(uvs[j])) return vec4(0.0); + float diff = clips[j].w - LinearDepth(depths[j]); + float tolerance = max(SSR_MIN_THICKNESS, (ts[j] - prevT) * SSR_THICKNESS_STEPS); + if (depths[j] < 1.0 && diff > 0.0 && diff < tolerance) { + float lo = prevT, hi = ts[j]; + vec2 uv = uvs[j]; + for (int k = 0; k < SSR_REFINE_STEPS; ++k) { + float mid = 0.5 * (lo + hi); + vec4 c = uViewProj * vec4(origin + dir * mid, 1.0); + vec3 m = ProjectToScreen(c); + float md = textureLod(sceneDepth, m.xy, 0.0).r; + if (c.w > LinearDepth(md)) { hi = mid; uv = m.xy; } else { lo = mid; } + } + vec2 edge = min(uv, 1.0 - uv); + float conf = smoothstep(0.0, SSR_EDGE_FADE, min(edge.x, edge.y)) + * (1.0 - smoothstep(SSR_DIST_FADE_START * SSR_MAX_DIST, SSR_MAX_DIST, hi)); + return vec4(textureLod(sceneColor, uv, 0.0).rgb, conf); + } + prevT = ts[j]; + } + } + return vec4(0.0); +} + +struct WaterResult { + vec3 color; // composited water, linear, before fog + vec3 backdrop; // unrefracted scene behind the surface, as stored + float edge; // 0 at the contact line, 1 once the water has depth +}; + +WaterResult ShadeWater(vec3 P, vec3 Nface, float t, float footprint) { + WaterResult r; + vec3 sunDir = SunDirection(); + vec3 sunCol = SunRadiance(); + vec3 amb = AmbientRadiance(); + vec3 toCam = CameraPosition() - P; + vec3 V = normalize(toCam); + bool top = Nface.y > TOP_FACE_MIN_Y; + bool below = dot(Nface, V) < 0.0; + bool scene = IsSet(uSceneFlags.x); - float NdotV = max(dot(N, V), 0.001); + // Waves + vec4 wave = top ? WaveNormal(P.xz, t, footprint, length(toCam)) : vec4(Nface, 0.0); + vec3 N = below ? -wave.xyz : wave.xyz; + float a2 = BASE_ROUGHNESS * BASE_ROUGHNESS + wave.w; + + float NdotV = max(dot(N, V), 0.0); float fresnel = FresnelSchlick(NdotV); - float shadow = CalcShadow(fragShadowCoord); - shadow *= smoothstep(-0.1, 0.2, dot(N, sunDir)); - float NdotL = clamp(dot(N, sunDir) * 0.65 + 0.35, 0.0, 1.0); - vec3 diffuse = sunCol * NdotL * shadow; - - // BSL water color - vec3 wcSqrt = vec3(64.0, 160.0, 255.0) / 255.0 * 0.35; - vec3 wColor = wcSqrt * wcSqrt * 3.0; - vec3 waterBody = wColor * (amb * 1.8 + diffuse * 0.9); - - float sss = pow(max(dot(V, -sunDir), 0.0), 4.0) * (1.0 - NdotV) * 0.35; - waterBody += vec3(0.0, 0.08, 0.14) * sunCol * sss; - - vec3 R = reflect(-V, N); - vec3 refl = SampleSky(R, sunDir, sunCol, amb); - - vec3 H = normalize(V + sunDir); - float NdotH = max(dot(N, H), 0.0); - float spec = GGX(NdotH, 0.07) * fresnel * shadow; - vec3 specC = sunCol * min(spec, 40.0); - - vec3 finalColor = mix(waterBody, refl, fresnel) + specC; - - // Caustic shimmer - if (fragNormal.y > 0.5) { - vec2 cp = fragWorldPos.xz * 0.65; - float t = time * 0.9; - float c = sin(cp.x*2.7+t)*sin(cp.y*2.7-t*0.75)*0.5+0.5; - c += sin(cp.x*1.9-t*0.55+cp.y*1.6)*sin(cp.y*2.4+t*0.35)*0.5+0.5; - c = clamp(pow(c * 0.5, 1.6) * 1.5, 0.0, 1.0); - finalColor += sunCol * c * 0.05 * shadow; + // Sun visibility at the surface + float NdotS = dot(N, sunDir); + float sunVis = 0.0; + if (!below && NdotS > SUN_SHADOW_FADE.x) + sunVis = CalcShadow(fragShadowCoord) * smoothstep(SUN_SHADOW_FADE.x, SUN_SHADOW_FADE.y, NdotS); + vec3 lightIn = amb + sunCol * (sunVis * max(sunDir.y, 0.0)); + vec3 inscatter = SCATTER_ALBEDO * lightIn; + + // Scene behind the surface + vec2 uv0 = gl_FragCoord.xy * uScreenSize.zw; + vec3 backdrop = vec3(0.0); + vec3 under = inscatter; + float depth0 = ABSENT_DEPTH; + if (scene) { + float d0 = textureLod(sceneDepth, uv0, 0.0).r; + vec3 B0 = Reconstruct(uv0, d0); + backdrop = textureLod(sceneColor, uv0, 0.0).rgb; + if (d0 < 1.0) depth0 = top ? max(P.y - B0.y, 0.0) : distance(P, B0); + + // Refraction: bend the bottom lookup by the wave slope, keep it behind the surface + vec2 uv1 = uv0; + vec3 B1 = B0; + float d1 = d0; + if (top && !below && d0 < 1.0) { + vec3 bent = B0 + vec3(N.x, 0.0, N.z) * (REFRACT_SCALE * min(depth0, REFRACT_MAX_DEPTH)); + vec4 bc = uViewProj * vec4(bent, 1.0); + if (bc.w > uSceneCamera.z) { + vec3 s = ProjectToScreen(bc); + float sd = textureLod(sceneDepth, s.xy, 0.0).r; + float surfZ = (uViewProj * vec4(P, 1.0)).w; + if (OnScreen(s.xy) && LinearDepth(sd) > surfZ + REFRACT_DEPTH_BIAS) { + uv1 = s.xy; + d1 = sd; + B1 = Reconstruct(uv1, sd); + } + } + } + float depth1 = d1 < 1.0 ? (top ? max(P.y - B1.y, 0.0) : distance(P, B1)) : ABSENT_DEPTH; + + // Beer-Lambert along the refracted view ray plus the sunlight's way down + float cosV = clamp(abs(dot(Nface, V)), 0.0, 1.0); + float cosT = sqrt(1.0 - (1.0 - cosV * cosV) / (WATER_IOR * WATER_IOR)); + float path = below ? length(toCam) : depth1 * (1.0 / max(cosT, MIN_TRANSMIT_COS) + 1.0); + vec3 trans = exp(-SIGMA_T * path); + + // Scattering softens the bottom with depth + vec3 bottom = textureLod(sceneColor, uv1, 0.0).rgb; + float blurPx = clamp(depth1 * BLUR_PER_DEPTH / max(footprint, EPSILON), 0.0, BLUR_MAX_PX); + if (blurPx > BLUR_MIN_PX) { + vec2 radius = blurPx * uScreenSize.zw; + for (int i = 0; i < BLUR_TAPS; ++i) { + float r = sqrt((float(i) + VOGEL_RING_OFFSET) / float(BLUR_TAPS)); + float a = float(i) * GOLDEN_ANGLE; + bottom += textureLod(sceneColor, uv1 + vec2(cos(a), sin(a)) * r * radius, 0.0).rgb; + } + bottom /= float(BLUR_TAPS + 1); + } + float bottomFog = FogAmount(B1); + bottom = SceneToLinear(Unfog(bottom, B1, bottomFog)); + if (top && !below && d1 < 1.0) { + vec2 cp = B1.xz + sunDir.xz * (depth1 / max(sunDir.y, MIN_SUN_ELEVATION)); + float c = Caustics(cp * CAUSTIC_SCALE, t); + bottom *= 1.0 + c * CAUSTIC_STRENGTH * sunVis * exp(-CAUSTIC_FALLOFF * depth1) + * smoothstep(CAUSTIC_FADE_IN.x, CAUSTIC_FADE_IN.y, depth1); + } + // A heavily fogged bottom carries no usable color; it sinks into the water body + float bottomTrust = 1.0 - smoothstep(BOTTOM_TRUST_FOG, MAX_UNFOG, bottomFog); + under = inscatter + (bottom - inscatter) * trans * bottomTrust; + } + + // Reflection: screen-space hit over the sky cubemap + vec3 R = reflect(-V, N); + vec3 refl = SkyColor(R, sqrt(wave.w) * SKY_LOD_PER_SLOPE, sunDir, sunCol, amb); + if (scene && !below && fresnel > SSR_MIN_FRESNEL) { + vec3 dir = normalize(vec3(R.x, max(R.y, SSR_MIN_DIR_Y), R.z)); + vec4 hit = TraceReflection(P + N * SSR_ORIGIN_OFFSET, dir); + refl = mix(refl, SceneToLinear(hit.rgb), hit.a); + } + if (below) { + refl = inscatter; + fresnel = max(fresnel, 1.0 - smoothstep(TIR_COS - TIR_SOFTEN, TIR_COS + TIR_SOFTEN, NdotV)); } - finalColor = ACESFilm(finalColor); - finalColor = ApplyFog(finalColor, fragWorldPos); + // Sun glint (GGX, widened by the filtered-out wave slopes) + vec3 spec = vec3(0.0); + if (!below && NdotS > 0.0) { + vec3 H = normalize(V + sunDir); + float NdotH = max(dot(N, H), 0.0); + float dd = NdotH * NdotH * (a2 - 1.0) + 1.0; + float D = a2 / (PI * dd * dd); + float smithV = NdotS * sqrt(NdotV * NdotV * (1.0 - a2) + a2); + float smithL = NdotV * sqrt(NdotS * NdotS * (1.0 - a2) + a2); + float vis = 0.5 / max(smithV + smithL, EPSILON); + float F = FresnelSchlick(max(dot(V, H), 0.0)); + spec = sunCol * min(D * vis * F * NdotS * sunVis, SPEC_MAX); + } + + vec3 color = mix(under, refl, fresnel) + spec; + + // Shoreline foam + float foam = 0.0; + if (scene && top && !below && depth0 < FOAM_MAX_DEPTH) { + float shore = 1.0 - smoothstep(0.0, FOAM_BAND, depth0); + vec2 fp = P.xz * FOAM_SCALE + FOAM_DRIFT * t; + float n = mix(ValueNoise(fp), ValueNoise(fp * FOAM_DETAIL_SCALE + FOAM_DETAIL_OFFSET), + FOAM_DETAIL_WEIGHT); + foam = smoothstep(FOAM_THRESHOLD.x, FOAM_THRESHOLD.y, + n * FOAM_NOISE_WEIGHT + shore * FOAM_SHORE_WEIGHT) * shore * FOAM_OPACITY; + color = mix(color, FOAM_ALBEDO * lightIn, foam); + } + + r.color = color; + r.backdrop = backdrop; + r.edge = (scene && !below) ? max(smoothstep(0.0, SHORE_FADE_DEPTH, depth0), foam) : 1.0; + return r; +} + +// Opacity of the plain blended fallback when the scene cannot be read +#define FALLBACK_ALPHA vec2(0.80, 0.97) +#define FALLBACK_ALPHA_POW 2.0 + +float FallbackAlpha(vec3 P, vec3 Nface) { + float NdotV = abs(dot(Nface, normalize(CameraPosition() - P))); + return mix(FALLBACK_ALPHA.x, FALLBACK_ALPHA.y, pow(1.0 - NdotV, FALLBACK_ALPHA_POW)); +} + +// ==== END SHARED WATER BODY ==== + +void main() { + FindSun(); + vec3 P = fragWorldPos; + float footprint = max(length(dFdx(P.xz)), length(dFdy(P.xz))); + WaterResult w = ShadeWater(P, fragNormal, fragTime, footprint); - float alpha = mix(0.72, 0.97, pow(clamp(1.0-NdotV, 0.0, 1.0), 2.0)); - outColor = vec4(finalColor, alpha); + // Display-referred like the rest of the GL frame: ACES, then fog + vec3 color = ACESFilm(w.color); + color = mix(color, FogColorAt(P), FogAmount(P)); + float alpha = 1.0; + if (IsSet(uSceneFlags.x)) + color = mix(w.backdrop, color, w.edge); + else + alpha = FallbackAlpha(P, fragNormal); + outColor = vec4(color, alpha); } diff --git a/Game/include/Game.hpp b/Game/include/Game.hpp index 3a05760..ac0a7e6 100644 --- a/Game/include/Game.hpp +++ b/Game/include/Game.hpp @@ -5,6 +5,9 @@ #include #include #include +#include + +#include "Player/CameraPose.hpp" class MainMenuScene; class MainScene; @@ -38,8 +41,21 @@ class SLEAK_API Game : public Sleak::GameBase { inline bool GetIsGameRunning() { return bIsGameRunning; } + /// Autopilot speed in m/s that new game scenes fly at, 0 for none. + void SetAutoFlySpeed(float speed) { m_autoFlySpeed = speed; } + /// Autopilot speed in m/s set by SetAutoFlySpeed(). + float GetAutoFlySpeed() const { return m_autoFlySpeed; } + /// Pins the camera of new game scenes at a fixed pose. + void SetCameraPose(const CameraPose& pose) { m_cameraPose = pose; } + /// Pose set by SetCameraPose(), if any. + const std::optional& GetCameraPose() const { + return m_cameraPose; + } + private: bool bIsGameRunning = true; + float m_autoFlySpeed = 0.0f; + std::optional m_cameraPose; MainMenuScene* m_menuScene = nullptr; MainScene* m_gameScene = nullptr; }; diff --git a/Game/include/MainScene.hpp b/Game/include/MainScene.hpp index 1dc542f..dc154cb 100644 --- a/Game/include/MainScene.hpp +++ b/Game/include/MainScene.hpp @@ -130,6 +130,7 @@ class MainScene : public Sleak::Scene { // UI state bool m_showUI = true; + bool m_showHud = true; // crosshair and hotbar, off for pinned captures bool m_showCrosshair = true; // Save/load UI feedback diff --git a/Game/include/Player/CameraPose.hpp b/Game/include/Player/CameraPose.hpp new file mode 100644 index 0000000..85e7cf2 --- /dev/null +++ b/Game/include/Player/CameraPose.hpp @@ -0,0 +1,19 @@ +#ifndef _CAMERA_POSE_HPP_ +#define _CAMERA_POSE_HPP_ + +/// A fixed camera placement for Debug captures: world position plus yaw and +/// pitch in degrees, where positive pitch looks down. +/// @ingroup player +struct CameraPose { + float x = 0.0f; + float y = 0.0f; + float z = 0.0f; + float yawDeg = 0.0f; + float pitchDeg = 0.0f; +}; + +/// Number of floats in a CameraPose, as written on the command line. +#define CAMERA_POSE_FIELD_COUNT \ + static_cast(sizeof(CameraPose) / sizeof(float)) + +#endif diff --git a/Game/include/Player/PlayerController.hpp b/Game/include/Player/PlayerController.hpp index e8ef2ff..064239c 100644 --- a/Game/include/Player/PlayerController.hpp +++ b/Game/include/Player/PlayerController.hpp @@ -2,6 +2,9 @@ #define _PLAYER_CONTROLLER_HPP_ #include +#include + +#include "Player/CameraPose.hpp" class ChunkManager; class MainScene; @@ -23,11 +26,23 @@ class PlayerController { void OnKeyReleased(const Sleak::Events::Input::KeyReleasedEvent& e); /// Applies fly vertical input and resolves voxel collision for this frame. void Update(); + /// Starts a hands-free flight along +Z just above the build limit so + /// tests can drive chunk streaming. Speed is in m/s; zero or less is + /// ignored. + void StartAutoFly(float speed); + /// Freezes the camera at a fixed pose so captures frame the same view on + /// every backend. + void PinCamera(const CameraPose& pose); private: ChunkManager& m_chunkManager; MainScene& m_scene; + float m_autoFlySpeed = 0.0f; + bool m_pinned = false; + Sleak::Math::Vector3D m_pinPosition; + Sleak::Math::Vector3D m_pinDirection; + // Minecraft-style double-tap space to toggle fly bool m_flying = false; float m_lastSpacePressTime = -1.0f; diff --git a/Game/include/UI/SettingsPanel.hpp b/Game/include/UI/SettingsPanel.hpp index 21e76ef..76df20e 100644 --- a/Game/include/UI/SettingsPanel.hpp +++ b/Game/include/UI/SettingsPanel.hpp @@ -6,7 +6,10 @@ class ChunkManager; class MainScene; -namespace Sleak { class DirectionalLight; } +namespace Sleak { class DirectionalLight; class Application; } + +// TAA recovers sub-pixel texture detail, so the atlas can sample one mip sharper +#define TAA_TEXTURE_LOD_BIAS (-1.0f) /// General/culling/lighting/SSAO/IBL/texture settings sections. Owns the /// live-edit sun/ambient/fog/texture state (crosshair/vsync/MT-loading/ @@ -15,8 +18,7 @@ namespace Sleak { class DirectionalLight; } /// @ingroup ui class SettingsPanel { public: - SettingsPanel(ChunkManager& chunkManager, MainScene& scene) - : m_chunkManager(chunkManager), m_scene(scene) {} + SettingsPanel(ChunkManager& chunkManager, MainScene& scene); /// Draws every settings section in sequence. void Render(); @@ -58,6 +60,9 @@ class SettingsPanel { float GetHeightFogFalloff() const { return m_heightFogFalloff; } Sleak::TextureFilter GetTextureFilter() const { return m_texFilter; } + /// Applies the user LOD bias to the block atlas, plus the TAA bias while TAA is on. + void ApplyTextureLodBias(); + private: /// Crosshair, vsync, and multithreaded-loading toggles. void RenderGeneralSection(); @@ -77,11 +82,15 @@ class SettingsPanel { void RenderTextureSection(); ChunkManager& m_chunkManager; + Sleak::Application* m_app; MainScene& m_scene; // Non-owning; the light itself is owned/created by MainScene. Sleak::DirectionalLight* m_sun = nullptr; + bool isDeferredEnabled = false; + bool isTAAEnabled = false; + // Sun state (live-editable) float m_sunElevation = 65.0f; // degrees above horizon (0=sunrise, 90=noon) float m_sunAzimuth = 255.0f; // degrees clockwise from north diff --git a/Game/include/World/ChunkManager.hpp b/Game/include/World/ChunkManager.hpp index e252743..703f0f8 100644 --- a/Game/include/World/ChunkManager.hpp +++ b/Game/include/World/ChunkManager.hpp @@ -79,6 +79,8 @@ class ChunkManager { float GetDrawDistance() const { return m_drawDistance; } void SetShadowCasterDistance(float dist) { m_shadowCasterDistSq = dist * dist; } + /// Stores the next shadow pass light view-projection (nullptr: none). + void SetShadowLightVP(const float* vp); void SetSeed(uint32_t seed) { m_generator.SetSeed(seed); } uint32_t GetSeed() const { return m_generator.GetSeed(); } @@ -147,6 +149,10 @@ class ChunkManager { void SetOOMThisFrame(bool oom) { m_oomThisFrame = oom; } float GetDrawDistSq() const { return m_drawDistSq; } float GetShadowCasterDistSq() const { return m_shadowCasterDistSq; } + /// Light view-projection used to cull shadow casters, or nullptr. + const float* GetShadowLightVP() const { + return m_hasShadowLightVP ? m_shadowLightVP : nullptr; + } const Sleak::RefPtr& GetMaterial() const { return m_material; } const Sleak::RefPtr& GetWaterMaterial() const { return m_waterMaterial; } @@ -222,6 +228,8 @@ class ChunkManager { float m_drawDistance = 96.0f; float m_drawDistSq = 96.0f * 96.0f; float m_shadowCasterDistSq = 96.0f * 96.0f; + float m_shadowLightVP[16] = {}; + bool m_hasShadowLightVP = false; int m_lastCenterX = INT_MAX; int m_lastCenterY = INT_MAX; int m_lastCenterZ = INT_MAX; diff --git a/Game/include/World/VoxelVertex.hpp b/Game/include/World/VoxelVertex.hpp index 8d60187..dbd62e8 100644 --- a/Game/include/World/VoxelVertex.hpp +++ b/Game/include/World/VoxelVertex.hpp @@ -95,6 +95,7 @@ inline Sleak::VertexFormatHandle GetVoxelVertexFormat() { desc.shadowShaderStem = "shadow_depth_voxel"; desc.gbufferShaderStem = "gbuffer_voxel"; desc.transparentShaderStem = "water_shader"; + desc.transparentReadsScene = true; return Sleak::VertexFormatRegistry::Register(desc); }(); return handle; diff --git a/Game/src/MainScene.cpp b/Game/src/MainScene.cpp index 4f1fac9..ce8f8f6 100644 --- a/Game/src/MainScene.cpp +++ b/Game/src/MainScene.cpp @@ -25,6 +25,9 @@ #include "Game.hpp" #include "World/TextureAtlas.hpp" +// Post-TAA RCAS strength (0 = off, 1 = strongest) +#define DEFAULT_SHARPEN_STRENGTH 0.5f + using namespace Sleak; using namespace Sleak::Math; @@ -73,6 +76,10 @@ bool MainScene::Initialize() { m_chunkManager.Initialize(this, m_blockMaterial); m_blockEffects.Initialize(this, m_blockMaterial); + // Captures load synchronously + if (!Sleak::CommandLine::GetValue("-capture").empty()) + m_multithreadedLoading = false; + if (m_isNewWorld) { m_chunkManager.SetSeed(m_worldSeed); @@ -109,8 +116,20 @@ bool MainScene::Initialize() { lm->SetFogDistances(fogDist * 0.91f, fogDist); } - // Register game-specific benchmark metrics + // Autopilot hides UI panels auto* app = Sleak::Application::GetInstance(); + auto* game = app ? static_cast(app->GetGame()) : nullptr; + if (game && game->GetAutoFlySpeed() > 0.0f) { + m_playerController.StartAutoFly(game->GetAutoFlySpeed()); + m_showUI = false; + } + if (game && game->GetCameraPose()) { + m_playerController.PinCamera(*game->GetCameraPose()); + m_showUI = false; + m_showHud = false; + } + + // Register game-specific benchmark metrics if (app && app->GetBenchmark()) { app->GetBenchmark()->RegisterMetric("RenderDistance", [this]() { return static_cast(m_chunkManager.GetRenderDistance()); @@ -274,6 +293,9 @@ void MainScene::Update(float deltaTime) { m_playerController.Update(); + // Next shadow pass matrix + auto* lm = GetLightManager(); + m_chunkManager.SetShadowLightVP(lm ? lm->GetShadowLightVP() : nullptr); m_chunkManager.Update(pos.GetX(), pos.GetY(), pos.GetZ()); m_chunkManager.RenderColumns(); @@ -296,11 +318,11 @@ void MainScene::Update(float deltaTime) { if (m_saveMessageTimer > 0.0f) m_saveMessageTimer -= deltaTime; if (m_loadConfirmTimer > 0.0f) m_loadConfirmTimer -= deltaTime; - m_hudPanel.RenderCrosshair(); + if (m_showHud) m_hudPanel.RenderCrosshair(); if (m_showUI) RenderUI(); - m_hotbar.Render(); + if (m_showHud) m_hotbar.Render(); } } @@ -358,7 +380,7 @@ void MainScene::SetupMaterial() { waterMat->SetDiffuseColor((uint8_t)255, (uint8_t)255, (uint8_t)255); waterMat->SetOpacity(0.7f); waterMat->SetRenderMode(MaterialRenderMode::Transparent); - waterMat->SetTwoSided(true); + waterMat->SetTwoSided(false); waterMat->SetShininess(64.0f); waterMat->SetSpecularColor((uint8_t)255, (uint8_t)255, (uint8_t)255); waterMat->Initialize(); @@ -388,9 +410,10 @@ void MainScene::SetupLighting() { cfg.ssrEnabled = false; cfg.iblEnabled = false; cfg.bloomEnabled = false; // match OpenGL ref (no post-FX bloom chain) - cfg.taaEnabled = true; // smooths shadow-edge aliasing (VK only for now) - cfg.shadowMapResolution = 3072; // BSL ULTRA tier; 4096 spiked VK frames - cfg.shadowFrustumSize = 128.0f; // half-extent → 256m ≈ fog range; 2x texel density + cfg.taaEnabled = true; // VK only for now + cfg.sharpenStrength = DEFAULT_SHARPEN_STRENGTH; + cfg.shadowMapResolution = 1072; // BSL ULTRA tier; 4096 spiked VK frames + cfg.shadowFrustumSize = 256.0f; // half-extent → 256m ≈ fog range; 2x texel density cfg.shadowCasterDistance = 256.0f; cfg.shadowDistance = 400.0f; // pull-back > frustum, else near-clip eats shadows @@ -401,7 +424,8 @@ void MainScene::SetupLighting() { cfg.maxOccluders = 192; auto* app = Sleak::Application::GetInstance(); - if (app) app->ApplyGraphicsConfig(cfg); + if (app) app->ApplyGraphicsConfig(cfg); + m_settingsPanel.ApplyTextureLodBias(); // Apply culling settings directly (not carried by ApplyGraphicsConfig). Sleak::CullingSystem::SetOcclusionBufferSize(cfg.occlusionBufferWidth, diff --git a/Game/src/Player/PlayerController.cpp b/Game/src/Player/PlayerController.cpp index 77ab9b7..563b000 100644 --- a/Game/src/Player/PlayerController.cpp +++ b/Game/src/Player/PlayerController.cpp @@ -5,11 +5,19 @@ #include #include #include