Files
airdef/airdef-mod/shaders/effects.wgsl
T

226 lines
9.6 KiB
WebGPU Shading Language

// AirDef effects: a post pass over the world (render.set-post, AirDef fork).
// Blasts are noise volumes marched along the view ray (fireball turning into a smoke
// column), with a flash, a shock wave that bends the light and heat haze; engine
// flames and muzzle flashes glow. Everything is placed in the world and hidden by
// what stands in front of it (the world's depth).
//
// frame.params (vec4):
// [0] now (mod ticks), blasts, lights, -
// [1 + 2i], [2 + 2i] blast i: x, y, z, size | start tick, life ticks, kind, -
// [97 + 2j], [98 + 2j] light j: x, y, z, radius | r, g, b, intensity
const BLAST_BASE: i32 = 1;
const LIGHT_BASE: i32 = 97;
fn hash3(p: vec3<f32>) -> f32 {
let q = fract(p * 0.3183099 + vec3<f32>(0.11, 0.17, 0.13));
let r = q * 17.0;
return fract(r.x * r.y * r.z * (r.x + r.y + r.z));
}
fn noise3(x: vec3<f32>) -> f32 {
let i = floor(x);
let f = fract(x);
let u = f * f * (3.0 - 2.0 * f);
let a = mix(hash3(i), hash3(i + vec3<f32>(1.0, 0.0, 0.0)), u.x);
let b = mix(hash3(i + vec3<f32>(0.0, 1.0, 0.0)), hash3(i + vec3<f32>(1.0, 1.0, 0.0)), u.x);
let c = mix(hash3(i + vec3<f32>(0.0, 0.0, 1.0)), hash3(i + vec3<f32>(1.0, 0.0, 1.0)), u.x);
let d = mix(hash3(i + vec3<f32>(0.0, 1.0, 1.0)), hash3(i + vec3<f32>(1.0, 1.0, 1.0)), u.x);
return mix(mix(a, b, u.y), mix(c, d, u.y), u.z);
}
fn fbm(p: vec3<f32>) -> f32 {
var a = 0.5;
var s = 0.0;
var q = p;
for (var i = 0; i < 3; i++) {
s += a * noise3(q);
q = q * 2.03 + vec3<f32>(1.7, 9.2, 3.1);
a *= 0.5;
}
return s;
}
// The ray (from the camera, along d) through a sphere: entry and exit distances.
fn ray_sphere(d: vec3<f32>, c: vec3<f32>, r: f32) -> vec2<f32> {
let b = dot(-c, d);
let k = dot(c, c) - r * r;
let h = b * b - k;
if h < 0.0 { return vec2<f32>(-1.0, -1.0); }
let s = sqrt(h);
return vec2<f32>(-b - s, -b + s);
}
// The screen size (pixels) of a world radius at a world point.
fn screen_radius(world: vec3<f32>, radius: f32) -> f32 {
let right = (frame.view_inverse * vec4<f32>(1.0, 0.0, 0.0, 0.0)).xyz;
let a = project(world);
let b = project(world + right * radius);
return length((b.xy - a.xy) * frame.resolution.xy);
}
fn now() -> f32 {
return frame.params[0].x + frame.time.y;
}
// Fire's colour from its heat (0 cold soot .. 1 white-hot).
fn fire(heat: f32) -> vec3<f32> {
let h = clamp(heat, 0.0, 1.0);
let ember = vec3<f32>(0.35, 0.05, 0.01);
let orange = vec3<f32>(1.6, 0.55, 0.12);
let white = vec3<f32>(3.0, 2.4, 1.5);
return mix(mix(ember, orange, smoothstep(0.0, 0.5, h)), white, smoothstep(0.6, 1.0, h));
}
fn post(uv: vec2<f32>, pixel: vec2<f32>) -> vec4<f32> {
let t_now = now();
let blasts = i32(frame.params[0].y);
let lights = i32(frame.params[0].z);
let dir = view_dir(uv);
let px = uv * frame.resolution.xy;
// 1. Shock waves and heat haze bend where the world is sampled.
var offset = vec2<f32>(0.0);
for (var i = 0; i < blasts; i++) {
let a = frame.params[BLAST_BASE + 2 * i];
let b = frame.params[BLAST_BASE + 2 * i + 1];
let age = t_now - b.x;
if age < 0.0 { continue; }
let centre = a.xyz;
let p = project(centre);
if p.z <= 0.5 { continue; }
let to = px - p.xy * frame.resolution.xy;
let dist = length(to);
// The shock: a ring racing out, fading in a second.
if age < 24.0 {
let ring = screen_radius(centre, age * 3.2 * (0.6 + a.w * 0.1));
let width = 6.0 + ring * 0.08;
let x = (dist - ring) / width;
let strength = (1.0 - age / 24.0) * min(a.w / 4.0, 2.0) * 10.0;
offset += normalize(to + vec2<f32>(1e-4)) * exp(-x * x) * x * strength * frame.resolution.zw;
}
// Heat haze over a hot fireball.
let heat = 1.0 - smoothstep(0.0, 0.5, age / b.y);
let r = screen_radius(centre, a.w * 2.0);
if heat > 0.0 && dist < r {
let n = noise3(vec3<f32>(px * 0.04, t_now * 0.35)) - 0.5;
offset += vec2<f32>(n, -abs(n)) * heat * (1.0 - dist / r) * 6.0 * frame.resolution.zw;
}
}
let suv = clamp(uv + offset, vec2<f32>(0.0), vec2<f32>(1.0));
var color = scene(suv);
let wall = scene_distance(suv);
// 1b. Fire lights the land and the vehicles: blasts and flames as point lights on what
// the pixel sees, much stronger at night.
let night = clamp(0.35 - frame.time.z * 1.8, 0.0, 1.0);
let strength = mix(0.35, 2.2, night);
// Under the HD lighting (params[0].w) the engine lights the world from the same lights.
if wall < 1.0e5 && frame.params[0].w < 0.5 {
let p = dir * wall;
var light = vec3<f32>(0.0);
for (var i = 0; i < blasts; i++) {
let a = frame.params[BLAST_BASE + 2 * i];
let b = frame.params[BLAST_BASE + 2 * i + 1];
let age = t_now - b.x;
if age < 0.0 { continue; }
let heat = exp(-age / max(b.y * 0.12, 1.0));
let flash = exp(-age * 0.5) * 3.0;
let d = length(p - (a.xyz - frame.camera.xyz)) / (a.w * 3.0);
light += vec3<f32>(1.0, 0.55, 0.2) * (heat + flash) * a.w * 0.6 / (1.0 + d * d * 4.0);
}
for (var j = 0; j < lights; j++) {
let a = frame.params[LIGHT_BASE + 2 * j];
let b = frame.params[LIGHT_BASE + 2 * j + 1];
let d = length(p - (a.xyz - frame.camera.xyz)) / max(a.w * 6.0, 1.0);
light += b.rgb * b.w / (1.0 + d * d * 6.0);
}
color += color * light * strength + light * 0.02 * night;
}
// 2. Fireballs and their smoke, marched front to back.
var glow = vec3<f32>(0.0);
var trans = 1.0;
var lit = vec3<f32>(0.0);
let sun = max(frame.time.z, 0.15);
for (var i = 0; i < blasts; i++) {
let a = frame.params[BLAST_BASE + 2 * i];
let b = frame.params[BLAST_BASE + 2 * i + 1];
let age = t_now - b.x;
if age < 0.0 { continue; }
let life = b.y;
let k = clamp(age / life, 0.0, 1.0);
let size = a.w;
let seed = vec3<f32>(a.x * 0.37, a.y * 0.11, a.z * 0.53);
// The ball swells fast, then rises and spreads as smoke.
let radius = size * (0.3 + 0.9 * (1.0 - exp(-age / 3.0))) * (1.0 + k * 0.9);
let centre = a.xyz - frame.camera.xyz + vec3<f32>(0.0, k * k * size * 2.2, 0.0);
let span = ray_sphere(dir, centre, radius);
if span.y <= 0.0 { continue; }
let t0 = max(span.x, 0.0);
let t1 = min(span.y, wall);
if t1 <= t0 { continue; }
let heat = 1.0 - smoothstep(0.05, 0.55, k);
let fade = 1.0 - smoothstep(0.65, 1.0, k);
// Fewer steps for a fireball that is small on screen.
let on_screen = radius / max(length(centre), 1.0);
let steps = i32(clamp(on_screen * 40.0, 6.0, 14.0));
let ds = (t1 - t0) / f32(steps);
for (var s = 0; s < steps; s++) {
if trans < 0.02 { break; }
let p = dir * (t0 + (f32(s) + 0.5) * ds);
let local = (p - centre) / radius;
let r = length(local);
let n = fbm(local * 2.4 + seed + vec3<f32>(0.0, -age * 0.06, age * 0.02));
let shape = smoothstep(1.0, 0.25, r + (n - 0.5) * 1.1);
if shape <= 0.0 { continue; }
let density = shape * (1.2 + n * 2.0) * fade;
let absorb = 1.0 - exp(-density * ds * 1.4 / max(radius * 0.25, 0.5));
// Hot inside, soot outside; smoke lit from above by the sky.
let core = heat * (1.0 - r) * (0.6 + n);
let smoke = vec3<f32>(0.10, 0.095, 0.09) * (0.5 + 0.8 * sun * (0.5 + local.y * 0.5));
let c = mix(smoke, fire(core * 1.6), clamp(core * 2.2, 0.0, 1.0));
lit += trans * absorb * c;
trans *= 1.0 - absorb;
}
// Light thrown round by the fire.
let d = length(centre);
glow += vec3<f32>(1.0, 0.5, 0.15) * heat * heat * size * 0.05 / (1.0 + d * 0.03);
}
color = color * trans + lit;
// 3. Flashes: a bright bloom at the blast and the whole view lit up for a moment.
for (var i = 0; i < blasts; i++) {
let a = frame.params[BLAST_BASE + 2 * i];
let b = frame.params[BLAST_BASE + 2 * i + 1];
let age = t_now - b.x;
if age < 0.0 || age > 10.0 { continue; }
let p = project(a.xyz);
if p.z <= 0.5 { continue; }
let visible = select(0.25, 1.0, scene_distance(p.xy) > p.z - a.w);
let f = exp(-age * 0.7) * min(a.w / 4.0, 2.5) * visible;
let r = screen_radius(a.xyz, a.w * 2.5) + 20.0;
let dd = length(px - p.xy * frame.resolution.xy) / r;
color += vec3<f32>(1.0, 0.85, 0.6) * f * (exp(-dd * dd * 3.0) * 2.5 + exp(-dd * 0.8) * 0.25);
color += vec3<f32>(1.0, 0.8, 0.55) * f * 0.12 / (1.0 + p.z * 0.01);
}
// 4. Glows: engine flames and muzzle flashes.
for (var j = 0; j < lights; j++) {
let a = frame.params[LIGHT_BASE + 2 * j];
let b = frame.params[LIGHT_BASE + 2 * j + 1];
let p = project(a.xyz);
if p.z <= 0.3 { continue; }
if scene_distance(p.xy) < p.z - a.w * 1.5 { continue; }
let r = screen_radius(a.xyz, a.w) + 3.0;
let dd = length(px - p.xy * frame.resolution.xy) / r;
let flicker = 0.85 + 0.15 * noise3(vec3<f32>(a.xyz * 0.7 + vec3<f32>(t_now * 1.3)));
color += b.rgb * b.w * flicker * (exp(-dd * dd * 4.0) * 1.2 + exp(-dd * 1.2) * 0.18);
}
color += glow * 0.3;
// Soft roll-off so bright fire stays coloured instead of clipping flat.
color = color / (1.0 + max(color - vec3<f32>(1.0), vec3<f32>(0.0)) * 0.5);
return vec4<f32>(color, 1.0);
}