- ra3.dx: D3D11 and D3D12 presentation backends (runtime HLSL via d3dcompiler); 2D image blit and the GPU heightfield terrain raymarch, with the corner minimap/FPS overlays. Non-Windows builds link a null fallback. - ra3.display: preferred backend plus ordered fallback (Vulkan/D3D11/D3D12/SDL); menu gains a Renderer option and the CLI gains --dx11/--dx12/--sdl, which the render command now honours too. - vendor libenderlog (MIT): every run writes openra3.log next to the exe and archives the previous run's log; records at warn and above carry a call stack (native fallback, since libc++ has no <stacktrace>). - Windows crash reporter writes openra3_crash.log (faulting module + backtrace); D3D/DXGI diagnostics are routed through the logger.
237 lines
8.6 KiB
HLSL
237 lines
8.6 KiB
HLSL
// GPU heightfield raymarcher for the Direct3D backends (the D3D port of
|
|
// terrain.vert/terrain.frag).
|
|
//
|
|
// 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, REPEAT). The
|
|
// material is sampled continuously (`uv = cell / span`), 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 Vulkan push constants (20 floats) become a constant buffer.
|
|
|
|
cbuffer TerrainCB : register(b0) {
|
|
float4 cam; // x=target_x, y=target_y, z=yaw, w=height
|
|
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
|
|
};
|
|
|
|
Texture2D<float> heightmap : register(t0);
|
|
Texture2D<float4> celldata : register(t1);
|
|
Texture2DArray<float4> atlas : register(t2);
|
|
SamplerState height_smp : register(s0);
|
|
SamplerState cell_smp : register(s1);
|
|
SamplerState atlas_smp : register(s2);
|
|
|
|
static const float CELL = 10.0; // must match ra3::terrain::cell_size
|
|
|
|
struct VSOut {
|
|
float4 pos : SV_Position;
|
|
float2 uv : TEXCOORD0;
|
|
};
|
|
|
|
VSOut VSMain(uint vertex_id : SV_VertexID) {
|
|
float2 p = float2((vertex_id << 1) & 2, vertex_id & 2);
|
|
VSOut o;
|
|
o.uv = p;
|
|
o.pos = float4(p.x * 2.0 - 1.0, 1.0 - p.y * 2.0, 0.0, 1.0);
|
|
return o;
|
|
}
|
|
|
|
float height_at(int2 c) {
|
|
c = clamp(c, int2(0, 0), int2((int) mapinfo.x - 1, (int) mapinfo.y - 1));
|
|
return heightmap.Load(int3(c, 0)) * 65535.0 * mapinfo.w;
|
|
}
|
|
|
|
float world_height(float wx, float wy) {
|
|
float world_w = mapinfo.x * CELL;
|
|
float world_h = mapinfo.y * CELL;
|
|
if (wx < 0.0 || wy < 0.0 || wx >= world_w || wy >= world_h) return -1.0e9;
|
|
int2 c = int2((int) (wx / CELL), (int) ((world_h - wy) / CELL));
|
|
return height_at(c);
|
|
}
|
|
|
|
float3 sky_color(float3 dir) {
|
|
float3 d = normalize(dir);
|
|
float3 sun_dir = normalize(sun.xyz);
|
|
float t = clamp(d.z, 0.0, 1.0);
|
|
float3 horizon = float3(0.70, 0.78, 0.85);
|
|
float3 zenith = float3(0.28, 0.48, 0.80);
|
|
float3 col = lerp(horizon, zenith, pow(t, 0.6));
|
|
float s = max(dot(d, sun_dir), 0.0);
|
|
col += float3(1.0, 0.95, 0.82) * pow(s, 300.0) * 1.6; // sun disk
|
|
col += float3(1.0, 0.90, 0.72) * pow(s, 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, float2 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 with REPEAT addressing, so it never restarts at a cell edge.
|
|
float3 sample_layer(uint layer, float wx, float wy) {
|
|
float span = max(misc.z, 1.0);
|
|
uint lw = 0;
|
|
uint lh = 0;
|
|
uint layer_count = 0;
|
|
atlas.GetDimensions(lw, lh, layer_count);
|
|
float l = (float) min(layer, layer_count > 0u ? layer_count - 1u : 0u);
|
|
return atlas.Sample(atlas_smp, float3(float2(wx, wy) / span, l)).rgb;
|
|
}
|
|
|
|
float4 PSMain(VSOut input) : SV_Target {
|
|
float4 p = cam;
|
|
float pitch = clamp(params.x, 0.15, 1.45);
|
|
float fov = clamp(params.y, 0.3, 1.4);
|
|
float world_w = mapinfo.x * CELL;
|
|
float world_h = mapinfo.y * CELL;
|
|
|
|
float cp = cos(pitch);
|
|
float3 fwd = float3(cp * sin(p.z), cp * cos(p.z), -sin(pitch));
|
|
float3 right = normalize(cross(fwd, float3(0, 0, 1)));
|
|
float3 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);
|
|
float3 cam_pos = float3(p.x, p.y, target_z + p.w) - fwd * dist;
|
|
|
|
float2 ndc = float2(input.uv.x * 2.0 - 1.0, 1.0 - input.uv.y * 2.0);
|
|
float aspect = misc.w;
|
|
float th = tan(fov * 0.5);
|
|
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);
|
|
}
|
|
|
|
// 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) {
|
|
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.06;
|
|
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)) {
|
|
hit = true;
|
|
hit_t = t;
|
|
break;
|
|
}
|
|
prev = t;
|
|
dt *= 1.06;
|
|
t += dt;
|
|
}
|
|
if (!hit) {
|
|
return float4(sky_color(dir), 1.0);
|
|
}
|
|
|
|
float lo = prev;
|
|
float hi = hit_t;
|
|
for (int i = 0; i < 6; ++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)) {
|
|
hi = mid;
|
|
} else {
|
|
lo = mid;
|
|
}
|
|
}
|
|
float3 hitpos = cam_pos + dir * hi;
|
|
|
|
float3 sun_dir = normalize(sun.xyz);
|
|
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);
|
|
}
|
|
|
|
// 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) mapinfo.x - 1);
|
|
int cy = clamp((int) wy, 0, (int) mapinfo.y - 1);
|
|
float fx = wx - floor(wx);
|
|
float fy = wy - floor(wy);
|
|
|
|
uint4 record = (uint4) (celldata.Load(int3(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;
|
|
|
|
float2 fracUV = float2(fx, fy);
|
|
float3 c0 = sample_layer(record.x, wx, wy);
|
|
float3 c1 = sample_layer(record.y, wx, wy);
|
|
float3 c2 = sample_layer(record.z, wx, wy);
|
|
float f1 = blend_factor(dir1, flags1, fracUV);
|
|
float f2 = blend_factor(dir2, flags2, fracUV);
|
|
float3 albedo = lerp(lerp(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);
|
|
float3 n = normalize(float3(hl - hr, hd - hu, 2.0 * CELL));
|
|
float lambert = max(0.0, dot(n, sun_dir));
|
|
float3 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 = lerp(lit, sky_color(float3(dir.x, dir.y, 0.0)), fog);
|
|
return float4(lit, 1.0);
|
|
}
|