brdf.slang

cross platform rendering playground

src/shaders/lib/brdf.slang

3.09 KB
#pragma once

#include "shared.h"

static const f32 PI = 3.14159265359;

// --- Low-discrepancy sampling helpers ---

f32 RadicalInverseVdC(u32 bits) {
    bits = (bits << 16u) | (bits >> 16u);
    bits = ((bits & 0x55555555u) << 1u) | ((bits & 0xAAAAAAAAu) >> 1u);
    bits = ((bits & 0x33333333u) << 2u) | ((bits & 0xCCCCCCCCu) >> 2u);
    bits = ((bits & 0x0F0F0F0Fu) << 4u) | ((bits & 0xF0F0F0F0u) >> 4u);
    bits = ((bits & 0x00FF00FFu) << 8u) | ((bits & 0xFF00FF00u) >> 8u);
    return f32(bits) * 2.3283064365386963e-10;
}

vec2 Hammersley(u32 i, u32 n) {
    return vec2(f32(i) / f32(n), RadicalInverseVdC(i));
}

vec3 ImportanceSampleGGX(vec2 xi, vec3 N, f32 roughness) {
    f32 a = roughness * roughness;
    f32 phi = 2.0 * PI * xi.x;
    f32 cosTheta = sqrt((1.0 - xi.y) / (1.0 + (a * a - 1.0) * xi.y));
    f32 sinTheta = sqrt(max(1.0 - cosTheta * cosTheta, 0.0));

    vec3 H;
    H.x = cos(phi) * sinTheta;
    H.y = sin(phi) * sinTheta;
    H.z = cosTheta;

    vec3 up = abs(N.z) < 0.999 ? vec3(0.0, 0.0, 1.0) : vec3(1.0, 0.0, 0.0);
    vec3 T = normalize(cross(up, N));
    vec3 B = cross(N, T);

    return normalize(T * H.x + B * H.y + N * H.z);
}

// --- Normal distribution function (GGX / Trowbridge-Reitz) ---

f32 D_GGX(f32 NdotH, f32 roughness) {
    f32 a = roughness * roughness;
    f32 a2 = a * a;
    f32 denom = (NdotH * NdotH) * (a2 - 1.0) + 1.0;
    return a2 / max(PI * denom * denom, 1e-7);
}

// --- Geometry (Smith-GGX with Schlick-GGX approximation) ---

f32 G_SchlickGGX(f32 NdotV, f32 k) {
    return NdotV / (NdotV * (1.0 - k) + k);
}

f32 G_Smith_Direct(f32 NdotV, f32 NdotL, f32 roughness) {
    f32 r = roughness + 1.0;
    f32 k = (r * r) / 8.0;
    return G_SchlickGGX(NdotV, k) * G_SchlickGGX(NdotL, k);
}

f32 G_Smith_IBL(f32 NdotV, f32 NdotL, f32 roughness) {
    f32 k = (roughness * roughness) / 2.0;
    return G_SchlickGGX(NdotV, k) * G_SchlickGGX(NdotL, k);
}

// --- Fresnel (Schlick approximation) ---

vec3 F_Schlick(f32 cosTheta, vec3 F0) {
    return F0 + (1.0 - F0) * pow(1.0 - cosTheta, 5.0);
}

vec3 F_SchlickRoughness(f32 cosTheta, vec3 F0, f32 roughness) {
    return F0 + (max(vec3(1.0 - roughness), F0) - F0) * pow(1.0 - cosTheta, 5.0);
}

// --- BRDF integration (split-sum approximation for the IBL LUT) ---

vec2 IntegrateBRDF(f32 NdotV, f32 roughness) {
    const u32 SAMPLE_COUNT = 1024u;

    vec3 V;
    V.x = sqrt(max(1.0 - NdotV * NdotV, 0.0));
    V.y = 0.0;
    V.z = NdotV;

    vec3 N = vec3(0.0, 0.0, 1.0);

    f32 A = 0.0;
    f32 B = 0.0;

    for (u32 i = 0u; i < SAMPLE_COUNT; ++i) {
        vec2 xi = Hammersley(i, SAMPLE_COUNT);
        vec3 H = ImportanceSampleGGX(xi, N, roughness);
        vec3 L = normalize(2.0 * dot(V, H) * H - V);

        f32 NdotL = saturate(L.z);
        f32 NdotH = saturate(H.z);
        f32 VdotH = saturate(dot(V, H));

        if (NdotL > 0.0) {
            f32 G = G_Smith_IBL(NdotV, NdotL, roughness);
            f32 G_Vis = (G * VdotH) / max(NdotH * NdotV, 0.0001);
            f32 Fc = pow(1.0 - VdotH, 5.0);

            A += (1.0 - Fc) * G_Vis;
            B += Fc * G_Vis;
        }
    }

    return vec2(A, B) / f32(SAMPLE_COUNT);
}