codecraft 0.1.1

A minimalist 3D game engine built on parts of Bevy (ECS, color) with wgpu and winit: OpenPBR materials, clustered lighting, an immediate-mode UI, audio and gamepad haptics
Documentation
// Port of glsl/pathtracing/main.glsl: constants, math utils, local frames,
// sampling, Fresnel, GGX microfacet functions, volumes, spectral helpers.

const PI: f32 = 3.141592653589793;
const PI2: f32 = 6.283185307179586;
const PI_HALF: f32 = 1.5707963267948966;
const RECIPROCAL_PI: f32 = 0.3183098861837907;
const RECIPROCAL_PI2: f32 = 0.15915494309189535;

const HUGE_DIST: f32 = 1.0e20;
// A floor on the subsurface radius, keeping its reciprocal finite. Unlike the
// ray offset this is not a geometric nudge, so it stays a fixed small length
// rather than scaling with the scene.
const RAY_OFFSET: f32 = 1.0e-4;
const DENOM_TOLERANCE: f32 = 1.0e-10;
const RADIANCE_EPSILON: f32 = 1.0e-12;
const TRANSMITTANCE_EPSILON: f32 = 1.0e-4;
const THROUGHPUT_EPSILON: f32 = 1.0e-6;
const PDF_EPSILON: f32 = 1.0e-6;
const IOR_EPSILON: f32 = 1.0e-5;
const FLT_EPSILON: f32 = 1.1920929e-7;
const INFINITY: f32 = 1.0e38;

const MATERIAL_PROPS: i32 = 0;
const MATERIAL_OPENPBR: i32 = 1;
const MATERIAL_GROUND: i32 = 2;

const ambient_ior: f32 = 1.0;

// Per-path state
var<private> rndSeed: u32;
var<private> wavelength_nm: f32;

fn sqr(x: f32) -> f32 { return x * x; }
fn sqr3(x: vec3<f32>) -> vec3<f32> { return x * x; }

fn safe_normalize(N: vec3<f32>) -> vec3<f32> {
    let l = length(N);
    return N / max(l, DENOM_TOLERANCE);
}

fn maxComponent(v: vec3<f32>) -> f32 { return max(v.x, max(v.y, v.z)); }
fn minComponent(v: vec3<f32>) -> f32 { return min(v.x, min(v.y, v.z)); }
fn avgComponent(v: vec3<f32>) -> f32 { return (v.x + v.y + v.z) / 3.0; }

fn cosTheta2(w: vec3<f32>) -> f32 { return w.z * w.z; }
fn cosTheta(w: vec3<f32>) -> f32 { return w.z; }
fn sinTheta2(w: vec3<f32>) -> f32 { return 1.0 - cosTheta2(w); }
fn sinTheta(w: vec3<f32>) -> f32 { return sqrt(max(0.0, sinTheta2(w))); }

// ---------------------------------------------------------------------------
// Local frame
// ---------------------------------------------------------------------------
struct Basis {
    nW: vec3<f32>,
    tW: vec3<f32>,
    bW: vec3<f32>,
    baryCoord: vec3<f32>,
};

fn normalToTangent(N: vec3<f32>) -> vec3<f32> {
    var T: vec3<f32>;
    if (abs(N.z) < abs(N.x)) {
        T = vec3<f32>(N.z, 0.0, -N.x);
    } else {
        T = vec3<f32>(0.0, N.z, -N.y);
    }
    return safe_normalize(T);
}

fn makeBasis(nW: vec3<f32>) -> Basis {
    var basis: Basis;
    basis.nW = safe_normalize(nW);
    basis.tW = normalToTangent(nW);
    basis.bW = cross(basis.nW, basis.tW);
    basis.baryCoord = vec3<f32>(0.0);
    return basis;
}

fn makeBasisT(nW: vec3<f32>, tW: vec3<f32>, baryCoord: vec3<f32>) -> Basis {
    var basis: Basis;
    basis.nW = safe_normalize(nW);
    basis.tW = safe_normalize(tW);
    basis.bW = cross(basis.nW, basis.tW);
    basis.baryCoord = baryCoord;
    return basis;
}

fn worldToLocal(vWorld: vec3<f32>, basis: Basis) -> vec3<f32> {
    return vec3<f32>(dot(vWorld, basis.tW), dot(vWorld, basis.bW), dot(vWorld, basis.nW));
}

fn localToWorld(vLocal: vec3<f32>, basis: Basis) -> vec3<f32> {
    return basis.tW * vLocal.x + basis.bW * vLocal.y + basis.nW * vLocal.z;
}

// V is assumed to be in local (+Z) space.
fn orthonormal_basis_ltc(V: vec3<f32>) -> mat3x3<f32> {
    let lenSqr = dot(V.xy, V.xy);
    var X: vec3<f32>;
    if (lenSqr > 0.0) {
        X = vec3<f32>(V.x, V.y, 0.0) * inverseSqrt(lenSqr);
    } else {
        X = vec3<f32>(1.0, 0.0, 0.0);
    }
    let Y = vec3<f32>(-X.y, X.x, 0.0);
    return mat3x3<f32>(X, Y, vec3<f32>(0.0, 0.0, 1.0));
}

// ---------------------------------------------------------------------------
// Sampling
// ---------------------------------------------------------------------------
fn pcg(v: u32) -> u32 {
    let state = v * 747796405u + 2891336453u;
    let word = ((state >> ((state >> 28u) + 4u)) ^ state) * 277803737u;
    return (word >> 22u) ^ word;
}

fn xorshift(seed: u32) -> u32 {
    var s = seed;
    s ^= s << 13u;
    s ^= s >> 17u;
    s ^= s << 5u;
    return s;
}

fn rand() -> f32 {
    rndSeed = pcg(rndSeed);
    let uint_range = 1.0 / 4294967295.0;
    return f32(rndSeed - 1u) * uint_range;
}

fn pdfHemisphereCosineWeighted(wiL: vec3<f32>) -> f32 {
    if (wiL.z <= PDF_EPSILON) { return PDF_EPSILON / PI; }
    return wiL.z / PI;
}

struct DirPdf {
    dir: vec3<f32>,
    pdf: f32,
};

fn sampleHemisphereCosineWeighted() -> DirPdf {
    let r = sqrt(rand());
    let theta = 2.0 * PI * rand();
    let x = r * cos(theta);
    let y = r * sin(theta);
    let z = sqrt(max(0.0, 1.0 - x * x - y * y));
    var out: DirPdf;
    out.pdf = max(PDF_EPSILON, abs(z) / PI);
    out.dir = vec3<f32>(x, y, z);
    return out;
}

fn powerHeuristic(a: f32, b: f32) -> f32 {
    return sqr(a) / max(DENOM_TOLERANCE, sqr(a) + sqr(b));
}

fn sample_triangle_filter(xi: f32) -> f32 {
    if (xi < 0.5) { return sqrt(2.0 * xi) - 1.0; }
    return 1.0 - sqrt(2.0 - 2.0 * xi);
}

// ---------------------------------------------------------------------------
// Fresnel
// ---------------------------------------------------------------------------
fn FresnelDielectricReflectance(mui: f32, eta_ti: f32) -> f32 {
    let c = mui;
    let mut2 = sqr(eta_ti) + sqr(c) - 1.0;
    if (mut2 <= 0.0) { return 1.0; }
    let g = sqrt(mut2);
    return 0.5 * sqr((g - c) / (g + c)) * (1.0 + sqr(((g + c) * c - 1.0) / ((g - c) * c + 1.0)));
}

fn FresnelSchlick(F0: vec3<f32>, mu: f32) -> vec3<f32> {
    return F0 + pow(1.0 - mu, 5.0) * (vec3<f32>(1.0) - F0);
}

// PR #256: clamp to [0,1]
fn FresnelF82Tint(mu: f32, F0: vec3<f32>, F82tint: vec3<f32>) -> vec3<f32> {
    let mu_bar = 1.0 / 7.0;
    let denom = mu_bar * pow(1.0 - mu_bar, 6.0);
    let Fschlick_bar = FresnelSchlick(F0, mu_bar);
    let Fschlick = FresnelSchlick(F0, mu);
    return clamp(Fschlick - mu * pow(1.0 - mu, 6.0) * (vec3<f32>(1.0) - F82tint) * Fschlick_bar / denom,
                 vec3<f32>(0.0), vec3<f32>(1.0));
}

fn E_F(eta: f32) -> f32 {
    return log((10893.0 * eta - 1438.2) / (-774.4 * sqr(eta) + 10212.0 * eta + 1.0));
}

fn DielectricFresnelAvg(eta: f32) -> f32 {
    if (eta > 1.0) { return E_F(eta); }
    else if (eta < 1.0) { return 1.0 - sqr(eta) * (1.0 - E_F(1.0 / eta)); }
    return 0.0;
}

