#version 450 // GPU heightfield raymarcher for the real RA3 terrain. // // Textures: heightmap (R16), a per-cell blend record (R16G16B16A16_UNORM: base // layer, blend layer, three-way layer, packed direction/flags; unpacked with // *65535) and a texture array of the tile materials (RGBA8, mipmapped, REPEAT). // 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; layout(push_constant) uniform Push { vec4 cam; // x=target_x, y=target_y, z=yaw, w=height 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=water kind (0=ocean,1=river), z=cells per repeat, w=aspect } pc; layout(location = 0) in vec2 in_uv; layout(location = 0) out vec4 out_color; const float CELL = 10.0; // must match ra3::terrain::cell_size // Must match object.vert: the shared projection writes the view-space depth // into gl_FragDepth so the static-map models depth-test against the terrain. 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; } float world_height(float wx, float wy) { float world_w = pc.mapinfo.x * CELL; float world_h = pc.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(pc.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: 0 at one edge of the cell, 1 at the opposite. // Direction: 1 right, 2 top, 4 top-right, 8 top-left; flag bit 0 flips, // bit 1 marks a two-sided diagonal. 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; } // Sample one tile material layer at global cell coordinates. The texture repeats // every `span` cells (SAGE `uv / (cellSize * 2)`) with REPEAT addressing, so it // never restarts at a cell edge. vec3 sample_layer(uint layer, float wx, float wy) { float span = max(pc.misc.z, 1.0); int layer_count = textureSize(atlas, 0).z; float l = float(min(layer, uint(layer_count - 1))); 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); float fov = clamp(pc.params.y, 0.3, 1.4); float world_w = pc.mapinfo.x * CELL; float world_h = pc.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; float aspect = 1.0; // set by caller implicitly via square-ish UV; corrected below vec2 ndc = vec2(in_uv.x * 2.0 - 1.0, 1.0 - in_uv.y * 2.0); // aspect passed in misc.w aspect = pc.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) { gl_FragDepth = 1.0; out_color = vec4(apply_underwater(sky_color(dir), 0.0, cam.z, pc.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) { 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 < 1024 && t <= t_exit; ++i) { vec3 w = cam + dir * t; 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(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 < 18; ++i) { float mid = 0.5 * (lo + hi); vec3 w = cam + dir * 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; // View-space depth of the hit (project onto the forward axis), matching the // projection object.vert applies to the model vertices. gl_FragDepth = clamp((dot(hitpos - cam, fwd) - NEAR) / (FAR - NEAR), 0.0, 1.0); vec3 sun = normalize(pc.sun.xyz); float ambient = pc.sun.w; if (pc.params.w > 0.5 && hitpos.z <= pc.params.z + 0.01) { out_color = vec4(apply_underwater(water_shade(hitpos, dir, hi), hi, cam.z, pc.params.z), 1.0); return; } // Terrain: read the per-cell blend record, sample the base/blend/three-way // material layers continuously and ramp between them across the cell. float wx = hitpos.x / CELL; float wy = (world_h - hitpos.y) / CELL; int cx = clamp(int(wx), 0, int(pc.mapinfo.x) - 1); int cy = clamp(int(wy), 0, int(pc.mapinfo.y) - 1); float fx = wx - floor(wx); float fy = wy - floor(wy); uvec4 record = uvec4(texelFetch(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); // Per-pixel normal from the heightfield. 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); // 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(apply_underwater(lit, hi, cam.z, pc.params.z), 1.0); }