Skip to content

Instantly share code, notes, and snippets.

@Pikachuxxxx
Created September 26, 2025 22:39
Show Gist options
  • Save Pikachuxxxx/786458e07098040f90a49a7151021a17 to your computer and use it in GitHub Desktop.
Save Pikachuxxxx/786458e07098040f90a49a7151021a17 to your computer and use it in GitHub Desktop.
Disney BRDF PBR (clear coat, sheen etc.)
// Disney BRDF Parameter Structure
struct DisneyBRDFParams
{
float3 baseColor; // Base color (albedo)
float metallic; // Metallic parameter [0, 1]
float subsurface; // Subsurface scattering amount [0, 1]
float specular; // Specular amount [0, 1]
float roughness; // Surface roughness [0, 1]
float specularTint; // Specular tint toward base color [0, 1]
float anisotropic; // Anisotropy amount [0, 1]
float sheen; // Sheen amount [0, 1]
float sheenTint; // Sheen tint toward base color [0, 1]
float clearcoat; // Clearcoat amount [0, 1]
float clearcoatGloss;// Clearcoat glossiness [0, 1]
};
// Helper functions
float SchlickFresnel(float u)
{
float m = clamp(1.0 - u, 0.0, 1.0);
float m2 = m * m;
return m2 * m2 * m; // m^5
}
float GTR1(float NdotH, float a)
{
if (a >= 1.0) return 1.0 / PI;
float a2 = a * a;
float t = 1.0 + (a2 - 1.0) * NdotH * NdotH;
return (a2 - 1.0) / (PI * log(a2) * t);
}
float GTR2(float NdotH, float a)
{
float a2 = a * a;
float t = 1.0 + (a2 - 1.0) * NdotH * NdotH;
return a2 / (PI * t * t);
}
float GTR2_aniso(float NdotH, float HdotX, float HdotY, float ax, float ay)
{
return 1.0 / (PI * ax * ay * pow(pow(HdotX / ax, 2.0) + pow(HdotY / ay, 2.0) + NdotH * NdotH, 2.0));
}
float SmithG_GGX(float NdotV, float alphaG)
{
float a = alphaG * alphaG;
float b = NdotV * NdotV;
return 1.0 / (NdotV + sqrt(a + b - a * b));
}
float SmithG_GGX_aniso(float NdotV, float VdotX, float VdotY, float ax, float ay)
{
return 1.0 / (NdotV + sqrt(pow(VdotX * ax, 2.0) + pow(VdotY * ay, 2.0) + NdotV * NdotV));
}
// Disney BRDF calculation function
float3 DisneyBRDF(float3 N, float3 L, float3 V, float3 X, float3 Y, DisneyBRDFParams params)
{
float3 H = normalize(L + V);
float NdotL = max(dot(N, L), 0.0);
float NdotV = max(dot(N, V), 0.0);
float NdotH = max(dot(N, H), 0.0);
float LdotH = max(dot(L, H), 0.0);
float HdotX = dot(H, X);
float HdotY = dot(H, Y);
float VdotX = dot(V, X);
float VdotY = dot(V, Y);
if (NdotL < 0.0 || NdotV < 0.0) return float3(0.0, 0.0, 0.0);
// Diffuse fresnel - go from 1 at normal incidence to 0.5 at grazing
float FL = SchlickFresnel(NdotL);
float FV = SchlickFresnel(NdotV);
float Fd90 = 0.5 + 2.0 * LdotH * LdotH * params.roughness;
float Fd = lerp(1.0, Fd90, FL) * lerp(1.0, Fd90, FV);
// Based on Fresnel specular reflection at grazing angle
float Fss90 = LdotH * LdotH * params.roughness;
float Fss = lerp(1.0, Fss90, FL) * lerp(1.0, Fss90, FV);
float ss = 1.25 * (Fss * (1.0 / (NdotL + NdotV) - 0.5) + 0.5);
// Specular
float aspect = sqrt(1.0 - params.anisotropic * 0.9);
float ax = max(0.001, pow(params.roughness, 2.0) / aspect);
float ay = max(0.001, pow(params.roughness, 2.0) * aspect);
float Ds = GTR2_aniso(NdotH, HdotX, HdotY, ax, ay);
float FH = SchlickFresnel(LdotH);
float3 Ctint = params.baseColor / (params.baseColor.r + params.baseColor.g + params.baseColor.b > 0.0 ?
(params.baseColor.r + params.baseColor.g + params.baseColor.b) / 3.0 : 1.0);
float3 Cspec0 = lerp(params.specular * 0.08 * lerp(float3(1.0, 1.0, 1.0), Ctint, params.specularTint),
params.baseColor, params.metallic);
float3 Fs = lerp(Cspec0, float3(1.0, 1.0, 1.0), FH);
float Gs = SmithG_GGX_aniso(NdotL, dot(L, X), dot(L, Y), ax, ay) *
SmithG_GGX_aniso(NdotV, VdotX, VdotY, ax, ay);
// Sheen
float3 Fsheen = FH * params.sheen * lerp(float3(1.0, 1.0, 1.0), Ctint, params.sheenTint);
// Clearcoat (ior = 1.5 -> F0 = 0.04)
float Dr = GTR1(NdotH, lerp(0.1, 0.001, params.clearcoatGloss));
float Fr = lerp(0.04, 1.0, FH);
float Gr = SmithG_GGX(NdotL, 0.25) * SmithG_GGX(NdotV, 0.25);
// Diffuse, subsurface, specular, sheen, clearcoat components
float3 diffuse = (1.0 / PI) * params.baseColor * (1.0 - params.metallic) *
(Fd * (1.0 - params.subsurface) + ss * params.subsurface);
float3 specular = Gs * Fs * Ds;
float3 sheen = Fsheen;
float3 clearcoat = 0.25 * params.clearcoat * Gr * Fr * Dr;
return diffuse + specular + sheen + clearcoat;
}
// Main lighting calculation using the Disney BRDF
float3 CalculatePBRLighting(float3 worldPos, float3 normal, DisneyBRDFParams material)
{
// Calculate tangent space basis
float3 N = normalize(normal);
float3 V = normalize(CameraPosition - worldPos);
// Create orthonormal basis (Frisvad method)
float3 X, Y;
if (N.z < -0.999999) {
X = float3(0.0, -1.0, 0.0);
Y = float3(-1.0, 0.0, 0.0);
} else {
float a = 1.0 / (1.0 + N.z);
float b = -N.x * N.y * a;
X = float3(1.0 - N.x * N.x * a, b, -N.x);
Y = float3(b, 1.0 - N.y * N.y * a, -N.y);
}
// Initialize lighting result
float3 finalColor = float3(0.0, 0.0, 0.0);
// For each light source
for (int i = 0; i < LightCount; i++)
{
float3 L = normalize(Lights[i].position - worldPos);
float distance = length(Lights[i].position - worldPos);
float attenuation = 1.0 / (distance * distance);
float3 radiance = Lights[i].color * attenuation;
// Calculate Disney BRDF for this light
float3 brdf = DisneyBRDF(N, L, V, X, Y, material);
// Add contribution from this light
float NdotL = max(dot(N, L), 0.0);
finalColor += brdf * radiance * NdotL;
}
// Add ambient lighting
float3 ambient = float3(0.03, 0.03, 0.03) * material.baseColor * (1.0 - material.metallic);
finalColor += ambient;
// Add Image-based lighting (IBL)
finalColor += CalculateIBL(N, V, X, Y, material);
return finalColor;
}
// Example usage in a pixel shader
float4 PSMain(PSInput input) : SV_TARGET
{
// Sample material textures
float3 albedo = AlbedoTexture.Sample(SamplerState, input.TexCoord).rgb;
float4 materialParams = MaterialParamsTexture.Sample(SamplerState, input.TexCoord);
float3 normal = NormalFromNormalMap(NormalTexture.Sample(SamplerState, input.TexCoord).rgb, input);
// Setup Disney BRDF parameters
DisneyBRDFParams material;
material.baseColor = albedo;
material.metallic = materialParams.r;
material.subsurface = materialParams.g;
material.specular = 0.5;
material.roughness = materialParams.b;
material.specularTint = 0.0;
material.anisotropic = 0.0;
material.sheen = 0.0;
material.sheenTint = 0.5;
material.clearcoat = materialParams.a;
material.clearcoatGloss = 0.0;
// Calculate lighting using Disney BRDF
float3 color = CalculatePBRLighting(input.WorldPos, normal, material);
// Tone mapping and gamma correction
color = color / (color + float3(1.0, 1.0, 1.0));
color = pow(color, float3(1.0/2.2, 1.0/2.2, 1.0/2.2));
return float4(color, 1.0);
}
//---------------------------------------------------
// NEW CODE: IBL IMPLEMENTATION FOR DISNEY BRDF
//---------------------------------------------------
// IBL Textures and samplers
TextureCube EnvironmentMap;
TextureCube IrradianceMap;
Texture2D BRDFLut;
SamplerState EnvMapSampler;
SamplerState BRDFSampler;
// Calculate IBL contribution for Disney BRDF
float3 CalculateIBL(float3 N, float3 V, float3 X, float3 Y, DisneyBRDFParams params)
{
float NdotV = max(dot(N, V), 0.0);
float3 R = reflect(-V, N);
// Extract material parameters needed for IBL
float roughness = params.roughness;
float metallic = params.metallic;
float3 baseColor = params.baseColor;
// Calculate diffuse IBL contribution
float3 diffuseIBL = IrradianceMap.Sample(EnvMapSampler, N).rgb * baseColor * (1.0 - metallic);
// Calculate specular IBL contribution using split-sum approximation
float lod = roughness * 6.0; // Assuming 6 mip levels
float3 prefilteredColor = EnvironmentMap.SampleLevel(EnvMapSampler, R, lod).rgb;
float2 envBRDF = BRDFLut.Sample(BRDFSampler, float2(NdotV, roughness)).rg;
// Calculate F0 (specular reflectance at normal incidence) based on Disney BRDF
float3 Ctint = baseColor / (baseColor.r + baseColor.g + baseColor.b > 0.0 ?
(baseColor.r + baseColor.g + baseColor.b) / 3.0 : 1.0);
float3 Cspec0 = lerp(params.specular * 0.08 * lerp(float3(1.0, 1.0, 1.0), Ctint, params.specularTint),
baseColor, metallic);
// Specular IBL
float3 specularIBL = prefilteredColor * (Cspec0 * envBRDF.x + envBRDF.y);
// Anisotropic IBL approximation (optional)
if (params.anisotropic > 0.0)
{
float aspect = sqrt(1.0 - params.anisotropic * 0.9);
float ax = max(0.001, pow(roughness, 2.0) / aspect);
float ay = max(0.001, pow(roughness, 2.0) * aspect);
// Create anisotropic roughness view-dependent bent normal
// This is a simple approximation - more accurate methods exist but are more complex
float3 anisotropicDirection = params.anisotropic * (dot(X, V) * X + dot(Y, V) * Y);
float3 bentNormal = normalize(N + anisotropicDirection * (ax - ay) * 0.5);
// Sample environment with bent normal
float3 bentR = reflect(-V, bentNormal);
float3 anisotropicPrefilteredColor = EnvironmentMap.SampleLevel(EnvMapSampler, bentR, lod).rgb;
// Blend based on anisotropy strength
prefilteredColor = lerp(prefilteredColor, anisotropicPrefilteredColor, params.anisotropic);
specularIBL = prefilteredColor * (Cspec0 * envBRDF.x + envBRDF.y);
}
// Clearcoat IBL contribution
float3 clearcoatIBL = float3(0, 0, 0);
if (params.clearcoat > 0.0)
{
float clearcoatRoughness = lerp(0.1, 0.001, params.clearcoatGloss);
float clearcoatLod = clearcoatRoughness * 6.0;
float3 clearcoatRadiance = EnvironmentMap.SampleLevel(EnvMapSampler, R, clearcoatLod).rgb;
// Fixed F0 = 0.04 for clearcoat layer (IOR = 1.5)
float clearcoatFresnel = lerp(0.04, 1.0, SchlickFresnel(NdotV));
clearcoatIBL = clearcoatRadiance * clearcoatFresnel * params.clearcoat * 0.25;
}
// Sheen IBL contribution (optional, simplified)
float3 sheenIBL = float3(0, 0, 0);
if (params.sheen > 0.0)
{
// Sheen works best with high roughness (fabric-like)
float sheenLod = 4.0; // Higher mip level for sheen
float3 sheenRadiance = IrradianceMap.Sample(EnvMapSampler, N).rgb; // Use irradiance map for diffuse-like behavior
float3 sheenTint = lerp(float3(1.0, 1.0, 1.0), Ctint, params.sheenTint);
sheenIBL = sheenRadiance * params.sheen * sheenTint * (1.0 - metallic) * SchlickFresnel(NdotV);
}
// Combine all IBL components
return diffuseIBL + specularIBL + clearcoatIBL + sheenIBL;
}
// Helper function to generate the BRDF LUT texture (run once at initialization)
void GenerateBRDFLut(RWTexture2D<float2> BRDFLutOutput)
{
uint2 pixel = DispatchThreadID.xy;
uint width, height;
BRDFLutOutput.GetDimensions(width, height);
float NdotV = (pixel.x + 0.5) / width;
float roughness = (pixel.y + 0.5) / height;
// Ensure valid input range
NdotV = max(NdotV, 0.001);
// GGX/Trowbridge-Reitz precomputed DFG term for IBL
float3 V;
V.x = sqrt(1.0 - NdotV * NdotV); // sin
V.y = 0.0;
V.z = NdotV; // cos
float3 N = float3(0.0, 0.0, 1.0);
float A = 0.0;
float B = 0.0;
const uint SAMPLE_COUNT = 1024;
for (uint i = 0; i < SAMPLE_COUNT; ++i)
{
// Sample using Hammersley sequence
float2 Xi = Hammersley(i, SAMPLE_COUNT);
// Sample with GGX importance sampling
float3 H = ImportanceSampleGGX(Xi, N, roughness);
float3 L = 2.0 * dot(V, H) * H - V;
float NdotL = max(L.z, 0.0);
if (NdotL > 0.0)
{
float NdotH = max(H.z, 0.0);
float VdotH = max(dot(V, H), 0.0);
// GGX BRDF
float G = SmithG_GGX(NdotV, roughness) * SmithG_GGX(NdotL, roughness);
float G_Vis = (G * VdotH) / (NdotH * NdotV);
float Fc = pow(1.0 - VdotH, 5.0);
A += (1.0 - Fc) * G_Vis;
B += Fc * G_Vis;
}
}
// Average values
A /= SAMPLE_COUNT;
B /= SAMPLE_COUNT;
// Output to LUT
BRDFLutOutput[pixel] = float2(A, B);
}
// Hammersley low-discrepancy sequence for Monte Carlo integration
float2 Hammersley(uint i, uint N)
{
float2 result;
result.x = float(i) / float(N);
// Radical inverse based on digital bit reversal
uint bits = i;
bits = (bits << 16) | (bits >> 16);
bits = ((bits & 0x55555555) << 1) | ((bits & 0xAAAAAAAA) >> 1);
bits = ((bits & 0x33333333) << 2) | ((bits & 0xCCCCCCCC) >> 2);
bits = ((bits & 0x0F0F0F0F) << 4) | ((bits & 0xF0F0F0F0) >> 4);
bits = ((bits & 0x00FF00FF) << 8) | ((bits & 0xFF00FF00) >> 8);
result.y = float(bits) * 2.3283064365386963e-10; // 0x100000000
return result;
}
// Generate a sample in the hemisphere based on the GGX distribution
float3 ImportanceSampleGGX(float2 Xi, float3 N, float roughness)
{
float a = roughness * roughness;
// Sample in spherical coordinates
float phi = 2.0 * PI * Xi.x;
float cosTheta = sqrt((1.0 - Xi.y) / (1.0 + (a*a - 1.0) * Xi.y));
float sinTheta = sqrt(1.0 - cosTheta * cosTheta);
// Convert to cartesian coordinates
float3 H;
H.x = cos(phi) * sinTheta;
H.y = sin(phi) * sinTheta;
H.z = cosTheta;
// From tangent-space to world-space
float3 up = abs(N.z) < 0.999 ? float3(0.0, 0.0, 1.0) : float3(1.0, 0.0, 0.0);
float3 tangent = normalize(cross(up, N));
float3 bitangent = cross(N, tangent);
float3 sampleVec = tangent * H.x + bitangent * H.y + N * H.z;
return normalize(sampleVec);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment