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.
230 lines
8.4 KiB
WebGPU Shading Language
230 lines
8.4 KiB
WebGPU Shading Language
// WebGPU counterpart of the GPU heightfield raymarcher (the WGSL port of
|
|
// webgl_terrain_frag.glsl / terrain.frag). The Vulkan push constants become a
|
|
// uniform buffer of five vec4s, laid out exactly as `ra3::wasmgl::detail::terrain_uniforms`.
|
|
//
|
|
// Bind group 0: uniform data[5], the R16 heightmap (u32), the RGBA16 cell record
|
|
// (vec4<u32>), the RGBA8 tile atlas (array), and an atlas sampler.
|
|
|
|
struct TerrainUniforms {
|
|
data: array<vec4<f32>, 5>,
|
|
};
|
|
|
|
@group(0) @binding(0) var<uniform> u: TerrainUniforms;
|
|
@group(0) @binding(1) var u_heightmap: texture_2d<u32>; // R16Uint heights
|
|
@group(0) @binding(2) var u_celldata: texture_2d<u32>; // RGBA16Uint blend record (texel is vec4<u32>)
|
|
@group(0) @binding(3) var u_atlas: texture_2d_array<f32>; // RGBA8 tile materials
|
|
@group(0) @binding(4) var u_atlas_samp: sampler;
|
|
|
|
const CELL: f32 = 10.0; // must match ra3::terrain::cell_size
|
|
|
|
fn cam_uniform() -> vec4<f32> { return u.data[0]; } // x=target_x, y=target_y, z=yaw, w=height
|
|
fn params_uniform() -> vec4<f32> { return u.data[1]; } // x=pitch, y=fov, z=water_z, w=has_water
|
|
fn sun_uniform() -> vec4<f32> { return u.data[2]; } // xyz=sun dir, w=ambient
|
|
fn mapinfo_uniform() -> vec4<f32> { return u.data[3]; }// x=W, y=H, z=unused, w=z_scale
|
|
fn misc_uniform() -> vec4<f32> { return u.data[4]; } // x=time, y=unused, z=cells per repeat, w=aspect
|
|
|
|
struct TerrainOut {
|
|
@builtin(position) position: vec4<f32>,
|
|
@location(0) uv: vec2<f32>,
|
|
};
|
|
|
|
@vertex
|
|
fn vs_main(@builtin(vertex_index) vertex_index: u32) -> TerrainOut {
|
|
let p = vec2<f32>(f32((vertex_index << 1u) & 2u), f32(vertex_index & 2u));
|
|
var out: TerrainOut;
|
|
out.uv = p;
|
|
out.position = vec4<f32>(p.x * 2.0 - 1.0, 1.0 - p.y * 2.0, 0.0, 1.0);
|
|
return out;
|
|
}
|
|
|
|
fn height_at(cell: vec2<i32>) -> f32 {
|
|
let limit = vec2<i32>(mapinfo_uniform().xy) - vec2<i32>(1);
|
|
let c = clamp(cell, vec2<i32>(0), limit);
|
|
return f32(textureLoad(u_heightmap, c, 0).r) * mapinfo_uniform().w;
|
|
}
|
|
|
|
fn world_height(wx: f32, wy: f32) -> f32 {
|
|
let world_w = mapinfo_uniform().x * CELL;
|
|
let world_h = mapinfo_uniform().y * CELL;
|
|
if (wx < 0.0 || wy < 0.0 || wx >= world_w || wy >= world_h) {
|
|
return -1.0e9;
|
|
}
|
|
let c = vec2<i32>(i32(wx / CELL), i32((world_h - wy) / CELL));
|
|
return height_at(c);
|
|
}
|
|
|
|
fn sky_color(dir: vec3<f32>) -> vec3<f32> {
|
|
let d = normalize(dir);
|
|
let sun_dir = normalize(sun_uniform().xyz);
|
|
let t = clamp(d.z, 0.0, 1.0);
|
|
let horizon = vec3<f32>(0.70, 0.78, 0.85);
|
|
let zenith = vec3<f32>(0.28, 0.48, 0.80);
|
|
var col = mix(horizon, zenith, pow(t, 0.6));
|
|
let sun = max(dot(d, sun_dir), 0.0);
|
|
col += vec3<f32>(1.0, 0.95, 0.82) * pow(sun, 300.0) * 1.6;
|
|
col += vec3<f32>(1.0, 0.90, 0.72) * pow(sun, 8.0) * 0.18;
|
|
return col;
|
|
}
|
|
|
|
// The retail SAGE blend ramp (see terrain.frag).
|
|
fn blend_factor(direction: u32, flags: u32, f_in: vec2<f32>) -> f32 {
|
|
var f = f_in;
|
|
let flipped = (flags & 1u) != 0u;
|
|
let 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) {
|
|
let s = (1.0 - f.x) + (1.0 - f.y);
|
|
return select(clamp(1.0 - s, 0.0, 1.0), 1.0 - clamp(s - 1.0, 0.0, 1.0), two_sided);
|
|
}
|
|
if (direction == 8u) {
|
|
let s = f.x + (1.0 - f.y);
|
|
return select(clamp(1.0 - s, 0.0, 1.0), 1.0 - clamp(s - 1.0, 0.0, 1.0), two_sided);
|
|
}
|
|
return 0.0;
|
|
}
|
|
|
|
fn sample_layer(layer: u32, wx: f32, wy: f32) -> vec3<f32> {
|
|
let span = max(misc_uniform().z, 1.0);
|
|
let layer_count = textureNumLayers(u_atlas);
|
|
let l = f32(min(layer, layer_count - 1u));
|
|
// The atlas is 0xAARRGGBB (BGRA in memory), uploaded as RGBA8.
|
|
let c = textureSampleLevel(u_atlas, u_atlas_samp, vec2<f32>(wx, wy) / span, i32(l), 0.0);
|
|
return vec3<f32>(c.b, c.g, c.r);
|
|
}
|
|
|
|
@fragment
|
|
fn fs_main(in: TerrainOut) -> @location(0) vec4<f32> {
|
|
let p = cam_uniform();
|
|
let pitch = clamp(params_uniform().x, 0.15, 1.45);
|
|
let fov = clamp(params_uniform().y, 0.3, 1.4);
|
|
let world_w = mapinfo_uniform().x * CELL;
|
|
let world_h = mapinfo_uniform().y * CELL;
|
|
|
|
let cp = cos(pitch);
|
|
let fwd = vec3<f32>(cp * sin(p.z), cp * cos(p.z), -sin(pitch));
|
|
let right = normalize(cross(fwd, vec3<f32>(0.0, 0.0, 1.0)));
|
|
let up = cross(right, fwd);
|
|
|
|
var target_z = world_height(p.x, p.y);
|
|
if (target_z < -1.0e8) { target_z = 0.0; }
|
|
let dist = p.w / sin(pitch);
|
|
let cam = vec3<f32>(p.x, p.y, target_z + p.w) - fwd * dist;
|
|
|
|
let ndc = vec2<f32>(in.uv.x * 2.0 - 1.0, 1.0 - in.uv.y * 2.0);
|
|
let aspect = misc_uniform().w;
|
|
let th = tan(fov * 0.5);
|
|
let dir = normalize(fwd + right * ndc.x * th * aspect + up * ndc.y * th);
|
|
|
|
if (dir.z >= -1e-4) {
|
|
return vec4<f32>(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.
|
|
var t = CELL * 0.5;
|
|
var dt = CELL * 0.5;
|
|
var prev = t;
|
|
var hit = false;
|
|
var hit_t = 0.0;
|
|
for (var i = 0; i < 256 && t < 20000.0; i = i + 1) {
|
|
let 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.10;
|
|
t += dt;
|
|
continue;
|
|
}
|
|
if (params_uniform().w > 0.5 && w.z <= params_uniform().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 vec4<f32>(sky_color(dir), 1.0);
|
|
}
|
|
|
|
var lo = prev;
|
|
var hi = hit_t;
|
|
for (var i = 0; i < 6; i = i + 1) {
|
|
let mid = 0.5 * (lo + hi);
|
|
let w = cam + dir * mid;
|
|
let water = params_uniform().w > 0.5 && w.z <= params_uniform().z;
|
|
if (water || w.z <= world_height(w.x, w.y)) {
|
|
hi = mid;
|
|
} else {
|
|
lo = mid;
|
|
}
|
|
}
|
|
let hitpos = cam + dir * hi;
|
|
|
|
let sun = normalize(sun_uniform().xyz);
|
|
let ambient = sun_uniform().w;
|
|
|
|
if (params_uniform().w > 0.5 && hitpos.z <= params_uniform().z + 0.01) {
|
|
let time = misc_uniform().x;
|
|
let q = hitpos.xy * 0.015;
|
|
let nx = sin(q.x * 1.3 + time * 1.7) + 0.5 * sin(q.x * 3.1 - time * 2.3);
|
|
let ny = sin(q.y * 1.1 - time * 1.3) + 0.5 * sin(q.y * 2.7 + time * 1.9);
|
|
let n = normalize(vec3<f32>(nx * 0.06, ny * 0.06, 1.0));
|
|
let fres = pow(1.0 - clamp(-dir.z, 0.0, 1.0), 3.0);
|
|
let deep = vec3<f32>(0.03, 0.16, 0.28);
|
|
let refl = sky_color(reflect(dir, n));
|
|
let lam = max(0.0, dot(n, sun));
|
|
var water = mix(deep, refl, clamp(0.25 + 0.55 * fres, 0.0, 0.9));
|
|
water += vec3<f32>(1.0, 0.98, 0.9) * pow(lam, 64.0) * 0.6;
|
|
let wfog = clamp(1.0 - exp(-hi * 0.00009), 0.0, 0.75);
|
|
water = mix(water, sky_color(vec3<f32>(dir.x, dir.y, 0.0)), wfog);
|
|
return vec4<f32>(water, 1.0);
|
|
}
|
|
|
|
let wx = hitpos.x / CELL;
|
|
let wy = (world_h - hitpos.y) / CELL;
|
|
let cx = clamp(i32(wx), 0, i32(mapinfo_uniform().x) - 1);
|
|
let cy = clamp(i32(wy), 0, i32(mapinfo_uniform().y) - 1);
|
|
let fx = wx - floor(wx);
|
|
let fy = wy - floor(wy);
|
|
|
|
let record = textureLoad(u_celldata, vec2<i32>(cx, cy), 0);
|
|
let packed = record.w;
|
|
let dir1 = packed & 0xFu;
|
|
let flags1 = (packed >> 4u) & 0x3u;
|
|
let dir2 = (packed >> 8u) & 0xFu;
|
|
let flags2 = (packed >> 12u) & 0x3u;
|
|
|
|
let frac_uv = vec2<f32>(fx, fy);
|
|
let c0 = sample_layer(record.x, wx, wy);
|
|
let c1 = sample_layer(record.y, wx, wy);
|
|
let c2 = sample_layer(record.z, wx, wy);
|
|
let f1 = blend_factor(dir1, flags1, frac_uv);
|
|
let f2 = blend_factor(dir2, flags2, frac_uv);
|
|
let albedo = mix(mix(c0, c1, f1), c2, f2);
|
|
|
|
let hl = world_height(hitpos.x - CELL, hitpos.y);
|
|
let hr = world_height(hitpos.x + CELL, hitpos.y);
|
|
let hd = world_height(hitpos.x, hitpos.y - CELL);
|
|
let hu = world_height(hitpos.x, hitpos.y + CELL);
|
|
let n = normalize(vec3<f32>(hl - hr, hd - hu, 2.0 * CELL));
|
|
let lambert = max(0.0, dot(n, sun));
|
|
var lit = albedo * (ambient + (1.0 - ambient) * lambert);
|
|
let fog = clamp(1.0 - exp(-hi * 0.00009), 0.0, 0.75);
|
|
lit = mix(lit, sky_color(vec3<f32>(dir.x, dir.y, 0.0)), fog);
|
|
return vec4<f32>(lit, 1.0);
|
|
}
|