mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-30 21:16:52 -06:00
531 lines
22 KiB
Rust
531 lines
22 KiB
Rust
//! Physically based shading for the Advanced 3D renderer (CPU reference; `crates/gpu`'s
|
||
//! `advanced3d.wgsl` repeats it step for step).
|
||
//!
|
||
//! - Direct light: Cook–Torrance microfacet specular with the GGX/Trowbridge–Reitz normal
|
||
//! distribution, the Smith–Schlick-GGX geometry term (`k = (r + 1)² / 8`) and Schlick's
|
||
//! Fresnel (`F0` = 0.04 for dielectrics, the base colour for metals), plus a Lambertian
|
||
//! diffuse lobe weighted by `(1 − F)(1 − metallic)` (Karis 2013, "Real Shading in Unreal
|
||
//! Engine 4"; the glTF 2.0 metallic-roughness model). Light colours are the irradiance on a
|
||
//! surface facing the light (a white 100% light shows a white diffuse surface at its base
|
||
//! colour), i.e. π is folded into the light intensity.
|
||
//! - Image-based light: a cosine-convolved irradiance map for diffuse, a box-filtered mip
|
||
//! chain of the equirectangular environment sampled by roughness for specular, and the
|
||
//! analytic split-sum environment BRDF fit (Karis 2014, "Physically Based Shading on
|
||
//! Mobile").
|
||
//! - Shadows: depth maps per light, percentage-closer filtered.
|
||
|
||
use super::scene::{EnvInfo, Light, Material, Scene, ShadowMap, TexInfo};
|
||
|
||
pub const PI: f32 = std::f32::consts::PI;
|
||
|
||
#[inline]
|
||
pub fn dot(a: [f32; 3], b: [f32; 3]) -> f32 {
|
||
a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
|
||
}
|
||
#[inline]
|
||
pub fn cross(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
|
||
[a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]]
|
||
}
|
||
#[inline]
|
||
pub fn norm(a: [f32; 3]) -> [f32; 3] {
|
||
let l = dot(a, a).sqrt();
|
||
if l > 1e-20 { [a[0] / l, a[1] / l, a[2] / l] } else { [0.0, 0.0, -1.0] }
|
||
}
|
||
#[inline]
|
||
fn add(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
|
||
[a[0] + b[0], a[1] + b[1], a[2] + b[2]]
|
||
}
|
||
#[inline]
|
||
fn sub(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
|
||
[a[0] - b[0], a[1] - b[1], a[2] - b[2]]
|
||
}
|
||
#[inline]
|
||
fn scale(a: [f32; 3], k: f32) -> [f32; 3] {
|
||
[a[0] * k, a[1] * k, a[2] * k]
|
||
}
|
||
#[inline]
|
||
fn mix(a: f32, b: f32, t: f32) -> f32 {
|
||
a + (b - a) * t
|
||
}
|
||
|
||
/// sRGB-encoded → linear light.
|
||
#[inline]
|
||
pub fn srgb_to_linear(c: f32) -> f32 {
|
||
if c <= 0.04045 { c / 12.92 } else { ((c + 0.055) / 1.055).powf(2.4) }
|
||
}
|
||
/// Linear light → sRGB-encoded (over-range values continue the curve).
|
||
#[inline]
|
||
pub fn linear_to_srgb(c: f32) -> f32 {
|
||
if c <= 0.0031308 { c * 12.92 } else { 1.055 * c.powf(1.0 / 2.4) - 0.055 }
|
||
}
|
||
|
||
/// GGX / Trowbridge–Reitz normal distribution, `a = roughness²`.
|
||
#[inline]
|
||
pub fn d_ggx(n_h: f32, a: f32) -> f32 {
|
||
let a2 = a * a;
|
||
let d = n_h * n_h * (a2 - 1.0) + 1.0;
|
||
a2 / (PI * d * d).max(1e-12)
|
||
}
|
||
|
||
/// Smith–Schlick-GGX geometry term for direct light (`k = (r + 1)² / 8`).
|
||
#[inline]
|
||
pub fn g_smith(n_v: f32, n_l: f32, rough: f32) -> f32 {
|
||
let k = (rough + 1.0) * (rough + 1.0) / 8.0;
|
||
let g1 = |x: f32| x / (x * (1.0 - k) + k);
|
||
g1(n_v) * g1(n_l)
|
||
}
|
||
|
||
/// Schlick's Fresnel approximation.
|
||
#[inline]
|
||
pub fn f_schlick(f0: [f32; 3], v_h: f32) -> [f32; 3] {
|
||
let k = (1.0 - v_h).clamp(0.0, 1.0).powi(5);
|
||
[f0[0] + (1.0 - f0[0]) * k, f0[1] + (1.0 - f0[1]) * k, f0[2] + (1.0 - f0[2]) * k]
|
||
}
|
||
|
||
/// Cook–Torrance BRDF × N·L × π for one light: (diffuse colour factor, specular colour).
|
||
/// Returned as the reflected fraction of the light's irradiance.
|
||
pub fn brdf(base: [f32; 3], metallic: f32, rough: f32, spec_k: f32, diff_k: f32, n: [f32; 3], v: [f32; 3], l: [f32; 3]) -> [f32; 3] {
|
||
let n_l = dot(n, l);
|
||
if n_l <= 0.0 {
|
||
return [0.0; 3];
|
||
}
|
||
let n_v = dot(n, v).max(1e-4);
|
||
let h = norm(add(v, l));
|
||
let n_h = dot(n, h).max(0.0);
|
||
let v_h = dot(v, h).max(0.0);
|
||
let r = rough.clamp(0.03, 1.0);
|
||
let f0 = [mix(0.04 * spec_k, base[0], metallic), mix(0.04 * spec_k, base[1], metallic), mix(0.04 * spec_k, base[2], metallic)];
|
||
let f = f_schlick(f0, v_h);
|
||
let d = d_ggx(n_h, r * r);
|
||
let g = g_smith(n_v, n_l, r);
|
||
let spec = d * g / (4.0 * n_v * n_l).max(1e-4);
|
||
let mut out = [0.0; 3];
|
||
for c in 0..3 {
|
||
let kd = (1.0 - f[c]) * (1.0 - metallic);
|
||
out[c] = (kd * base[c] / PI * diff_k + f[c] * spec) * n_l * PI;
|
||
}
|
||
out
|
||
}
|
||
|
||
/// Split-sum environment BRDF (scale, bias) for `F0·A + B` (analytic fit).
|
||
#[inline]
|
||
pub fn env_brdf(n_v: f32, rough: f32) -> (f32, f32) {
|
||
let c0 = [-1.0, -0.0275, -0.572, 0.022];
|
||
let c1 = [1.0, 0.0425, 1.04, -0.04];
|
||
let r = [rough * c0[0] + c1[0], rough * c0[1] + c1[1], rough * c0[2] + c1[2], rough * c0[3] + c1[3]];
|
||
let a004 = (r[0] * r[0]).min((-9.28 * n_v).exp2()) * r[0] + r[1];
|
||
(-1.04 * a004 + r[2], 1.04 * a004 + r[3])
|
||
}
|
||
|
||
/// Wrap a texel index.
|
||
#[inline]
|
||
fn wrap(i: i64, n: i64, mode: u32) -> i64 {
|
||
match mode {
|
||
1 => i.clamp(0, n - 1),
|
||
2 => {
|
||
let m = i.rem_euclid(2 * n);
|
||
if m >= n { 2 * n - 1 - m } else { m }
|
||
}
|
||
_ => i.rem_euclid(n),
|
||
}
|
||
}
|
||
|
||
/// Bilinear texture sample at (u, v) (0..1 across the texture, texel centres at +0.5).
|
||
pub fn sample(t: &TexInfo, texels: &[[f32; 4]], u: f32, v: f32) -> [f32; 4] {
|
||
let (w, h) = (t.width as i64, t.height as i64);
|
||
if w == 0 || h == 0 {
|
||
return [1.0; 4];
|
||
}
|
||
let x = u * w as f32 - 0.5;
|
||
let y = v * h as f32 - 0.5;
|
||
let (x0, y0) = (x.floor(), y.floor());
|
||
let (fx, fy) = (x - x0, y - y0);
|
||
let (x0, y0) = (x0 as i64, y0 as i64);
|
||
let get = |xi: i64, yi: i64| -> [f32; 4] {
|
||
let xi = wrap(xi, w, t.wrap_u);
|
||
let yi = wrap(yi, h, t.wrap_v);
|
||
texels.get(t.offset as usize + (yi * w + xi) as usize).copied().unwrap_or([0.0; 4])
|
||
};
|
||
let (a, b, c, d) = (get(x0, y0), get(x0 + 1, y0), get(x0, y0 + 1), get(x0 + 1, y0 + 1));
|
||
let mut o = [0.0; 4];
|
||
for k in 0..4 {
|
||
let top = a[k] + (b[k] - a[k]) * fx;
|
||
let bot = c[k] + (d[k] - c[k]) * fx;
|
||
o[k] = top + (bot - top) * fy;
|
||
}
|
||
o
|
||
}
|
||
|
||
/// Equirectangular coordinates of a world direction (AE world: +Y down; the environment's up
|
||
/// is −Y), rotated about the vertical axis by `rot` radians.
|
||
#[inline]
|
||
pub fn equirect_uv(d: [f32; 3], rot: f32) -> (f32, f32) {
|
||
let u = 0.5 + (d[0].atan2(d[2]) + rot) / (2.0 * PI);
|
||
let v = (-d[1]).clamp(-1.0, 1.0).acos() / PI;
|
||
(u - u.floor(), v)
|
||
}
|
||
|
||
/// Inverse of [`equirect_uv`] (texel centre → direction).
|
||
pub fn equirect_dir(u: f32, v: f32, rot: f32) -> [f32; 3] {
|
||
let phi = (u - 0.5) * 2.0 * PI - rot;
|
||
let th = v * PI;
|
||
[th.sin() * phi.sin(), -th.cos(), th.sin() * phi.cos()]
|
||
}
|
||
|
||
/// Diffuse (irradiance) and roughness-filtered specular radiance of the environment.
|
||
pub fn env_light(s: &Scene, e: &EnvInfo, n: [f32; 3], r: [f32; 3], rough: f32) -> ([f32; 3], [f32; 3]) {
|
||
let (u, v) = equirect_uv(n, e.rotation);
|
||
let irr = s.textures.get(e.irradiance as usize).map_or([0.0; 4], |t| sample(t, &s.texels, u, v));
|
||
let (u, v) = equirect_uv(r, e.rotation);
|
||
let lv = rough.clamp(0.0, 1.0) * (e.mips.saturating_sub(1)) as f32;
|
||
let l0 = lv.floor() as u32;
|
||
let l1 = (l0 + 1).min(e.mips.saturating_sub(1));
|
||
let f = lv - l0 as f32;
|
||
let a = s.textures.get((e.radiance + l0) as usize).map_or([0.0; 4], |t| sample(t, &s.texels, u, v));
|
||
let b = s.textures.get((e.radiance + l1) as usize).map_or([0.0; 4], |t| sample(t, &s.texels, u, v));
|
||
let spec = [mix(a[0], b[0], f), mix(a[1], b[1], f), mix(a[2], b[2], f)];
|
||
(scale([irr[0], irr[1], irr[2]], e.intensity), scale(spec, e.intensity))
|
||
}
|
||
|
||
/// Light-space position (u, v in shadow-map pixels, depth) of a world point.
|
||
#[inline]
|
||
pub fn shadow_coord(m: &ShadowMap, p: [f32; 3]) -> Option<(f32, f32, f32)> {
|
||
let v = &m.view;
|
||
let q = [
|
||
v[0][0] * p[0] + v[0][1] * p[1] + v[0][2] * p[2] + v[0][3],
|
||
v[1][0] * p[0] + v[1][1] * p[1] + v[1][2] * p[2] + v[1][3],
|
||
v[2][0] * p[0] + v[2][1] * p[1] + v[2][2] * p[2] + v[2][3],
|
||
];
|
||
let c = m.size as f32 * 0.5;
|
||
if m.ortho {
|
||
Some((q[0] * m.focal + c, q[1] * m.focal + c, q[2]))
|
||
} else if q[2] > 1e-3 {
|
||
Some((q[0] * m.focal / q[2] + c, q[1] * m.focal / q[2] + c, q[2]))
|
||
} else {
|
||
None
|
||
}
|
||
}
|
||
|
||
/// Integer tap jitter hash: pure u32 bit math so the WGSL backend computes
|
||
/// bit-identical jitter from the same inputs (a float hash would diverge between
|
||
/// backends and break CPU/GPU parity).
|
||
fn tap_hash(x: i32, y: i32) -> u32 {
|
||
let mut h = (x as u32).wrapping_mul(0x9E37_79B9) ^ (y as u32).wrapping_mul(0x85EB_CA6B);
|
||
h ^= h >> 16;
|
||
h = h.wrapping_mul(0x2545_F491);
|
||
h ^= h >> 15;
|
||
h
|
||
}
|
||
|
||
/// Upper bound of the PCF grid's taps per axis. The grid grows with the reach so taps
|
||
/// stay about one texel apart for moderate penumbrae; the cap bounds the per-fragment
|
||
/// cost of very wide ones (bilinear taps still overlap and the jitter dithers the rest).
|
||
const PCF_MAX_TAPS: i32 = 10;
|
||
|
||
/// Steepest receiver-plane slope the PCF extrapolates, in texel footprints of depth per
|
||
/// texel (about 89°). Steeper planes are edge-on to the light, where their depth runs
|
||
/// away; they are clamped to this slope.
|
||
const PLANE_SLOPE_MAX: f32 = 64.0;
|
||
|
||
/// Fraction of light reaching `p` through shadow map `m` (1 = lit). `ng` is the
|
||
/// receiver's geometric (not normal-mapped) normal facing the viewer.
|
||
///
|
||
/// PCF over an N×N tap grid (5 ≤ N ≤ `PCF_MAX_TAPS`) spread over a reach derived
|
||
/// from `radius` (the wanted penumbra in output pixels) divided by the receiver's
|
||
/// texel footprint, each tap jittered by an integer hash of the receiver's
|
||
/// sub-texel cell and the tap index, and each tap comparing against the four
|
||
/// nearest texel centres with bilinear weights. The jitter detaches the tap grid
|
||
/// from the texel axes (axis-aligned binary taps are what stair-step a straight
|
||
/// shadow edge), and keying it below texel size keeps the dither finer than an
|
||
/// output pixel even where one texel covers several. The bilinear compare makes
|
||
/// each tap's contribution vary smoothly inside a texel instead of snapping.
|
||
///
|
||
/// Every texel is compared against the depth the receiver's own plane has at that
|
||
/// texel's centre, not the receiver point's depth: a wide kernel on a surface tilted
|
||
/// towards the light would otherwise see its own nearer depth and shadow itself.
|
||
/// With that, the bias stays at texel scale; a diffusion-scaled bias lifts shadows
|
||
/// off their casters.
|
||
pub fn shadow_factor(m: &ShadowMap, texels: &[f32], p: [f32; 3], ng: [f32; 3], n_l: f32) -> f32 {
|
||
let Some((u, v, z)) = shadow_coord(m, p) else { return 1.0 };
|
||
let size = m.size as i64;
|
||
// Slope-scaled bias at texel scale (world units are pixels), steeper for
|
||
// grazing light: it absorbs depth-raster error and the gap between the shading
|
||
// normal's plane and the faceted surface the map rasterised.
|
||
let texel = if m.ortho { 1.0 / m.focal } else { z.abs() / m.focal };
|
||
let bias = m.bias * texel * (1.0 + 3.0 * (1.0 - n_l.clamp(0.0, 1.0)));
|
||
let at = |xi: i64, yi: i64| -> f32 {
|
||
if xi < 0 || yi < 0 || xi >= size || yi >= size {
|
||
return f32::INFINITY; // outside the map: the light was never blocked there (WGSL mirrors this with a large finite depth)
|
||
}
|
||
texels.get(m.offset as usize + (yi * size + xi) as usize).copied().unwrap_or(f32::INFINITY)
|
||
};
|
||
// The receiver plane in light space as the slope of its depth across the map, in
|
||
// texel footprints per texel (`tx`, `ty`; depth grows along −t), clamped where the
|
||
// plane grazes the light. `den0` is the plane normal against the light ray through
|
||
// the receiver (the depth axis for an orthographic map), negative while it faces it.
|
||
let w = &m.view;
|
||
let nv = [0, 1, 2].map(|r| w[r][0] * ng[0] + w[r][1] * ng[1] + w[r][2] * ng[2]);
|
||
let c = m.size as f32 * 0.5;
|
||
let den0 = if m.ortho { nv[2] } else { nv[0] * ((u - c) / m.focal) + nv[1] * ((v - c) / m.focal) + nv[2] };
|
||
let (tx, ty) = if den0 < -1e-6 {
|
||
((nv[0] / den0).clamp(-PLANE_SLOPE_MAX, PLANE_SLOPE_MAX), (nv[1] / den0).clamp(-PLANE_SLOPE_MAX, PLANE_SLOPE_MAX))
|
||
} else {
|
||
(0.0, 0.0)
|
||
};
|
||
// 1 where texel (xi, yi) does not occlude the receiver plane at the texel's centre.
|
||
// Perspective depth is not linear across the map but its inverse is, so that
|
||
// compare runs on 1/depth (inv · (texel + bias) ≥ 1) without a division; a plane
|
||
// whose inverse depth reaches 0 has left the light's side and cannot be occluded.
|
||
let (iz, iq) = (1.0 / z, 1.0 / (m.focal * z));
|
||
let open = |xi: i64, yi: i64| -> f32 {
|
||
let s = at(xi, yi) + bias;
|
||
let g = tx * (xi as f32 + 0.5 - u) + ty * (yi as f32 + 0.5 - v);
|
||
let lit = if m.ortho {
|
||
z - g * texel <= s
|
||
} else {
|
||
let inv = iz + g * iq;
|
||
inv <= 0.0 || inv * s >= 1.0
|
||
};
|
||
if lit { 1.0 } else { 0.0 }
|
||
};
|
||
// `radius` is the wanted penumbra in output pixels; converting it through the
|
||
// receiver's texel footprint keeps the penumbra width in output pixels at any
|
||
// map resolution (and lets perspective penumbra grow with distance from the light).
|
||
// At least two texels: a hard edge then ramps across two texels, which hides the
|
||
// texel steps of the binary depth compare where one texel spans several output
|
||
// pixels (receivers grazing the light); a one-texel ramp leaves a sawtooth.
|
||
let mut reach = (m.radius / texel).clamp(2.0, 256.0);
|
||
if !reach.is_finite() {
|
||
reach = 1.0;
|
||
}
|
||
let n = (reach.ceil() as i32 + 1).clamp(5, PCF_MAX_TAPS);
|
||
let step = reach / (n - 1) as f32;
|
||
let half = (n - 1) as f32 * 0.5;
|
||
let (cx, cy) = ((u * 8.0).floor() as i32, (v * 8.0).floor() as i32);
|
||
let mut lit = 0.0;
|
||
for j in 0..n {
|
||
for i in 0..n {
|
||
let h = tap_hash(cx.wrapping_add(i.wrapping_mul(7919)), cy.wrapping_add(j.wrapping_mul(7919)));
|
||
let jx = ((h & 0xFFFF) as f32) * (1.0 / 65536.0) - 0.5;
|
||
let jy = (((h >> 16) & 0xFFFF) as f32) * (1.0 / 65536.0) - 0.5;
|
||
let (su, sv) = (u + ((i as f32 - half) + jx) * step, v + ((j as f32 - half) + jy) * step);
|
||
// Texel (x, y) holds the depth at its centre (x + 0.5, y + 0.5); each of the
|
||
// four is compared against the receiver plane at that centre.
|
||
let (tu, tv) = (su - 0.5, sv - 0.5);
|
||
let (fx, fy) = (tu - tu.floor(), tv - tv.floor());
|
||
let (x0, y0) = (tu.floor() as i64, tv.floor() as i64);
|
||
let (h00, h10, h01, h11) = (open(x0, y0), open(x0 + 1, y0), open(x0, y0 + 1), open(x0 + 1, y0 + 1));
|
||
let a = h00 + (h10 - h00) * fx;
|
||
let b = h01 + (h11 - h01) * fx;
|
||
lit += a + (b - a) * fy;
|
||
}
|
||
}
|
||
lit / (n * n) as f32
|
||
}
|
||
|
||
/// The visibility of light `li` at `p` (1 = unshadowed), darkened by Shadow Darkness.
|
||
pub fn light_shadow(s: &Scene, l: &Light, p: [f32; 3], ng: [f32; 3], n_l: f32) -> f32 {
|
||
if l.shadow < 0 {
|
||
return 1.0;
|
||
}
|
||
let first = l.shadow as usize;
|
||
let m = if l.kind == 2 {
|
||
// Point light: the cube face of the major axis of the light → point direction.
|
||
let d = sub(p, l.pos);
|
||
let (ax, ay, az) = (d[0].abs(), d[1].abs(), d[2].abs());
|
||
let face = if ax >= ay && ax >= az {
|
||
if d[0] > 0.0 { 0 } else { 1 }
|
||
} else if ay >= az {
|
||
if d[1] > 0.0 { 2 } else { 3 }
|
||
} else if d[2] > 0.0 {
|
||
4
|
||
} else {
|
||
5
|
||
};
|
||
s.shadows.get(first + face)
|
||
} else {
|
||
s.shadows.get(first)
|
||
};
|
||
let Some(m) = m else { return 1.0 };
|
||
let f = shadow_factor(m, &s.shadow_texels, p, ng, n_l);
|
||
1.0 - l.shadow_darkness * (1.0 - f)
|
||
}
|
||
|
||
/// Unit direction from `p` to light `l`, and the distance (infinite for a parallel light).
|
||
fn to_light(l: &Light, p: [f32; 3]) -> ([f32; 3], f32) {
|
||
if l.kind == 0 {
|
||
return (scale(l.dir, -1.0), f32::INFINITY);
|
||
}
|
||
let d = sub(l.pos, p);
|
||
let len = dot(d, d).sqrt();
|
||
if len < 1e-6 { ([0.0, 0.0, -1.0], 0.0) } else { (scale(d, 1.0 / len), len) }
|
||
}
|
||
|
||
/// Distance attenuation × spot cone of a light at `p` (AE falloff modes).
|
||
pub fn attenuation(l: &Light, p: [f32; 3], to_l: [f32; 3], dist: f32) -> f32 {
|
||
let mut k = match l.falloff {
|
||
1 if l.kind != 0 => {
|
||
if dist <= l.radius {
|
||
1.0
|
||
} else if l.falloff_distance <= 0.0 {
|
||
0.0
|
||
} else {
|
||
let t = ((dist - l.radius) / l.falloff_distance).clamp(0.0, 1.0);
|
||
1.0 - t * t * (3.0 - 2.0 * t)
|
||
}
|
||
}
|
||
2 if l.kind != 0 => {
|
||
if dist <= l.radius || (l.radius <= 0.0 && dist <= 1.0) {
|
||
1.0
|
||
} else {
|
||
(l.radius.max(1.0) / dist).powi(2)
|
||
}
|
||
}
|
||
_ => 1.0,
|
||
};
|
||
let _ = p;
|
||
if l.kind == 1 {
|
||
let cos_a = dot(scale(to_l, -1.0), l.dir).clamp(-1.0, 1.0);
|
||
k *= if cos_a >= l.cos_inner {
|
||
1.0
|
||
} else if cos_a <= l.cos_outer {
|
||
0.0
|
||
} else {
|
||
// Smoothstep across the feather in angle.
|
||
let a = cos_a.acos();
|
||
let (ai, ao) = (l.cos_inner.acos(), l.cos_outer.acos());
|
||
let t = ((a - ai) / (ao - ai).max(1e-6)).clamp(0.0, 1.0);
|
||
1.0 - t * t * (3.0 - 2.0 * t)
|
||
};
|
||
}
|
||
k
|
||
}
|
||
|
||
/// Inputs of one fragment.
|
||
#[derive(Clone, Copy, Debug)]
|
||
pub struct Frag {
|
||
pub pos: [f32; 3],
|
||
/// Interpolated (unnormalised) vertex normal.
|
||
pub normal: [f32; 3],
|
||
pub uv: [f32; 2],
|
||
pub tangent: [f32; 4],
|
||
pub material: u32,
|
||
pub front: bool,
|
||
}
|
||
|
||
/// Shade a fragment: premultiplied linear RGBA, or `None` when discarded (alpha test, back
|
||
/// face of a single-sided material).
|
||
pub fn shade(s: &Scene, f: &Frag) -> Option<[f32; 4]> {
|
||
let m: &Material = s.materials.get(f.material as usize)?;
|
||
if !f.front && !m.double_sided {
|
||
return None;
|
||
}
|
||
let tex = |i: i32| -> Option<[f32; 4]> { (i >= 0).then(|| s.textures.get(i as usize).map(|t| sample(t, &s.texels, f.uv[0], f.uv[1]))).flatten() };
|
||
let mut base = m.base;
|
||
if let Some(t) = tex(m.tex_base) {
|
||
for k in 0..4 {
|
||
base[k] *= t[k];
|
||
}
|
||
}
|
||
let alpha = base[3] * m.opacity;
|
||
if m.alpha_mode == 1 && alpha < m.alpha_cutoff {
|
||
return None;
|
||
}
|
||
if alpha <= 1e-5 && m.alpha_mode != 0 {
|
||
return None;
|
||
}
|
||
let alpha = if m.alpha_mode == 0 { m.opacity } else { alpha };
|
||
let rgb = [base[0], base[1], base[2]];
|
||
let mut emissive = m.emissive;
|
||
if let Some(t) = tex(m.tex_emissive) {
|
||
emissive = [emissive[0] * t[0], emissive[1] * t[1], emissive[2] * t[2]];
|
||
}
|
||
if m.shadows_only {
|
||
return Some(shadow_only(s, m, f, alpha));
|
||
}
|
||
if m.unlit || !m.accepts_lights || (s.lights.is_empty() && s.env.is_none() && s.ambient == [0.0; 3]) {
|
||
let c = add(rgb, emissive);
|
||
return Some([c[0] * alpha, c[1] * alpha, c[2] * alpha, alpha]);
|
||
}
|
||
let (mut metallic, mut rough) = (m.metallic, m.roughness);
|
||
if let Some(t) = tex(m.tex_mr) {
|
||
rough *= t[1];
|
||
metallic *= t[2];
|
||
}
|
||
let mut n = norm(f.normal);
|
||
if !f.front {
|
||
n = scale(n, -1.0);
|
||
}
|
||
// Shadow lookups follow the geometric surface, not the normal map.
|
||
let ng = n;
|
||
if let Some(t) = tex(m.tex_normal) {
|
||
let tn = [(t[0] * 2.0 - 1.0) * m.normal_scale, (t[1] * 2.0 - 1.0) * m.normal_scale, t[2] * 2.0 - 1.0];
|
||
let tg = [f.tangent[0], f.tangent[1], f.tangent[2]];
|
||
// Gram-Schmidt against the shading normal.
|
||
let tg = norm(sub(tg, scale(n, dot(n, tg))));
|
||
let sign = if f.front { f.tangent[3] } else { -f.tangent[3] };
|
||
let bt = scale(cross(n, tg), sign);
|
||
n = norm(add(add(scale(tg, tn[0]), scale(bt, tn[1])), scale(n, tn[2])));
|
||
}
|
||
let ao = match tex(m.tex_occlusion) {
|
||
Some(t) => 1.0 + m.occlusion_strength * (t[0] - 1.0),
|
||
None => 1.0,
|
||
};
|
||
let v = if s.ortho { scale(s.cam_fwd, -1.0) } else { norm(sub(s.eye, f.pos)) };
|
||
let mut out = [0.0f32; 3];
|
||
for l in &s.lights {
|
||
let (to_l, dist) = to_light(l, f.pos);
|
||
let k = attenuation(l, f.pos, to_l, dist);
|
||
if k <= 0.0 {
|
||
continue;
|
||
}
|
||
let n_l = dot(n, to_l);
|
||
if n_l <= 0.0 {
|
||
continue;
|
||
}
|
||
let sh = if m.receives_shadows { light_shadow(s, l, f.pos, ng, n_l) } else { 1.0 };
|
||
let b = brdf(rgb, metallic, rough, m.specular_k, m.diffuse_k, n, v, to_l);
|
||
for c in 0..3 {
|
||
out[c] += b[c] * l.color[c] * k * sh;
|
||
}
|
||
}
|
||
for c in 0..3 {
|
||
out[c] += s.ambient[c] * rgb[c] * m.ambient_k * ao;
|
||
}
|
||
if let Some(e) = &s.env {
|
||
let n_v = dot(n, v).clamp(1e-4, 1.0);
|
||
let r = sub(scale(n, 2.0 * dot(n, v)), v);
|
||
let (irr, spec) = env_light(s, e, n, r, rough);
|
||
let f0 = [mix(0.04 * m.specular_k, rgb[0], metallic), mix(0.04 * m.specular_k, rgb[1], metallic), mix(0.04 * m.specular_k, rgb[2], metallic)];
|
||
let (a, b) = env_brdf(n_v, rough);
|
||
for c in 0..3 {
|
||
let fs = f0[c] * a + b;
|
||
out[c] += (irr[c] * rgb[c] * (1.0 - fs) * (1.0 - metallic) * m.diffuse_k + spec[c] * fs) * ao;
|
||
}
|
||
}
|
||
let c = add(out, emissive);
|
||
Some([c[0] * alpha, c[1] * alpha, c[2] * alpha, alpha])
|
||
}
|
||
|
||
/// "Accepts Shadows: Only" (classic `compose::shade_texel`): black at the strongest shadow any
|
||
/// light casts on the fragment, nothing where it is unshadowed or takes no lights.
|
||
fn shadow_only(s: &Scene, m: &Material, f: &Frag, alpha: f32) -> [f32; 4] {
|
||
if m.unlit || !m.accepts_lights {
|
||
return [0.0; 4];
|
||
}
|
||
let n = if f.front { norm(f.normal) } else { scale(norm(f.normal), -1.0) };
|
||
let mut amount = 0.0f32;
|
||
for l in &s.lights {
|
||
let (to_l, dist) = to_light(l, f.pos);
|
||
if attenuation(l, f.pos, to_l, dist) <= 0.0 {
|
||
continue;
|
||
}
|
||
let n_l = dot(n, to_l);
|
||
if n_l <= 0.0 {
|
||
continue;
|
||
}
|
||
amount = amount.max(1.0 - light_shadow(s, l, f.pos, n, n_l));
|
||
}
|
||
[0.0, 0.0, 0.0, alpha * amount]
|
||
}
|