// ---------------------------------------------------------------------------
// Microfacet multiple-scattering energy compensation (albedo scaling)
// ---------------------------------------------------------------------------
fn energy_table_lookup(table: u32, mu: f32) -> f32 {
    let x = clamp(mu, 0.0, 1.0) * f32(ENERGY_BINS) - 0.5;
    let i0 = u32(clamp(floor(x), 0.0, f32(ENERGY_BINS - 1u)));
    let i1 = min(i0 + 1u, ENERGY_BINS - 1u);
    let t = clamp(x - f32(i0), 0.0, 1.0);
    // `energy_base` is ours: the path tracer shaded one material and read
    // the tables from zero, and a table per material in one buffer needs to
    // know where its own set starts. The only edit to this file.
    let base = energy_base + table * ENERGY_BINS;
    return mix(energy_tables[base + i0], energy_tables[base + i1], t);
}

const MAX_ENERGY_SCALE: f32 = 8.0;

// Scale for the metal lobe: 1 + F0 (1/E_ss - 1)  (Turquin 2019)
fn metal_energy_scale(winputL: vec3<f32>) -> vec3<f32> {
    if (!energy_compensation) { return vec3<f32>(1.0); }
    let E = energy_table_lookup(0u, abs(winputL.z));
    let F0 = clamp(base_weight * base_color, vec3<f32>(0.0), vec3<f32>(1.0));
    return vec3<f32>(1.0) + F0 * min(1.0 / max(E, 1e-3) - 1.0, MAX_ENERGY_SCALE - 1.0);
}

// Scale for the dielectric reflection + transmission lobes: 1 / E_ss(R+T)
fn dielectric_energy_scale(winputL: vec3<f32>) -> f32 {
    if (!energy_compensation) { return 1.0; }
    let E = energy_table_lookup(select(2u, 1u, winputL.z > 0.0), abs(winputL.z));
    return min(1.0 / max(E, 1e-3), MAX_ENERGY_SCALE);
}

// ---------------------------------------------------------------------------
// GGX
// ---------------------------------------------------------------------------
fn ggx_ndf_eval(m: vec3<f32>, alpha_x: f32, alpha_y: f32) -> f32 {
    let ax = max(alpha_x, DENOM_TOLERANCE);
    let ay = max(alpha_y, DENOM_TOLERANCE);
    let Ddenom = PI * ax * ay * sqr(sqr(m.x / ax) + sqr(m.y / ay) + sqr(m.z));
    return 1.0 / max(Ddenom, DENOM_TOLERANCE);
}

// Dupuy et al. 2023, "Sampling Visible GGX Normals with Spherical Caps"
fn ggx_ndf_sample(wiL: vec3<f32>, alpha_x: f32, alpha_y: f32) -> vec3<f32> {
    let Xi = vec2<f32>(rand(), rand());
    let alpha = vec2<f32>(alpha_x, alpha_y);
    let V = normalize(vec3<f32>(wiL.xy * alpha, wiL.z));
    let phi = 2.0 * PI * Xi.x;
    let z = (1.0 - Xi.y) * (1.0 + V.z) - V.z;
    let sinT = sqrt(clamp(1.0 - z * z, 0.0, 1.0));
    let x = sinT * cos(phi);
    let y = sinT * sin(phi);
    let c = vec3<f32>(x, y, z);
    var H = c + V;
    H = normalize(vec3<f32>(H.xy * alpha, H.z));
    return H;
}

fn ggx_lambda(w: vec3<f32>, alpha_x: f32, alpha_y: f32) -> f32 {
    if (abs(w.z) < FLT_EPSILON) { return 0.0; }
    return (-1.0 + sqrt(1.0 + (sqr(alpha_x * w.x) + sqr(alpha_y * w.y)) / sqr(w.z))) / 2.0;
}

fn ggx_G1(w: vec3<f32>, alpha_x: f32, alpha_y: f32) -> f32 {
    return 1.0 / (1.0 + ggx_lambda(w, alpha_x, alpha_y));
}

fn ggx_G2(woL: vec3<f32>, wiL: vec3<f32>, alpha_x: f32, alpha_y: f32) -> f32 {
    return 1.0 / (1.0 + ggx_lambda(woL, alpha_x, alpha_y) + ggx_lambda(wiL, alpha_x, alpha_y));
}

