awsm-renderer 0.4.0

awsm-renderer
Documentation
// --- constants & helpers -------------------------------------
const PI      : f32 = 3.1415926535897932384626433832795;
const TAU     : f32 = 6.283185307179586476925286766559; // 2*PI
const EPSILON : f32 = 1e-4;
const F32_MAX = 2139095039u;
const U32_MAX = 4294967295u;

fn saturate(x: f32) -> f32 { return clamp(x, 0.0, 1.0); }
fn saturate3(v: vec3<f32>) -> vec3<f32> { return clamp(v, vec3<f32>(0.0), vec3<f32>(1.0)); }

// attenuation for a point/spot light per KHR_lights_punctual:
//   attenuation = max(min(1 - (dist/range)^4, 1), 0) / dist^2
// When range == 0, falloff is omitted (light has unlimited range).
fn inverse_square(range: f32, dist: f32) -> f32 {
    let inv_sq = 1.0 / max(dist * dist, 1e-4);
    if (range <= 0.0) {
        return inv_sq;
    }
    let ratio = dist / range;
    let ratio4 = ratio * ratio * ratio * ratio;
    return saturate(1.0 - ratio4) * inv_sq;
}

fn safe_normalize(normal: vec3<f32>) -> vec3<f32> {
    let len_sq = dot(normal, normal);
    if (len_sq > 0.0) {
        return normal * inverseSqrt(len_sq);
    }
    // fallback: up vector to avoid NaNs; scene lighting expects unit normal
    return vec3<f32>(0.0, 0.0, 1.0);
}

fn join32(lo: u32, hi: u32) -> u32 {
  return (hi << 16u) | (lo & 0xFFFFu);
}

fn split16(x: u32) -> vec2<u32> {
  let lo = x & 0xFFFFu;
  let hi = x >> 16u;
  return vec2<u32>(lo, hi);
}

// ------------------------------------------------------------
// Octahedral normal encoding (unit normal <-> vec2)
// Encodes a unit normal into 2 channels with minimal distortion
// ------------------------------------------------------------
fn encode_octahedral(n_in: vec3<f32>) -> vec2<f32> {
    var n = n_in / (abs(n_in.x) + abs(n_in.y) + abs(n_in.z));
    if (n.z < 0.0) {
        let one = vec2<f32>(1.0, 1.0);
        let sgn = sign(n.xy);
        let wrapped = (one - abs(n.yx)) * sgn;
        n = vec3<f32>(wrapped.x, wrapped.y, n.z);
    }
    return n.xy * 0.5 + vec2<f32>(0.5, 0.5);
}

fn decode_octahedral(e: vec2<f32>) -> vec3<f32> {
    let f = e * 2.0 - vec2<f32>(1.0, 1.0);
    var n = vec3<f32>(f.x, f.y, 1.0 - abs(f.x) - abs(f.y));
    let t = clamp(-n.z, 0.0, 1.0);

    // Add -t where n.xy >= 0, else +t (per component)
    let vx = select(t, -t, n.x >= 0.0);
    let vy = select(t, -t, n.y >= 0.0);
    n = vec3<f32>(n.x + vx, n.y + vy, n.z);

    return normalize(n);
}

// ------------------------------------------------------------
// Stable canonical tangent/bitangent basis (Frisvad-style)
// Generates an orthonormal basis from a normal vector
// ------------------------------------------------------------
struct TB { t: vec3<f32>, b: vec3<f32> };

fn canonical_tb(n: vec3<f32>) -> TB {
    if (n.z < -0.9999999) {
        return TB(vec3<f32>(0.0, -1.0, 0.0), vec3<f32>(-1.0,  0.0, 0.0));
    } else {
        let a  = 1.0 / (1.0 + n.z);
        let bb = -n.x * n.y * a;
        let t  = vec3<f32>(1.0 - n.x * n.x * a, bb, -n.x);
        let b  = vec3<f32>(bb, 1.0 - n.y * n.y * a, -n.y);
        return TB(t, b);
    }
}

// ------------------------------------------------------------
// TBN packing/unpacking
// Pack: N (unit), T (unit), s (+1 right-handed, -1 left-handed)
// -> vec4<f32> : [octN.xy, angleU, signU]
// angleU = theta in [0,1] where theta is rotation of T in N's plane
// signU  = 1 for s>0, 0 for s<=0
// ------------------------------------------------------------
fn pack_normal_tangent(N: vec3<f32>, T: vec3<f32>, s: f32) -> vec4<f32> {
    let octN   = encode_octahedral(N);
    let tb     = canonical_tb(N);
    let x      = dot(T, tb.t);
    let y      = dot(T, tb.b);
    let theta  = atan2(y, x);                 // [-PI, PI]
    let angleU = (theta + PI) / TAU;          // [0,1]
    let signU  = select(0.0, 1.0, s > 0.0);   // 0 or 1
    return vec4<f32>(octN.x, octN.y, angleU, signU);
}

// Unpack: vec4<f32> -> N, T, B (orthonormal)
struct TBN { N: vec3<f32>, T: vec3<f32>, B: vec3<f32> };

fn unpack_normal_tangent(rgba: vec4<f32>) -> TBN {
    let N     = decode_octahedral(rgba.xy);
    let theta = rgba.z * TAU - PI;           // [-PI, PI]
    let s     = select(-1.0, 1.0, rgba.w >= 0.5);

    let tb0 = canonical_tb(N);
    let T   = normalize(cos(theta) * tb0.t + sin(theta) * tb0.b);
    let B   = s * normalize(cross(N, T));
    return TBN(N, T, B);
}

// Convert relative indices to absolute indices (0 stays 0)
fn abs_index(base_index: u32, relative_index: u32) -> u32 {
    return select(0u, base_index + relative_index, relative_index != 0u);
}

// -------------------------------------------------------------
// IOR and Refraction Utilities
// -------------------------------------------------------------
// Moved here from brdf.wgsl so they're always available: the transparent pass's
// transmission helpers (sample_transmission_background_for_ior) use them even on
// non-PBR pipelines where brdf.wgsl is gated out. They're generic utilities, not
// BRDF lobes.

// Get effective IOR value, defaulting to 1.5 when invalid (< 1.0).
// IOR = 1.0 is valid (air, no refraction), IOR < 1.0 is physically invalid.
fn effective_ior(ior: f32) -> f32 {
    return select(ior, 1.5, ior < 1.0);
}

// Convert index of refraction to F0 (reflectance at normal incidence).
// Default IOR of 1.5 yields F0 = 0.04 (standard dielectric).
fn ior_to_f0(ior: f32) -> f32 {
    let ior_val = effective_ior(ior);
    let ratio = (ior_val - 1.0) / (ior_val + 1.0);
    return ratio * ratio;
}

// Calculate refracted direction using Snell's law.
// Returns vec3(0) if total internal reflection occurs.
fn refract_direction(incident: vec3<f32>, normal: vec3<f32>, eta: f32) -> vec3<f32> {
    // Optimization: no refraction when eta ≈ 1.0 (same medium)
    if (abs(eta - 1.0) < 0.001) {
        return incident;
    }
    let cos_i = -dot(incident, normal);
    let sin_t2 = eta * eta * (1.0 - cos_i * cos_i);
    if (sin_t2 > 1.0) {
        return vec3<f32>(0.0);  // Signal TIR to caller
    }
    let cos_t = sqrt(1.0 - sin_t2);
    return eta * incident + (eta * cos_i - cos_t) * normal;
}