Files
effectcraft/crates/render/src/three_d/adv/shade.rs
T

531 lines
22 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 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]
}