// ---------------------------------------------------------------------------
// Volumetrics
// ---------------------------------------------------------------------------
struct Volume {
    extinction: vec3<f32>,
    albedo: vec3<f32>,
    anisotropy: f32,
};

// The Henyey-Greenstein inversion, kept identical to
// `openpbr_shader::math::sample_phase_function` -- see the note there for what
// the quotient and the square are, and for how the previous form failed. The
// engine-parity test only binds if the two draw the same sequence.
fn samplePhaseFunction(dW: vec3<f32>, anisotropy: f32) -> vec3<f32> {
    let U = rand();
    let V = rand();
    let g = anisotropy;
    var costheta: f32;
    if (abs(g) < 1.0e-3) {
        // `2U - 1`, matching the limit of the branch below; see the note on
        // `hg_cos_theta`.
        costheta = 2.0 * U - 1.0;
    } else {
        let s = (1.0 - g * g) / (1.0 - g + 2.0 * g * U);
        costheta = (1.0 + g * g - s * s) / (2.0 * g);
    }
    let sintheta = sqrt(max(0.0, 1.0 - costheta * costheta));
    let phi = 2.0 * PI * V;
    let basis = makeBasis(dW);
    return costheta * dW + sintheta * (cos(phi) * basis.tW + sin(phi) * basis.bW);
}

// Wavelength-dependent IOR according to Cauchy formula
fn specular_ior_dispersive() -> f32 {
    let lambda_C = 656.3;
    let lambda_d = 587.6;
    let lambda_F = 486.1;
    let lambda_FC2 = 1.0 / (1.0 / (lambda_F * lambda_F) - 1.0 / (lambda_C * lambda_C));
    let Vd = transmission_dispersion_abbe_number / max(DENOM_TOLERANCE, transmission_dispersion_scale);
    let nd = specular_ior;
    let B = (nd - 1.0) * lambda_FC2 / max(DENOM_TOLERANCE, Vd);
    let A = nd - B / sqr(lambda_d);
    return A + B / sqr(wavelength_nm);
}

// ---------------------------------------------------------------------------
// Color / spectral
// ---------------------------------------------------------------------------
fn luminance_srgb(C: vec3<f32>) -> f32 {
    return 0.2126 * C.r + 0.7152 * C.g + 0.0722 * C.b;
}

fn xFit_1931(w: f32) -> f32 {
    let t1 = (w - 442.0) * select(0.0374, 0.0624, w < 442.0);
    let t2 = (w - 599.8) * select(0.0323, 0.0264, w < 599.8);
    let t3 = (w - 501.1) * select(0.0382, 0.0490, w < 501.1);
    return 0.362 * exp(-0.5 * t1 * t1) + 1.056 * exp(-0.5 * t2 * t2) - 0.065 * exp(-0.5 * t3 * t3);
}
fn yFit_1931(w: f32) -> f32 {
    let t1 = (w - 568.8) * select(0.0247, 0.0213, w < 568.8);
    let t2 = (w - 530.9) * select(0.0322, 0.0613, w < 530.9);
    return 0.821 * exp(-0.5 * t1 * t1) + 0.286 * exp(-0.5 * t2 * t2);
}
fn zFit_1931(w: f32) -> f32 {
    let t1 = (w - 437.0) * select(0.0278, 0.0845, w < 437.0);
    let t2 = (w - 459.0) * select(0.0725, 0.0385, w < 459.0);
    return 1.217 * exp(-0.5 * t1 * t1) + 0.681 * exp(-0.5 * t2 * t2);
}
fn xyzFit_1931(w: f32) -> vec3<f32> {
    return vec3<f32>(xFit_1931(w), yFit_1931(w), zFit_1931(w));
}

fn xyzToRgb(XYZ: vec3<f32>) -> vec3<f32> {
    return vec3<f32>(
        dot(XYZ, vec3<f32>(3.240479, -1.537150, -0.498535)),
        dot(XYZ, vec3<f32>(-0.969256, 1.875991, 0.041556)),
        dot(XYZ, vec3<f32>(0.055648, -0.204043, 1.057311)));
}

// Spectral normalization: chosen so that E[xyzToRgb(xyzFit_1931(λ)) * SPECTRAL_NORM] = (1,1,1)
// for uniform λ in [360, 700] nm (1 / mean of the fit; the reference viewer uses the rougher
// (2.7, 3.3, 3.45), which leaves a slight pink cast).
const SPECTRAL_NORM: vec3<f32> = vec3<f32>(2.65507, 3.34676, 3.50145);