v0.3.0: display abstraction, map-browser menu, SAGE terrain tiling/blends
- ra3.client::display: shared interactive loops; SDL and Vulkan backends implement only the primitives (init/present/poll_event/window_size/ key_down/present_terrain). ra3.display picks the backend. - Menu: maps by localized name (gamestrings.csf), red/gold theme, hover highlight, mouse + keyboard, wheel scroll, fullscreen and FPS/vsync options, loading progress bar. - Terrain: continuous tile sampling via a texture array (REPEAT, uv = cell/(2*cellSize)) removes per-cell grid seams; SAGE blend ramp for material transitions; FPS label + top-right minimap overlays. - Skip the skirmish sim for map views; reuse the Vulkan texture; no idle terrain redraw.
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
#!/usr/bin/env sh
|
||||
# Regenerate the committed SPIR-V blobs under shaders/generated/.
|
||||
#
|
||||
# The build embeds the .spv files directly (see CMakeLists.txt), so no shader
|
||||
# compiler is needed to build OpenRA3. Run this only after editing a .vert/.frag.
|
||||
#
|
||||
# Needs one of: glslc (shaderc) or glslangValidator (glslang-tools). On a host
|
||||
# without either, use a throwaway container:
|
||||
# docker run --rm -v "$PWD/shaders:/s" ubuntu:26.04 bash -c \
|
||||
# "apt-get update -qq && apt-get install -y -qq glslang-tools && \
|
||||
# cd /s && sh ./compile.sh"
|
||||
set -eu
|
||||
|
||||
DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
OUT="$DIR/generated"
|
||||
mkdir -p "$OUT"
|
||||
|
||||
compile() {
|
||||
src="$1"
|
||||
dst="$2"
|
||||
if command -v glslc >/dev/null 2>&1; then
|
||||
glslc "$src" -o "$dst"
|
||||
else
|
||||
glslangValidator -V "$src" -o "$dst"
|
||||
fi
|
||||
}
|
||||
|
||||
for name in scene terrain; do
|
||||
compile "$DIR/$name.vert" "$OUT/$name.vert.spv"
|
||||
compile "$DIR/$name.frag" "$OUT/$name.frag.spv"
|
||||
done
|
||||
|
||||
echo "wrote $OUT/{scene,terrain}.{vert,frag}.spv"
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,11 @@
|
||||
#version 450
|
||||
|
||||
layout(binding = 0) uniform sampler2D scene;
|
||||
|
||||
layout(location = 0) in vec2 in_uv;
|
||||
layout(location = 0) out vec4 out_color;
|
||||
|
||||
void main() {
|
||||
if (in_uv.x < 0.0 || in_uv.x > 1.0 || in_uv.y < 0.0 || in_uv.y > 1.0) discard;
|
||||
out_color = texture(scene, in_uv);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
#version 450
|
||||
|
||||
// Fullscreen triangle. The scene image is drawn as a single textured quad; the
|
||||
// push constant carries the destination rectangle (in window-normalized
|
||||
// coordinates, y down), so the map is letterboxed rather than stretched.
|
||||
layout(push_constant) uniform Push {
|
||||
vec4 rect; // xy = top-left (0..1), zw = size (0..1)
|
||||
} pc;
|
||||
|
||||
layout(location = 0) out vec2 out_uv;
|
||||
|
||||
void main() {
|
||||
// Vulkan clip space has +Y pointing DOWN, so p already runs top->bottom and
|
||||
// matches the image's top-left origin; do NOT flip it (OpenGL would).
|
||||
vec2 p = vec2((gl_VertexIndex << 1) & 2, gl_VertexIndex & 2);
|
||||
out_uv = (p - pc.rect.xy) / pc.rect.zw;
|
||||
gl_Position = vec4(p * 2.0 - 1.0, 0.0, 1.0);
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
#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.
|
||||
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=unused, z=cells per texture 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
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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) {
|
||||
out_color = vec4(sky_color(dir), 1.0);
|
||||
return;
|
||||
}
|
||||
|
||||
// March the heightfield (bounded work: the step grows toward the horizon).
|
||||
float t = CELL * 0.5;
|
||||
float dt = CELL * 0.5;
|
||||
float prev = t;
|
||||
bool hit = false;
|
||||
float hit_t = 0.0;
|
||||
for (int i = 0; i < 512 && t < 60000.0; ++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.06; 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.06; t += dt;
|
||||
}
|
||||
if (!hit) { out_color = vec4(sky_color(dir), 1.0); return; }
|
||||
|
||||
float lo = prev, hi = hit_t;
|
||||
for (int i = 0; i < 6; ++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;
|
||||
}
|
||||
vec3 hitpos = cam + dir * hi;
|
||||
|
||||
vec3 sun = normalize(pc.sun.xyz);
|
||||
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);
|
||||
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(lit, 1.0);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
#version 450
|
||||
|
||||
// Fullscreen triangle; the terrain is ray-marched in the fragment shader
|
||||
// (GPU), so the vertex stage only emits the screen UV.
|
||||
layout(location = 0) out vec2 out_uv;
|
||||
|
||||
void main() {
|
||||
vec2 p = vec2((gl_VertexIndex << 1) & 2, gl_VertexIndex & 2);
|
||||
out_uv = p;
|
||||
gl_Position = vec4(p * 2.0 - 1.0, 0.0, 1.0);
|
||||
}
|
||||
Reference in New Issue
Block a user