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.
319 lines
12 KiB
GLSL
319 lines
12 KiB
GLSL
#version 300 es
|
|
// GPU heightfield raymarcher for the real RA3 terrain (the WebGL port of
|
|
// terrain.frag). The Vulkan push constants become a set of vec4 uniforms.
|
|
precision highp float;
|
|
precision highp int;
|
|
precision highp sampler2D;
|
|
precision highp sampler2DArray;
|
|
|
|
uniform sampler2D u_heightmap; // R16 heights
|
|
uniform sampler2D u_celldata; // per-cell blend record (RGBA16)
|
|
uniform sampler2DArray u_atlas; // tile material array (RGBA8)
|
|
|
|
// One contiguous array so the host can upload all five vec4s with a single
|
|
// glUniform4fv; the names keep the shader body identical to terrain.frag.
|
|
uniform vec4 u_data[5];
|
|
#define u_cam u_data[0] // x=target_x, y=target_y, z=yaw, w=height
|
|
#define u_params u_data[1] // x=pitch, y=fov, z=water_z, w=has_water
|
|
#define u_sun u_data[2] // xyz=sun dir, w=ambient
|
|
#define u_mapinfo u_data[3] // x=W, y=H, z=unused, w=z_scale
|
|
#define u_misc u_data[4] // x=time, y=unused, z=cells per texture repeat, w=aspect
|
|
|
|
in vec2 v_uv;
|
|
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;
|
|
}
|
|
|
|
float world_height(float wx, float wy) {
|
|
float world_w = u_mapinfo.x * CELL;
|
|
float world_h = u_mapinfo.y * CELL;
|
|
if (wx < 0.0 || wy < 0.0 || wx >= world_w || wy >= world_h) return -1.0e9;
|
|
ivec2 c = ivec2(int(wx / CELL), int((world_h - wy) / CELL));
|
|
return height_at(c);
|
|
}
|
|
|
|
vec3 sky_color(vec3 dir) {
|
|
vec3 d = normalize(dir);
|
|
vec3 sun_dir = normalize(u_sun.xyz);
|
|
float t = clamp(d.z, 0.0, 1.0);
|
|
vec3 horizon = vec3(0.70, 0.78, 0.85);
|
|
vec3 zenith = vec3(0.28, 0.48, 0.80);
|
|
vec3 col = mix(horizon, zenith, pow(t, 0.6));
|
|
float sun = max(dot(d, sun_dir), 0.0);
|
|
col += vec3(1.0, 0.95, 0.82) * pow(sun, 300.0) * 1.6; // sun disk
|
|
col += vec3(1.0, 0.90, 0.72) * pow(sun, 8.0) * 0.18; // glow
|
|
return col;
|
|
}
|
|
|
|
// The retail SAGE blend ramp (see terrain.frag).
|
|
float blend_factor(uint direction, uint flags, vec2 f) {
|
|
bool flipped = (flags & 1u) != 0u;
|
|
bool two_sided = (flags & 2u) != 0u;
|
|
if (flipped) {
|
|
if (direction == 1u) {
|
|
f.x = 1.0 - f.x;
|
|
} else if (direction == 2u || direction == 4u || direction == 8u) {
|
|
f.y = 1.0 - f.y;
|
|
}
|
|
}
|
|
if (direction == 1u) return f.x;
|
|
if (direction == 2u) return f.y;
|
|
if (direction == 4u) {
|
|
float s = (1.0 - f.x) + (1.0 - f.y);
|
|
return two_sided ? 1.0 - clamp(s - 1.0, 0.0, 1.0) : clamp(1.0 - s, 0.0, 1.0);
|
|
}
|
|
if (direction == 8u) {
|
|
float s = f.x + (1.0 - f.y);
|
|
return two_sided ? 1.0 - clamp(s - 1.0, 0.0, 1.0) : clamp(1.0 - s, 0.0, 1.0);
|
|
}
|
|
return 0.0;
|
|
}
|
|
|
|
vec3 sample_layer(uint layer, float wx, float wy) {
|
|
float span = max(u_misc.z, 1.0);
|
|
int layer_count = textureSize(u_atlas, 0).z;
|
|
float l = float(min(layer, uint(layer_count - 1)));
|
|
// The atlas is 0xAARRGGBB (BGRA in memory), uploaded as GL_RGBA.
|
|
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);
|
|
float fov = clamp(u_params.y, 0.3, 1.4);
|
|
float world_w = u_mapinfo.x * CELL;
|
|
float world_h = u_mapinfo.y * CELL;
|
|
|
|
float cp = cos(pitch);
|
|
vec3 fwd = vec3(cp * sin(p.z), cp * cos(p.z), -sin(pitch));
|
|
vec3 right = normalize(cross(fwd, vec3(0, 0, 1)));
|
|
vec3 up = cross(right, fwd);
|
|
|
|
float target_z = world_height(p.x, p.y);
|
|
if (target_z < -1.0e8) target_z = 0.0;
|
|
float dist = p.w / sin(pitch);
|
|
vec3 cam = vec3(p.x, p.y, target_z + p.w) - fwd * dist;
|
|
|
|
vec2 ndc = vec2(v_uv.x * 2.0 - 1.0, 1.0 - v_uv.y * 2.0);
|
|
float aspect = u_misc.w;
|
|
float th = tan(fov * 0.5);
|
|
vec3 dir = normalize(fwd + right * ndc.x * th * aspect + up * ndc.y * th);
|
|
|
|
if (dir.z >= -1e-4) {
|
|
frag_color = vec4(apply_underwater(sky_color(dir), 0.0, cam.z, u_params.z), 1.0);
|
|
return;
|
|
}
|
|
|
|
// 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 < 1024 && t <= t_exit; ++i) {
|
|
vec3 w = cam + dir * t;
|
|
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;
|
|
t += clamp(clearance, cell_step, cell_step * 8.0);
|
|
}
|
|
if (!hit) {
|
|
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 < 18; ++i) {
|
|
float mid = 0.5 * (lo + hi);
|
|
vec3 w = cam + dir * mid;
|
|
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;
|
|
}
|
|
}
|
|
vec3 hitpos = cam + dir * hi;
|
|
|
|
vec3 sun = normalize(u_sun.xyz);
|
|
float ambient = u_sun.w;
|
|
|
|
if (u_params.w > 0.5 && hitpos.z <= u_params.z + 0.01) {
|
|
frag_color = vec4(apply_underwater(water_shade(hitpos, dir, hi), hi, cam.z, u_params.z), 1.0);
|
|
return;
|
|
}
|
|
|
|
float wx = hitpos.x / CELL;
|
|
float wy = (world_h - hitpos.y) / CELL;
|
|
int cx = clamp(int(wx), 0, int(u_mapinfo.x) - 1);
|
|
int cy = clamp(int(wy), 0, int(u_mapinfo.y) - 1);
|
|
float fx = wx - floor(wx);
|
|
float fy = wy - floor(wy);
|
|
|
|
uvec4 record = uvec4(texelFetch(u_celldata, ivec2(cx, cy), 0) * 65535.0 + 0.5);
|
|
uint packed = record.w;
|
|
uint dir1 = packed & 0xFu;
|
|
uint flags1 = (packed >> 4u) & 0x3u;
|
|
uint dir2 = (packed >> 8u) & 0xFu;
|
|
uint flags2 = (packed >> 12u) & 0x3u;
|
|
|
|
vec2 fracUV = vec2(fx, fy);
|
|
vec3 c0 = sample_layer(record.x, wx, wy);
|
|
vec3 c1 = sample_layer(record.y, wx, wy);
|
|
vec3 c2 = sample_layer(record.z, wx, wy);
|
|
float f1 = blend_factor(dir1, flags1, fracUV);
|
|
float f2 = blend_factor(dir2, flags2, fracUV);
|
|
vec3 albedo = mix(mix(c0, c1, f1), c2, f2);
|
|
|
|
float hl = world_height(hitpos.x - CELL, hitpos.y);
|
|
float hr = world_height(hitpos.x + CELL, hitpos.y);
|
|
float hd = world_height(hitpos.x, hitpos.y - CELL);
|
|
float hu = world_height(hitpos.x, hitpos.y + CELL);
|
|
vec3 n = normalize(vec3(hl - hr, hd - hu, 2.0 * CELL));
|
|
float lambert = max(0.0, dot(n, sun));
|
|
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(apply_underwater(lit, hi, cam.z, u_params.z), 1.0);
|
|
}
|