feat(render): SAGE water/ocean shaders, per-user logs, camera & culling

Terrain pass now ports the SAGE water model (Ocean.fx / OpenSAGE Water.frag): a de-gridded procedural wave normal combined with the retail ra3_deepocean flow and ra3_deepocean_nrm bump maps (appended as the last two terrain-atlas layers, no new backend binding), Schlick fresnel, sky reflection + depth-graded refraction, SAGE diffuse/specular lighting, depth-based transparency, and an underwater tint (UnderwaterDeferred.fx). Mirrored across terrain.frag / dx_terrain.hlsl / webgl_terrain_frag.glsl / webgpu_terrain.wgsl.

Also: logs move to the per-user state dir (%LOCALAPPDATA%\\OpenRA3\\logs, else XDG) and archive as openra3.<stamp>.log; the FPS label shows the active backend; middle-drag camera reset; objects and roads below the water plane are culled.
This commit is contained in:
EnderTheCoder
2026-09-30 22:18:08 +08:00
parent 10a1963eec
commit 26e1934a5d
16 changed files with 969 additions and 249 deletions
+162 -42
View File
@@ -8,6 +8,10 @@
// `Terrain.fx` / OpenSAGE `Terrain.frag` do, so it never restarts at a cell
// edge; material boundaries cross-fade with the SAGE blend ramp.
//
// The water plane is shaded with a port of the SAGE water effect
// (`Ocean.fx` / OpenSAGE `Water.frag`); the camera-below-water tint stands in
// for `UnderwaterDeferred.fx`. See docs/REVERSE_ENGINEERING.md.
//
// The Vulkan push constants (20 floats) become a constant buffer.
cbuffer TerrainCB : register(b0) {
@@ -15,7 +19,7 @@ cbuffer TerrainCB : register(b0) {
float4 params; // x=pitch, y=fov, z=water_z, w=has_water
float4 sun; // xyz=sun dir, w=ambient
float4 mapinfo; // x=W, y=H, z=unused, w=z_scale
float4 misc; // x=time, y=unused, z=cells per texture repeat, w=aspect
float4 misc; // x=time, y=water kind (0=ocean,1=river), z=cells per repeat, w=aspect
};
Texture2D<float> heightmap : register(t0);
@@ -27,6 +31,12 @@ SamplerState atlas_smp : register(s2);
static const float CELL = 10.0; // must match ra3::terrain::cell_size
// SAGE water model constants (see docs/REVERSE_ENGINEERING.md).
static const float WATER_SCALE = 1.0 / 320.0;
static const float WATER_TRANSPARENT_DEPTH = 10.0;
static const float WATER_MIN_OPACITY = 0.70;
static const float WATER_RIVER_MULTIPLIER = 1.0;
struct VSOut {
float4 pos : SV_Position;
float2 uv : TEXCOORD0;
@@ -104,6 +114,105 @@ float3 sample_layer(uint layer, float wx, float wy) {
return atlas.Sample(atlas_smp, float3(float2(wx, wy) / span, l)).rgb;
}
// ---- SAGE water (Ocean.fx / RiverWater.fx / Water.frag port) ----------------
// Sample an atlas layer by an explicit layer index (the water flow/bump maps
// are appended as the last two layers of the tile atlas).
float3 water_tex(int layer, float2 uv) {
uint w = 0;
uint h = 0;
uint lc = 0;
atlas.GetDimensions(w, h, lc);
float l = (float) clamp(layer, 0, (int) lc - 1);
return atlas.Sample(atlas_smp, float3(uv, l)).rgb;
}
// Scrolling wave normal on the water plane: the retail bump map (atlas's last
// layer), offset by the flow map (second-last layer) and combined with a
// de-gridded procedural wave so the sun glint is irregular and always moving.
float3 water_normal(float2 world_xy, float time) {
float2 q = world_xy * (WATER_SCALE * 6.0);
float a1 = q.x * 0.80 + q.y * 0.30 + time * 1.10;
float a2 = q.y * 0.95 - q.x * 0.45 - time * 1.30;
float a3 = (q.x + q.y) * 1.60 + time * 2.10;
float a4 = (q.x - q.y) * 2.30 - time * 1.70;
float dx = 0.224 * cos(a1) - 0.099 * cos(a2) + 0.256 * cos(a3) + 0.230 * cos(a4);
float dy = 0.084 * cos(a1) + 0.209 * cos(a2) + 0.256 * cos(a3) - 0.230 * cos(a4);
uint w = 0;
uint h = 0;
uint lc = 0;
atlas.GetDimensions(w, h, lc);
float3 flow = water_tex((int) lc - 2, q - float2(time * 0.010, time * 0.014)) * 2.0 - 1.0;
float3 bump = water_tex((int) lc - 1, q + flow.xy * 0.05 + float2(time * 0.006, time * 0.008)) * 2.0 - 1.0;
float sx = -dx * 0.30 + bump.x * 0.45;
float sy = -dy * 0.30 + bump.y * 0.45;
return normalize(float3(sx, sy, 1.0));
}
// Water.frag distortionPower * the flow texture: a small scrolling UV offset.
float water_distortion(float2 world_xy, float time) {
float2 q = world_xy * (WATER_SCALE * 6.0);
return 0.05 * (sin(q.x * 0.9 + time * 0.7) + sin(q.y * 1.1 - time * 0.5));
}
// Water.frag GetCloudColor: no cloud texture is bound, so a slow low-frequency
// scroll stands in.
float3 water_cloud(float2 world_xy, float time) {
return float3(0.85 + 0.15 * sin((world_xy.x + world_xy.y) * 0.0007 - time * 0.05));
}
// Retail UnderwaterDeferred.fx: absorbs red and fogs with distance. Applied to
// every output while the camera is below the water plane.
float3 apply_underwater(float3 color, float dist, float cam_z, float water_z) {
if (cam_z >= water_z - 0.5) return color;
const float3 absorb = float3(0.35, 0.62, 0.75);
float fog = clamp(1.0 - exp(-dist * 0.00022), 0.0, 0.9);
return lerp(color * absorb, float3(0.02, 0.10, 0.16), fog);
}
// Water.frag: fresnel-mixed reflection (sky) and refraction (depth-graded
// seabed), SAGE diffuse + specular lighting, cloud term, and a depth-based
// transparency fade. `river` (>0.5) applies RiverTransparencyMultiplier.
float3 water_shade(float3 hitpos, float3 dir, float dist) {
float time = misc.x;
float river = misc.y;
float seabed = world_height(hitpos.x, hitpos.y);
float depth = max(0.0, params.z - seabed);
float3 n = water_normal(hitpos.xy, time + water_distortion(hitpos.xy, time));
float3 sun_dir = normalize(sun.xyz);
// Schlick fresnel, water F0 = 0.02.
float cos_theta = clamp(dot(-dir, n), 0.0, 1.0);
float fresnel = 0.02 + 0.98 * pow(1.0 - cos_theta, 5.0);
// Reflection: the sky the surface mirrors. Refraction: the seabed, graded
// from shallow to deep and lit by the SAGE diffuse + specular model.
float3 reflection = sky_color(reflect(dir, n));
float3 shallow = float3(0.10, 0.34, 0.38);
float3 deep = float3(0.02, 0.12, 0.22);
float3 refraction = lerp(shallow, deep, clamp(depth / 40.0, 0.0, 1.0));
float ndotl = max(dot(n, sun_dir), 0.0);
float ambient = sun.w;
float3 diffuse = float3(ambient + (1.0 - ambient) * ndotl);
float3 half_v = normalize(sun_dir - dir);
float spec = pow(max(dot(n, half_v), 0.0), 90.0);
float3 color = lerp(refraction, reflection, clamp(fresnel, 0.0, 1.0)) * diffuse * water_cloud(hitpos.xy, time);
color += float3(1.0, 0.97, 0.9) * spec * 0.45;
// Depth-based transparency: shallow water shows the seabed, deep water goes
// opaque toward the deep colour.
float alpha = clamp(depth / WATER_TRANSPARENT_DEPTH, 0.0, 1.0) * WATER_MIN_OPACITY;
if (river > 0.5) alpha *= WATER_RIVER_MULTIPLIER;
color = lerp(refraction, color, clamp(alpha + 0.15, 0.0, 1.0));
// Distance haze toward the horizon, as the terrain.
float wfog = clamp(1.0 - exp(-dist * 0.00009), 0.0, 0.75);
return lerp(color, sky_color(float3(dir.x, dir.y, 0.0)), wfog);
}
float4 PSMain(VSOut input) : SV_Target {
float4 p = cam;
float pitch = clamp(params.x, 0.15, 1.45);
@@ -127,50 +236,75 @@ float4 PSMain(VSOut input) : SV_Target {
float3 dir = normalize(fwd + right * ndc.x * th * aspect + up * ndc.y * th);
if (dir.z >= -1e-4) {
return float4(sky_color(dir), 1.0);
return float4(apply_underwater(sky_color(dir), 0.0, cam_pos.z, params.z), 1.0);
}
// March the heightfield. The step grows quickly: the map diagonal is only
// ~9000 world units, so marching past ~20000 adds cost without detail, and
// a 6% growth rate more than doubles the worst-case iteration count.
float t = CELL * 0.5;
float dt = CELL * 0.5;
// Clip the ray to the map's XY rectangle: the boundary is an exact plane,
// so the silhouette there stays razor-sharp instead of stair-stepping
// across it. Outside the map is sky.
float t_enter = 0.0;
float t_exit = 1.0e30;
bool inside = true;
if (abs(dir.x) < 1e-6) {
inside = (cam_pos.x >= 0.0 && cam_pos.x <= world_w);
} else {
float a = (0.0 - cam_pos.x) / dir.x;
float b = (world_w - cam_pos.x) / dir.x;
t_enter = max(t_enter, min(a, b));
t_exit = min(t_exit, max(a, b));
}
if (inside) {
if (abs(dir.y) < 1e-6) {
inside = (cam_pos.y >= 0.0 && cam_pos.y <= world_h);
} else {
float a = (0.0 - cam_pos.y) / dir.y;
float b = (world_h - cam_pos.y) / dir.y;
t_enter = max(t_enter, min(a, b));
t_exit = min(t_exit, max(a, b));
}
}
if (!inside || t_exit <= 0.0) {
return float4(apply_underwater(sky_color(dir), 0.0, cam_pos.z, params.z), 1.0);
}
// March the heightfield cell by cell: the step is never longer than the
// time to cross one cell (dominant horizontal axis), while a clearance term
// lets the ray skip the empty air above the surface. Resolving every cell is
// what keeps cliff and map-edge silhouettes from quantising into huge
// stair-steps that crawl as the camera pans.
float horiz = max(abs(dir.x), abs(dir.y));
float cell_step = min(CELL / max(horiz, 1e-4), CELL * 32.0);
float t = max(t_enter, CELL * 0.5);
float prev = t;
bool hit = false;
float hit_t = 0.0;
for (int i = 0; i < 256 && t < 20000.0; ++i) {
for (int i = 0; i < 1024 && t <= t_exit; ++i) {
float3 w = cam_pos + dir * t;
if (w.x < 0.0 || w.y < 0.0 || w.x >= world_w || w.y >= world_h) {
prev = t;
dt *= 1.10;
t += dt;
continue;
}
if (params.w > 0.5 && w.z <= params.z) {
hit = true;
hit_t = t;
break;
}
if (w.z <= world_height(w.x, w.y)) {
float h = world_height(w.x, w.y);
float surface = (params.w > 0.5) ? max(h, params.z) : h;
if (w.z <= surface) {
hit = true;
hit_t = t;
break;
}
float clearance = (w.z - surface) / max(-dir.z, 1e-4);
prev = t;
dt *= 1.10;
t += dt;
t += clamp(clearance, cell_step, cell_step * 8.0);
}
if (!hit) {
return float4(sky_color(dir), 1.0);
return float4(apply_underwater(sky_color(dir), 0.0, cam_pos.z, params.z), 1.0);
}
// Refine the first crossing; with a sub-cell bracket this converges to the
// exact surface point.
float lo = prev;
float hi = hit_t;
for (int i = 0; i < 6; ++i) {
for (int i = 0; i < 18; ++i) {
float mid = 0.5 * (lo + hi);
float3 w = cam_pos + dir * mid;
bool water = params.w > 0.5 && w.z <= params.z;
if (water || w.z <= world_height(w.x, w.y)) {
float h = world_height(w.x, w.y);
float surface = (params.w > 0.5) ? max(h, params.z) : h;
if (w.z <= surface) {
hi = mid;
} else {
lo = mid;
@@ -182,21 +316,7 @@ float4 PSMain(VSOut input) : SV_Target {
float ambient = sun.w;
if (params.w > 0.5 && hitpos.z <= params.z + 0.01) {
// Water: animated normal from a procedural wave, sky reflection + fresnel.
float time = misc.x;
float2 q = hitpos.xy * 0.015;
float nx = sin(q.x * 1.3 + time * 1.7) + 0.5 * sin(q.x * 3.1 - time * 2.3);
float ny = sin(q.y * 1.1 - time * 1.3) + 0.5 * sin(q.y * 2.7 + time * 1.9);
float3 n = normalize(float3(nx * 0.06, ny * 0.06, 1.0));
float fres = pow(1.0 - clamp(-dir.z, 0.0, 1.0), 3.0);
float3 deep = float3(0.03, 0.16, 0.28);
float3 refl = sky_color(reflect(dir, n));
float lam = max(0.0, dot(n, sun_dir));
float3 water = lerp(deep, refl, clamp(0.25 + 0.55 * fres, 0.0, 0.9));
water += float3(1.0, 0.98, 0.9) * pow(lam, 64.0) * 0.6; // sun glint
float wfog = clamp(1.0 - exp(-hi * 0.00009), 0.0, 0.75);
water = lerp(water, sky_color(float3(dir.x, dir.y, 0.0)), wfog);
return float4(water, 1.0);
return float4(apply_underwater(water_shade(hitpos, dir, hi), hi, cam_pos.z, params.z), 1.0);
}
// Terrain: read the per-cell blend record, sample the base/blend/three-way
@@ -234,5 +354,5 @@ float4 PSMain(VSOut input) : SV_Target {
// Distance haze toward the horizon so the map edge blends into the sky.
float fog = clamp(1.0 - exp(-hi * 0.00009), 0.0, 0.75);
lit = lerp(lit, sky_color(float3(dir.x, dir.y, 0.0)), fog);
return float4(lit, 1.0);
return float4(apply_underwater(lit, hi, cam_pos.z, params.z), 1.0);
}
Binary file not shown.
+159 -34
View File
@@ -8,6 +8,10 @@
// The material is sampled **continuously** (`uv = cell / (2 * cellSize)`), as the
// retail `Terrain.fx` / OpenSAGE `Terrain.frag` do, so it never restarts at a
// cell edge; material boundaries cross-fade with the SAGE blend ramp.
//
// The water plane is shaded with a port of the SAGE water effect
// (`Ocean.fx` / OpenSAGE `Water.frag`); the camera-below-water tint stands in
// for `UnderwaterDeferred.fx`. See docs/REVERSE_ENGINEERING.md.
layout(binding = 0) uniform sampler2D heightmap;
layout(binding = 1) uniform sampler2D celldata;
layout(binding = 2) uniform sampler2DArray atlas;
@@ -17,7 +21,7 @@ layout(push_constant) uniform Push {
vec4 params; // x=pitch, y=fov, z=water_z, w=has_water
vec4 sun; // xyz=sun dir, w=ambient
vec4 mapinfo; // x=W, y=H, z=unused, w=z_scale
vec4 misc; // x=time, y=unused, z=cells per texture repeat, w=aspect
vec4 misc; // x=time, y=water kind (0=ocean,1=river), z=cells per repeat, w=aspect
} pc;
layout(location = 0) in vec2 in_uv;
@@ -30,6 +34,12 @@ const float CELL = 10.0; // must match ra3::terrain::cell_size
const float NEAR = 10.0;
const float FAR = 60000.0;
// SAGE water model constants (see docs/REVERSE_ENGINEERING.md).
const float WATER_SCALE = 1.0 / 320.0; // Water.frag: worldPos.xy / 320
const float WATER_TRANSPARENT_DEPTH = 10.0; // WaterTransparency.TransparentWaterDepth
const float WATER_MIN_OPACITY = 0.70; // WaterTransparency.TransparentWaterMinOpacity
const float WATER_RIVER_MULTIPLIER = 1.0; // WaterTransparency.RiverTransparencyMultiplier
float height_at(ivec2 c) {
c = clamp(c, ivec2(0), ivec2(pc.mapinfo.xy) - 1);
return texelFetch(heightmap, c, 0).r * 65535.0 * pc.mapinfo.w;
@@ -92,6 +102,99 @@ vec3 sample_layer(uint layer, float wx, float wy) {
return texture(atlas, vec3(vec2(wx, wy) / span, l)).rgb;
}
// ---- SAGE water (Ocean.fx / RiverWater.fx / Water.frag port) ----------------
// Sample an atlas layer by an explicit float layer index (the water flow/bump
// maps are appended as the last two layers of the tile atlas).
vec3 water_tex(int layer, vec2 uv) {
int lc = textureSize(atlas, 0).z;
float l = float(clamp(layer, 0, max(lc - 1, 0)));
return texture(atlas, vec3(uv, l)).rgb;
}
// Scrolling wave normal on the water plane: the retail bump map (atlas's last
// layer), offset by the flow map (second-last layer) and combined with a
// de-gridded procedural wave so the sun glint is irregular and always moving.
vec3 water_normal(vec2 world_xy, float time) {
vec2 q = world_xy * (WATER_SCALE * 6.0);
float a1 = q.x * 0.80 + q.y * 0.30 + time * 1.10;
float a2 = q.y * 0.95 - q.x * 0.45 - time * 1.30;
float a3 = (q.x + q.y) * 1.60 + time * 2.10;
float a4 = (q.x - q.y) * 2.30 - time * 1.70;
float dx = 0.224 * cos(a1) - 0.099 * cos(a2) + 0.256 * cos(a3) + 0.230 * cos(a4);
float dy = 0.084 * cos(a1) + 0.209 * cos(a2) + 0.256 * cos(a3) - 0.230 * cos(a4);
int lc = textureSize(atlas, 0).z;
vec3 flow = water_tex(lc - 2, q - vec2(time * 0.010, time * 0.014)) * 2.0 - 1.0;
vec3 bump = water_tex(lc - 1, q + flow.xy * 0.05 + vec2(time * 0.006, time * 0.008)) * 2.0 - 1.0;
float sx = -dx * 0.30 + bump.x * 0.45;
float sy = -dy * 0.30 + bump.y * 0.45;
return normalize(vec3(sx, sy, 1.0));
}
// Water.frag distortionPower * the flow texture: a small scrolling UV offset.
float water_distortion(vec2 world_xy, float time) {
vec2 q = world_xy * (WATER_SCALE * 6.0);
return 0.05 * (sin(q.x * 0.9 + time * 0.7) + sin(q.y * 1.1 - time * 0.5));
}
// Water.frag GetCloudColor: no cloud texture is bound, so a slow low-frequency
// scroll stands in.
vec3 water_cloud(vec2 world_xy, float time) {
return vec3(0.85 + 0.15 * sin((world_xy.x + world_xy.y) * 0.0007 - time * 0.05));
}
// Retail UnderwaterDeferred.fx: absorbs red and fogs with distance. Applied to
// every output while the camera is below the water plane.
vec3 apply_underwater(vec3 color, float dist, float cam_z, float water_z) {
if (cam_z >= water_z - 0.5) return color;
const vec3 absorb = vec3(0.35, 0.62, 0.75);
float fog = clamp(1.0 - exp(-dist * 0.00022), 0.0, 0.9);
return mix(color * absorb, vec3(0.02, 0.10, 0.16), fog);
}
// Water.frag: fresnel-mixed reflection (sky) and refraction (depth-graded
// seabed), SAGE diffuse + specular lighting, cloud term, and a depth-based
// transparency fade. `river` (>0.5) applies RiverTransparencyMultiplier.
vec3 water_shade(vec3 hitpos, vec3 dir, float dist) {
float time = pc.misc.x;
float river = pc.misc.y;
float seabed = world_height(hitpos.x, hitpos.y);
float depth = max(0.0, pc.params.z - seabed);
vec3 n = water_normal(hitpos.xy, time + water_distortion(hitpos.xy, time));
vec3 sun = normalize(pc.sun.xyz);
// Schlick fresnel, water F0 = 0.02.
float cos_theta = clamp(dot(-dir, n), 0.0, 1.0);
float fresnel = 0.02 + 0.98 * pow(1.0 - cos_theta, 5.0);
// Reflection: the sky the surface mirrors. Refraction: the seabed, graded
// from shallow to deep and lit by the SAGE diffuse + specular model.
vec3 reflection = sky_color(reflect(dir, n));
vec3 shallow = vec3(0.10, 0.34, 0.38);
vec3 deep = vec3(0.02, 0.12, 0.22);
vec3 refraction = mix(shallow, deep, clamp(depth / 40.0, 0.0, 1.0));
float ndotl = max(dot(n, sun), 0.0);
float ambient = pc.sun.w;
vec3 diffuse = vec3(ambient + (1.0 - ambient) * ndotl);
vec3 half_v = normalize(sun - dir);
float spec = pow(max(dot(n, half_v), 0.0), 90.0);
vec3 color = mix(refraction, reflection, clamp(fresnel, 0.0, 1.0)) * diffuse * water_cloud(hitpos.xy, time);
color += vec3(1.0, 0.97, 0.9) * spec * 0.45;
// Depth-based transparency: shallow water shows the seabed, deep water goes
// opaque toward the deep colour.
float alpha = clamp(depth / WATER_TRANSPARENT_DEPTH, 0.0, 1.0) * WATER_MIN_OPACITY;
if (river > 0.5) alpha *= WATER_RIVER_MULTIPLIER;
color = mix(refraction, color, clamp(alpha + 0.15, 0.0, 1.0));
// Distance haze toward the horizon, as the terrain.
float wfog = clamp(1.0 - exp(-dist * 0.00009), 0.0, 0.75);
return mix(color, sky_color(vec3(dir.x, dir.y, 0.0)), wfog);
}
void main() {
vec4 p = pc.cam;
float pitch = clamp(pc.params.x, 0.15, 1.45);
@@ -118,35 +221,71 @@ void main() {
if (dir.z >= -1e-4) {
gl_FragDepth = 1.0;
out_color = vec4(sky_color(dir), 1.0);
out_color = vec4(apply_underwater(sky_color(dir), 0.0, cam.z, pc.params.z), 1.0);
return;
}
// March the heightfield. The step grows quickly: the map diagonal is only
// ~9000 world units, so marching past ~20000 adds cost without detail, and
// a 6% growth rate more than doubles the worst-case iteration count.
float t = CELL * 0.5;
float dt = CELL * 0.5;
// Clip the ray to the map's XY rectangle: the boundary is an exact plane,
// so the silhouette there stays razor-sharp instead of stair-stepping
// across it. Outside the map is sky.
float t_enter = 0.0;
float t_exit = 1.0e30;
bool inside = true;
if (abs(dir.x) < 1e-6) {
inside = (cam.x >= 0.0 && cam.x <= world_w);
} else {
float a = (0.0 - cam.x) / dir.x;
float b = (world_w - cam.x) / dir.x;
t_enter = max(t_enter, min(a, b));
t_exit = min(t_exit, max(a, b));
}
if (inside) {
if (abs(dir.y) < 1e-6) {
inside = (cam.y >= 0.0 && cam.y <= world_h);
} else {
float a = (0.0 - cam.y) / dir.y;
float b = (world_h - cam.y) / dir.y;
t_enter = max(t_enter, min(a, b));
t_exit = min(t_exit, max(a, b));
}
}
if (!inside || t_exit <= 0.0) {
gl_FragDepth = 1.0;
out_color = vec4(apply_underwater(sky_color(dir), 0.0, cam.z, pc.params.z), 1.0);
return;
}
// March the heightfield cell by cell: the step is never longer than the
// time to cross one cell (dominant horizontal axis), while a clearance term
// lets the ray skip the empty air above the surface. Resolving every cell is
// what keeps cliff and map-edge silhouettes from quantising into huge
// stair-steps that crawl as the camera pans.
float horiz = max(abs(dir.x), abs(dir.y));
float cell_step = min(CELL / max(horiz, 1e-4), CELL * 32.0);
float t = max(t_enter, CELL * 0.5);
float prev = t;
bool hit = false;
float hit_t = 0.0;
for (int i = 0; i < 256 && t < 20000.0; ++i) {
for (int i = 0; i < 1024 && t <= t_exit; ++i) {
vec3 w = cam + dir * t;
if (w.x < 0.0 || w.y < 0.0 || w.x >= world_w || w.y >= world_h) {
prev = t; dt *= 1.10; t += dt; continue;
}
if (pc.params.w > 0.5 && w.z <= pc.params.z) { hit = true; hit_t = t; break; }
if (w.z <= world_height(w.x, w.y)) { hit = true; hit_t = t; break; }
prev = t; dt *= 1.10; t += dt;
float h = world_height(w.x, w.y);
float surface = (pc.params.w > 0.5) ? max(h, pc.params.z) : h;
if (w.z <= surface) { hit = true; hit_t = t; break; }
float clearance = (w.z - surface) / max(-dir.z, 1e-4);
prev = t;
t += clamp(clearance, cell_step, cell_step * 8.0);
}
if (!hit) { gl_FragDepth = 1.0; out_color = vec4(sky_color(dir), 1.0); return; }
if (!hit) { gl_FragDepth = 1.0; out_color = vec4(apply_underwater(sky_color(dir), 0.0, cam.z, pc.params.z), 1.0); return; }
// Refine the first crossing; with a sub-cell bracket this converges to the
// exact surface point.
float lo = prev, hi = hit_t;
for (int i = 0; i < 6; ++i) {
for (int i = 0; i < 18; ++i) {
float mid = 0.5 * (lo + hi);
vec3 w = cam + dir * mid;
bool water = pc.params.w > 0.5 && w.z <= pc.params.z;
if (water || w.z <= world_height(w.x, w.y)) hi = mid; else lo = mid;
float h = world_height(w.x, w.y);
float surface = (pc.params.w > 0.5) ? max(h, pc.params.z) : h;
if (w.z <= surface) hi = mid; else lo = mid;
}
vec3 hitpos = cam + dir * hi;
@@ -158,21 +297,7 @@ void main() {
float ambient = pc.sun.w;
if (pc.params.w > 0.5 && hitpos.z <= pc.params.z + 0.01) {
// Water: animated normal from a procedural wave, sky reflection + fresnel.
float time = pc.misc.x;
vec2 q = hitpos.xy * 0.015;
float nx = sin(q.x * 1.3 + time * 1.7) + 0.5 * sin(q.x * 3.1 - time * 2.3);
float ny = sin(q.y * 1.1 - time * 1.3) + 0.5 * sin(q.y * 2.7 + time * 1.9);
vec3 n = normalize(vec3(nx * 0.06, ny * 0.06, 1.0));
float fres = pow(1.0 - clamp(-dir.z, 0.0, 1.0), 3.0);
vec3 deep = vec3(0.03, 0.16, 0.28);
vec3 refl = sky_color(reflect(dir, n));
float lam = max(0.0, dot(n, sun));
vec3 water = mix(deep, refl, clamp(0.25 + 0.55 * fres, 0.0, 0.9));
water += vec3(1.0, 0.98, 0.9) * pow(lam, 64.0) * 0.6; // sun glint
float wfog = clamp(1.0 - exp(-hi * 0.00009), 0.0, 0.75);
water = mix(water, sky_color(vec3(dir.x, dir.y, 0.0)), wfog);
out_color = vec4(water, 1.0);
out_color = vec4(apply_underwater(water_shade(hitpos, dir, hi), hi, cam.z, pc.params.z), 1.0);
return;
}
@@ -211,5 +336,5 @@ void main() {
// Distance haze toward the horizon so the map edge blends into the sky.
float fog = clamp(1.0 - exp(-hi * 0.00009), 0.0, 0.75);
lit = mix(lit, sky_color(vec3(dir.x, dir.y, 0.0)), fog);
out_color = vec4(lit, 1.0);
out_color = vec4(apply_underwater(lit, hi, cam.z, pc.params.z), 1.0);
}
+144 -40
View File
@@ -24,6 +24,12 @@ out vec4 frag_color;
const float CELL = 10.0; // must match ra3::terrain::cell_size
// SAGE water model constants (see docs/REVERSE_ENGINEERING.md).
const float WATER_SCALE = 1.0 / 320.0;
const float WATER_TRANSPARENT_DEPTH = 10.0;
const float WATER_MIN_OPACITY = 0.70;
const float WATER_RIVER_MULTIPLIER = 1.0;
float height_at(ivec2 c) {
c = clamp(c, ivec2(0), ivec2(u_mapinfo.xy) - 1);
return texelFetch(u_heightmap, c, 0).r * 65535.0 * u_mapinfo.w;
@@ -82,6 +88,91 @@ vec3 sample_layer(uint layer, float wx, float wy) {
return texture(u_atlas, vec3(vec2(wx, wy) / span, l)).bgr;
}
// ---- SAGE water (Ocean.fx / RiverWater.fx / Water.frag port) ----------------
// Sample an atlas layer by an explicit layer index (the water flow/bump maps
// are appended as the last two layers of the tile atlas). The atlas is
// 0xAARRGGBB, so `.bgr` restores RGB (as `sample_layer`).
vec3 water_tex(int layer, vec2 uv) {
int lc = textureSize(u_atlas, 0).z;
float l = float(clamp(layer, 0, max(lc - 1, 0)));
return texture(u_atlas, vec3(uv, l)).bgr;
}
// Scrolling wave normal on the water plane: the retail bump map (atlas's last
// layer), offset by the flow map (second-last layer) and combined with a
// de-gridded procedural wave so the sun glint is irregular and always moving.
vec3 water_normal(vec2 world_xy, float time) {
vec2 q = world_xy * (WATER_SCALE * 6.0);
float a1 = q.x * 0.80 + q.y * 0.30 + time * 1.10;
float a2 = q.y * 0.95 - q.x * 0.45 - time * 1.30;
float a3 = (q.x + q.y) * 1.60 + time * 2.10;
float a4 = (q.x - q.y) * 2.30 - time * 1.70;
float dx = 0.224 * cos(a1) - 0.099 * cos(a2) + 0.256 * cos(a3) + 0.230 * cos(a4);
float dy = 0.084 * cos(a1) + 0.209 * cos(a2) + 0.256 * cos(a3) - 0.230 * cos(a4);
int lc = textureSize(u_atlas, 0).z;
vec3 flow = water_tex(lc - 2, q - vec2(time * 0.010, time * 0.014)) * 2.0 - 1.0;
vec3 bump = water_tex(lc - 1, q + flow.xy * 0.05 + vec2(time * 0.006, time * 0.008)) * 2.0 - 1.0;
float sx = -dx * 0.30 + bump.x * 0.45;
float sy = -dy * 0.30 + bump.y * 0.45;
return normalize(vec3(sx, sy, 1.0));
}
// Water.frag distortionPower * the flow texture: a small scrolling UV offset.
float water_distortion(vec2 world_xy, float time) {
vec2 q = world_xy * (WATER_SCALE * 6.0);
return 0.05 * (sin(q.x * 0.9 + time * 0.7) + sin(q.y * 1.1 - time * 0.5));
}
// Water.frag GetCloudColor: no cloud texture is bound, so a slow scroll stands in.
vec3 water_cloud(vec2 world_xy, float time) {
return vec3(0.85 + 0.15 * sin((world_xy.x + world_xy.y) * 0.0007 - time * 0.05));
}
// Retail UnderwaterDeferred.fx: absorbs red and fogs with distance.
vec3 apply_underwater(vec3 color, float dist, float cam_z, float water_z) {
if (cam_z >= water_z - 0.5) return color;
const vec3 absorb = vec3(0.35, 0.62, 0.75);
float fog = clamp(1.0 - exp(-dist * 0.00022), 0.0, 0.9);
return mix(color * absorb, vec3(0.02, 0.10, 0.16), fog);
}
// Water.frag: fresnel-mixed sky reflection and depth-graded seabed refraction,
// SAGE diffuse + specular lighting, cloud term, depth-based transparency fade.
vec3 water_shade(vec3 hitpos, vec3 dir, float dist) {
float time = u_misc.x;
float river = u_misc.y;
float seabed = world_height(hitpos.x, hitpos.y);
float depth = max(0.0, u_params.z - seabed);
vec3 n = water_normal(hitpos.xy, time + water_distortion(hitpos.xy, time));
vec3 sun_dir = normalize(u_sun.xyz);
float cos_theta = clamp(dot(-dir, n), 0.0, 1.0);
float fresnel = 0.02 + 0.98 * pow(1.0 - cos_theta, 5.0);
vec3 reflection = sky_color(reflect(dir, n));
vec3 shallow = vec3(0.10, 0.34, 0.38);
vec3 deep = vec3(0.02, 0.12, 0.22);
vec3 refraction = mix(shallow, deep, clamp(depth / 40.0, 0.0, 1.0));
float ndotl = max(dot(n, sun_dir), 0.0);
float ambient = u_sun.w;
vec3 diffuse = vec3(ambient + (1.0 - ambient) * ndotl);
vec3 half_v = normalize(sun_dir - dir);
float spec = pow(max(dot(n, half_v), 0.0), 90.0);
vec3 color = mix(refraction, reflection, clamp(fresnel, 0.0, 1.0)) * diffuse * water_cloud(hitpos.xy, time);
color += vec3(1.0, 0.97, 0.9) * spec * 0.45;
float alpha = clamp(depth / WATER_TRANSPARENT_DEPTH, 0.0, 1.0) * WATER_MIN_OPACITY;
if (river > 0.5) alpha *= WATER_RIVER_MULTIPLIER;
color = mix(refraction, color, clamp(alpha + 0.15, 0.0, 1.0));
float wfog = clamp(1.0 - exp(-dist * 0.00009), 0.0, 0.75);
return mix(color, sky_color(vec3(dir.x, dir.y, 0.0)), wfog);
}
void main() {
vec4 p = u_cam;
float pitch = clamp(u_params.x, 0.15, 1.45);
@@ -105,52 +196,78 @@ void main() {
vec3 dir = normalize(fwd + right * ndc.x * th * aspect + up * ndc.y * th);
if (dir.z >= -1e-4) {
frag_color = vec4(sky_color(dir), 1.0);
frag_color = vec4(apply_underwater(sky_color(dir), 0.0, cam.z, u_params.z), 1.0);
return;
}
// March the heightfield. The step grows quickly: the map diagonal is only
// ~9000 world units, so marching past ~20000 adds cost without detail, and
// a 6% growth rate more than doubles the worst-case iteration count.
float t = CELL * 0.5;
float dt = CELL * 0.5;
// Clip the ray to the map's XY rectangle: the boundary is an exact plane,
// so the silhouette there stays razor-sharp instead of stair-stepping
// across it. Outside the map is sky.
float t_enter = 0.0;
float t_exit = 1.0e30;
bool inside = true;
if (abs(dir.x) < 1e-6) {
inside = (cam.x >= 0.0 && cam.x <= world_w);
} else {
float a = (0.0 - cam.x) / dir.x;
float b = (world_w - cam.x) / dir.x;
t_enter = max(t_enter, min(a, b));
t_exit = min(t_exit, max(a, b));
}
if (inside) {
if (abs(dir.y) < 1e-6) {
inside = (cam.y >= 0.0 && cam.y <= world_h);
} else {
float a = (0.0 - cam.y) / dir.y;
float b = (world_h - cam.y) / dir.y;
t_enter = max(t_enter, min(a, b));
t_exit = min(t_exit, max(a, b));
}
}
if (!inside || t_exit <= 0.0) {
frag_color = vec4(apply_underwater(sky_color(dir), 0.0, cam.z, u_params.z), 1.0);
return;
}
// March the heightfield cell by cell: the step is never longer than the
// time to cross one cell (dominant horizontal axis), while a clearance term
// lets the ray skip the empty air above the surface. Resolving every cell is
// what keeps cliff and map-edge silhouettes from quantising into huge
// stair-steps that crawl as the camera pans.
float horiz = max(abs(dir.x), abs(dir.y));
float cell_step = min(CELL / max(horiz, 1e-4), CELL * 32.0);
float t = max(t_enter, CELL * 0.5);
float prev = t;
bool hit = false;
float hit_t = 0.0;
for (int i = 0; i < 256 && t < 20000.0; ++i) {
for (int i = 0; i < 1024 && t <= t_exit; ++i) {
vec3 w = cam + dir * t;
if (w.x < 0.0 || w.y < 0.0 || w.x >= world_w || w.y >= world_h) {
prev = t;
dt *= 1.10;
t += dt;
continue;
}
if (u_params.w > 0.5 && w.z <= u_params.z) {
hit = true;
hit_t = t;
break;
}
if (w.z <= world_height(w.x, w.y)) {
float h = world_height(w.x, w.y);
float surface = (u_params.w > 0.5) ? max(h, u_params.z) : h;
if (w.z <= surface) {
hit = true;
hit_t = t;
break;
}
float clearance = (w.z - surface) / max(-dir.z, 1e-4);
prev = t;
dt *= 1.10;
t += dt;
t += clamp(clearance, cell_step, cell_step * 8.0);
}
if (!hit) {
frag_color = vec4(sky_color(dir), 1.0);
frag_color = vec4(apply_underwater(sky_color(dir), 0.0, cam.z, u_params.z), 1.0);
return;
}
// Refine the first crossing; with a sub-cell bracket this converges to the
// exact surface point.
float lo = prev;
float hi = hit_t;
for (int i = 0; i < 6; ++i) {
for (int i = 0; i < 18; ++i) {
float mid = 0.5 * (lo + hi);
vec3 w = cam + dir * mid;
bool water = u_params.w > 0.5 && w.z <= u_params.z;
if (water || w.z <= world_height(w.x, w.y)) {
float h = world_height(w.x, w.y);
float surface = (u_params.w > 0.5) ? max(h, u_params.z) : h;
if (w.z <= surface) {
hi = mid;
} else {
lo = mid;
@@ -162,20 +279,7 @@ void main() {
float ambient = u_sun.w;
if (u_params.w > 0.5 && hitpos.z <= u_params.z + 0.01) {
float time = u_misc.x;
vec2 q = hitpos.xy * 0.015;
float nx = sin(q.x * 1.3 + time * 1.7) + 0.5 * sin(q.x * 3.1 - time * 2.3);
float ny = sin(q.y * 1.1 - time * 1.3) + 0.5 * sin(q.y * 2.7 + time * 1.9);
vec3 n = normalize(vec3(nx * 0.06, ny * 0.06, 1.0));
float fres = pow(1.0 - clamp(-dir.z, 0.0, 1.0), 3.0);
vec3 deep = vec3(0.03, 0.16, 0.28);
vec3 refl = sky_color(reflect(dir, n));
float lam = max(0.0, dot(n, sun));
vec3 water = mix(deep, refl, clamp(0.25 + 0.55 * fres, 0.0, 0.9));
water += vec3(1.0, 0.98, 0.9) * pow(lam, 64.0) * 0.6;
float wfog = clamp(1.0 - exp(-hi * 0.00009), 0.0, 0.75);
water = mix(water, sky_color(vec3(dir.x, dir.y, 0.0)), wfog);
frag_color = vec4(water, 1.0);
frag_color = vec4(apply_underwater(water_shade(hitpos, dir, hi), hi, cam.z, u_params.z), 1.0);
return;
}
@@ -210,5 +314,5 @@ void main() {
vec3 lit = albedo * (ambient + (1.0 - ambient) * lambert);
float fog = clamp(1.0 - exp(-hi * 0.00009), 0.0, 0.75);
lit = mix(lit, sky_color(vec3(dir.x, dir.y, 0.0)), fog);
frag_color = vec4(lit, 1.0);
frag_color = vec4(apply_underwater(lit, hi, cam.z, u_params.z), 1.0);
}
+144 -39
View File
@@ -17,6 +17,12 @@ struct TerrainUniforms {
const CELL: f32 = 10.0; // must match ra3::terrain::cell_size
// SAGE water model constants (see docs/REVERSE_ENGINEERING.md).
const WATER_SCALE: f32 = 1.0 / 320.0;
const WATER_TRANSPARENT_DEPTH: f32 = 10.0;
const WATER_MIN_OPACITY: f32 = 0.70;
const WATER_RIVER_MULTIPLIER: f32 = 1.0;
fn cam_uniform() -> vec4<f32> { return u.data[0]; } // x=target_x, y=target_y, z=yaw, w=height
fn params_uniform() -> vec4<f32> { return u.data[1]; } // x=pitch, y=fov, z=water_z, w=has_water
fn sun_uniform() -> vec4<f32> { return u.data[2]; } // xyz=sun dir, w=ambient
@@ -100,6 +106,92 @@ fn sample_layer(layer: u32, wx: f32, wy: f32) -> vec3<f32> {
return vec3<f32>(c.b, c.g, c.r);
}
// ---- SAGE water (Ocean.fx / RiverWater.fx / Water.frag port) ----------------
// Sample an atlas layer by an explicit layer index (the water flow/bump maps
// are appended as the last two layers of the tile atlas). The atlas is
// 0xAARRGGBB, so the b/g/r swap restores RGB (as `sample_layer`).
fn water_tex(layer: i32, uv: vec2<f32>) -> vec3<f32> {
let lc = i32(textureNumLayers(u_atlas));
let l = clamp(layer, 0, max(lc - 1, 0));
let c = textureSampleLevel(u_atlas, u_atlas_samp, uv, l, 0.0);
return vec3<f32>(c.b, c.g, c.r);
}
// Scrolling wave normal on the water plane: the retail bump map (atlas's last
// layer), offset by the flow map (second-last layer) and combined with a
// de-gridded procedural wave so the sun glint is irregular and always moving.
fn water_normal(world_xy: vec2<f32>, time: f32) -> vec3<f32> {
let q = world_xy * (WATER_SCALE * 6.0);
let a1 = q.x * 0.80 + q.y * 0.30 + time * 1.10;
let a2 = q.y * 0.95 - q.x * 0.45 - time * 1.30;
let a3 = (q.x + q.y) * 1.60 + time * 2.10;
let a4 = (q.x - q.y) * 2.30 - time * 1.70;
let dx = 0.224 * cos(a1) - 0.099 * cos(a2) + 0.256 * cos(a3) + 0.230 * cos(a4);
let dy = 0.084 * cos(a1) + 0.209 * cos(a2) + 0.256 * cos(a3) - 0.230 * cos(a4);
let lc = i32(textureNumLayers(u_atlas));
let flow = water_tex(lc - 2, q - vec2<f32>(time * 0.010, time * 0.014)) * 2.0 - 1.0;
let bump = water_tex(lc - 1, q + flow.xy * 0.05 + vec2<f32>(time * 0.006, time * 0.008)) * 2.0 - 1.0;
let sx = -dx * 0.30 + bump.x * 0.45;
let sy = -dy * 0.30 + bump.y * 0.45;
return normalize(vec3<f32>(sx, sy, 1.0));
}
// Water.frag distortionPower * the flow texture: a small scrolling UV offset.
fn water_distortion(world_xy: vec2<f32>, time: f32) -> f32 {
let q = world_xy * (WATER_SCALE * 6.0);
return 0.05 * (sin(q.x * 0.9 + time * 0.7) + sin(q.y * 1.1 - time * 0.5));
}
// Water.frag GetCloudColor: no cloud texture is bound, so a slow scroll stands in.
fn water_cloud(world_xy: vec2<f32>, time: f32) -> vec3<f32> {
return vec3<f32>(0.85 + 0.15 * sin((world_xy.x + world_xy.y) * 0.0007 - time * 0.05));
}
// Retail UnderwaterDeferred.fx: absorbs red and fogs with distance.
fn apply_underwater(color: vec3<f32>, distance: f32, cam_z: f32, water_z: f32) -> vec3<f32> {
if (cam_z >= water_z - 0.5) { return color; }
let absorb = vec3<f32>(0.35, 0.62, 0.75);
let fog = clamp(1.0 - exp(-distance * 0.00022), 0.0, 0.9);
return mix(color * absorb, vec3<f32>(0.02, 0.10, 0.16), fog);
}
// Water.frag: fresnel-mixed sky reflection and depth-graded seabed refraction,
// SAGE diffuse + specular lighting, cloud term, depth-based transparency fade.
fn water_shade(hitpos: vec3<f32>, dir: vec3<f32>, distance: f32) -> vec3<f32> {
let time = misc_uniform().x;
let river = misc_uniform().y;
let seabed = world_height(hitpos.x, hitpos.y);
let depth = max(0.0, params_uniform().z - seabed);
let n = water_normal(hitpos.xy, time + water_distortion(hitpos.xy, time));
let sun_dir = normalize(sun_uniform().xyz);
let cos_theta = clamp(dot(-dir, n), 0.0, 1.0);
let fresnel = 0.02 + 0.98 * pow(1.0 - cos_theta, 5.0);
let reflection = sky_color(reflect(dir, n));
let shallow = vec3<f32>(0.10, 0.34, 0.38);
let deep = vec3<f32>(0.02, 0.12, 0.22);
let refraction = mix(shallow, deep, clamp(depth / 40.0, 0.0, 1.0));
let ndotl = max(dot(n, sun_dir), 0.0);
let ambient = sun_uniform().w;
let diffuse = vec3<f32>(ambient + (1.0 - ambient) * ndotl);
let half_v = normalize(sun_dir - dir);
let spec = pow(max(dot(n, half_v), 0.0), 90.0);
var color = mix(refraction, reflection, clamp(fresnel, 0.0, 1.0)) * diffuse * water_cloud(hitpos.xy, time);
color += vec3<f32>(1.0, 0.97, 0.9) * spec * 0.45;
var alpha = clamp(depth / WATER_TRANSPARENT_DEPTH, 0.0, 1.0) * WATER_MIN_OPACITY;
if (river > 0.5) { alpha *= WATER_RIVER_MULTIPLIER; }
color = mix(refraction, color, clamp(alpha + 0.15, 0.0, 1.0));
let wfog = clamp(1.0 - exp(-distance * 0.00009), 0.0, 0.75);
return mix(color, sky_color(vec3<f32>(dir.x, dir.y, 0.0)), wfog);
}
@fragment
fn fs_main(in: TerrainOut) -> @location(0) vec4<f32> {
let p = cam_uniform();
@@ -124,49 +216,75 @@ fn fs_main(in: TerrainOut) -> @location(0) vec4<f32> {
let dir = normalize(fwd + right * ndc.x * th * aspect + up * ndc.y * th);
if (dir.z >= -1e-4) {
return vec4<f32>(sky_color(dir), 1.0);
return vec4<f32>(apply_underwater(sky_color(dir), 0.0, cam.z, params_uniform().z), 1.0);
}
// March the heightfield. The step grows quickly: the map diagonal is only
// ~9000 world units, so marching past ~20000 adds cost without detail.
var t = CELL * 0.5;
var dt = CELL * 0.5;
// Clip the ray to the map's XY rectangle: the boundary is an exact plane,
// so the silhouette there stays razor-sharp instead of stair-stepping
// across it. Outside the map is sky.
var t_enter = 0.0;
var t_exit = 1.0e30;
var inside = true;
if (abs(dir.x) < 1e-6) {
inside = (cam.x >= 0.0 && cam.x <= world_w);
} else {
let a = (0.0 - cam.x) / dir.x;
let b = (world_w - cam.x) / dir.x;
t_enter = max(t_enter, min(a, b));
t_exit = min(t_exit, max(a, b));
}
if (inside) {
if (abs(dir.y) < 1e-6) {
inside = (cam.y >= 0.0 && cam.y <= world_h);
} else {
let a = (0.0 - cam.y) / dir.y;
let b = (world_h - cam.y) / dir.y;
t_enter = max(t_enter, min(a, b));
t_exit = min(t_exit, max(a, b));
}
}
if (!inside || t_exit <= 0.0) {
return vec4<f32>(apply_underwater(sky_color(dir), 0.0, cam.z, params_uniform().z), 1.0);
}
// March the heightfield cell by cell: the step is never longer than the
// time to cross one cell (dominant horizontal axis), while a clearance term
// lets the ray skip the empty air above the surface. Resolving every cell is
// what keeps cliff and map-edge silhouettes from quantising into huge
// stair-steps that crawl as the camera pans.
let horiz = max(abs(dir.x), abs(dir.y));
let cell_step = min(CELL / max(horiz, 1e-4), CELL * 32.0);
var t = max(t_enter, CELL * 0.5);
var prev = t;
var hit = false;
var hit_t = 0.0;
for (var i = 0; i < 256 && t < 20000.0; i = i + 1) {
for (var i = 0; i < 1024 && t <= t_exit; i = i + 1) {
let w = cam + dir * t;
if (w.x < 0.0 || w.y < 0.0 || w.x >= world_w || w.y >= world_h) {
prev = t;
dt *= 1.10;
t += dt;
continue;
}
if (params_uniform().w > 0.5 && w.z <= params_uniform().z) {
hit = true;
hit_t = t;
break;
}
if (w.z <= world_height(w.x, w.y)) {
let h = world_height(w.x, w.y);
let surface = select(h, max(h, params_uniform().z), params_uniform().w > 0.5);
if (w.z <= surface) {
hit = true;
hit_t = t;
break;
}
let clearance = (w.z - surface) / max(-dir.z, 1e-4);
prev = t;
dt *= 1.10;
t += dt;
t += clamp(clearance, cell_step, cell_step * 8.0);
}
if (!hit) {
return vec4<f32>(sky_color(dir), 1.0);
return vec4<f32>(apply_underwater(sky_color(dir), 0.0, cam.z, params_uniform().z), 1.0);
}
// Refine the first crossing; with a sub-cell bracket this converges to the
// exact surface point.
var lo = prev;
var hi = hit_t;
for (var i = 0; i < 6; i = i + 1) {
for (var i = 0; i < 18; i = i + 1) {
let mid = 0.5 * (lo + hi);
let w = cam + dir * mid;
let water = params_uniform().w > 0.5 && w.z <= params_uniform().z;
if (water || w.z <= world_height(w.x, w.y)) {
let h = world_height(w.x, w.y);
let surface = select(h, max(h, params_uniform().z), params_uniform().w > 0.5);
if (w.z <= surface) {
hi = mid;
} else {
lo = mid;
@@ -178,20 +296,7 @@ fn fs_main(in: TerrainOut) -> @location(0) vec4<f32> {
let ambient = sun_uniform().w;
if (params_uniform().w > 0.5 && hitpos.z <= params_uniform().z + 0.01) {
let time = misc_uniform().x;
let q = hitpos.xy * 0.015;
let nx = sin(q.x * 1.3 + time * 1.7) + 0.5 * sin(q.x * 3.1 - time * 2.3);
let ny = sin(q.y * 1.1 - time * 1.3) + 0.5 * sin(q.y * 2.7 + time * 1.9);
let n = normalize(vec3<f32>(nx * 0.06, ny * 0.06, 1.0));
let fres = pow(1.0 - clamp(-dir.z, 0.0, 1.0), 3.0);
let deep = vec3<f32>(0.03, 0.16, 0.28);
let refl = sky_color(reflect(dir, n));
let lam = max(0.0, dot(n, sun));
var water = mix(deep, refl, clamp(0.25 + 0.55 * fres, 0.0, 0.9));
water += vec3<f32>(1.0, 0.98, 0.9) * pow(lam, 64.0) * 0.6;
let wfog = clamp(1.0 - exp(-hi * 0.00009), 0.0, 0.75);
water = mix(water, sky_color(vec3<f32>(dir.x, dir.y, 0.0)), wfog);
return vec4<f32>(water, 1.0);
return vec4<f32>(apply_underwater(water_shade(hitpos, dir, hi), hi, cam.z, params_uniform().z), 1.0);
}
let wx = hitpos.x / CELL;
@@ -225,5 +330,5 @@ fn fs_main(in: TerrainOut) -> @location(0) vec4<f32> {
var lit = albedo * (ambient + (1.0 - ambient) * lambert);
let fog = clamp(1.0 - exp(-hi * 0.00009), 0.0, 0.75);
lit = mix(lit, sky_color(vec3<f32>(dir.x, dir.y, 0.0)), fog);
return vec4<f32>(lit, 1.0);
return vec4<f32>(apply_underwater(lit, hi, cam.z, params_uniform().z), 1.0);
}