c42d97af08
build / mod (push) Successful in 2m26s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
226 lines
9.6 KiB
WebGPU Shading Language
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);
|
|
}
|