The engine now runs on a plain Web Worker (no SDL, no PROXY_TO_PTHREAD: in a pthread Emscripten proxies every filesystem syscall to the main browser thread, where synchronous XHR - and thus FS.createLazyFile - is forbidden). A page/worker pair transfers an OffscreenCanvas and forwards DOM input; assets load lazily from an embedded manifest, so entering a map fetches only that map and its tiles. Presentation: ra3.webgpu (WebGPU via Emscripten's emdawnwebgpu port, WGSL shaders, the default on wasm) and ra3.wasmgl (WebGL2, GLSL ES). The legacy SDL ra3.webgl backend is removed.
239 lines
8.8 KiB
HLSL
239 lines
8.8 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. 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;
|
|
float prev = t;
|
|
bool hit = false;
|
|
float hit_t = 0.0;
|
|
for (int i = 0; i < 256 && t < 20000.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.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)) {
|
|
hit = true;
|
|
hit_t = t;
|
|
break;
|
|
}
|
|
prev = t;
|
|
dt *= 1.10;
|
|
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);
|
|
}
|