#ifndef UNITY_BSDF_INCLUDED #define UNITY_BSDF_INCLUDED // Note: All NDF and diffuse term have a version with and without divide by PI. // Version with divide by PI are use for direct lighting. // Version without divide by PI are use for image based lighting where often the PI cancel during importance sampling //----------------------------------------------------------------------------- // Fresnel term //----------------------------------------------------------------------------- float F_Schlick(float f0, float f90, float u) { float x = 1.0 - u; float x2 = x * x; float x5 = x * x2 * x2; return (f90 - f0) * x5 + f0; // sub mul mul mul sub mad } float F_Schlick(float f0, float u) { return F_Schlick(f0, 1.0, u); // sub mul mul mul sub mad } float3 F_Schlick(float3 f0, float f90, float u) { float x = 1.0 - u; float x2 = x * x; float x5 = x * x2 * x2; return f0 * (1.0 - x5) + (f90 * x5); // sub mul mul mul sub mul mad*3 } float3 F_Schlick(float3 f0, float u) { return F_Schlick(f0, 1.0, u); // sub mul mul mul sub mad*3 } // Does not handle TIR. float F_Transm_Schlick(float f0, float f90, float u) { float x = 1.0 - u; float x2 = x * x; float x5 = x * x2 * x2; return (1.0 - f90 * x5) - f0 * (1.0 - x5); // sub mul mul mul mad sub mad } // Does not handle TIR. float F_Transm_Schlick(float f0, float u) { return F_Transm_Schlick(f0, 1.0, u); // sub mul mul mad mad } // Does not handle TIR. float3 F_Transm_Schlick(float3 f0, float f90, float u) { float x = 1.0 - u; float x2 = x * x; float x5 = x * x2 * x2; return (1.0 - f90 * x5) - f0 * (1.0 - x5); // sub mul mul mul mad sub mad*3 } // Does not handle TIR. float3 F_Transm_Schlick(float3 f0, float u) { return F_Transm_Schlick(f0, 1.0, u); // sub mul mul mad mad*3 } //----------------------------------------------------------------------------- // Specular BRDF //----------------------------------------------------------------------------- float D_GGXNoPI(float NdotH, float roughness) { float a2 = Sq(roughness); float s = (NdotH * a2 - NdotH) * NdotH + 1.0; return a2 / (s * s); } float D_GGX(float NdotH, float roughness) { return INV_PI * D_GGXNoPI(NdotH, roughness); } // Ref: Understanding the Masking-Shadowing Function in Microfacet-Based BRDFs, p. 19, 29. float G_MaskingSmithGGX(float NdotV, float roughness) { // G1(V, H) = HeavisideStep(VdotH) / (1 + Λ(V)). // Λ(V) = -0.5 + 0.5 * sqrt(1 + 1 / a²). // a = 1 / (roughness * tan(theta)). // 1 + Λ(V) = 0.5 + 0.5 * sqrt(1 + roughness² * tan²(theta)). // tan²(theta) = (1 - cos²(theta)) / cos²(theta) = 1 / cos²(theta) - 1. // Assume that (VdotH > 0), e.i. (acos(LdotV) < Pi). return 1.0 / (0.5 + 0.5 * sqrt(1.0 + Sq(roughness) * (1.0 / Sq(NdotV) - 1.0))); } // Ref: Understanding the Masking-Shadowing Function in Microfacet-Based BRDFs, p. 12. float D_GGX_Visible(float NdotH, float NdotV, float VdotH, float roughness) { return D_GGX(NdotH, roughness) * G_MaskingSmithGGX(NdotV, roughness) * VdotH / NdotV; } // Precompute part of lambdaV float GetSmithJointGGXPartLambdaV(float NdotV, float roughness) { float a2 = Sq(roughness); return sqrt((-NdotV * a2 + NdotV) * NdotV + a2); } // Note: V = G / (4 * NdotL * NdotV) // Ref: http://jcgt.org/published/0003/02/03/paper.pdf float V_SmithJointGGX(float NdotL, float NdotV, float roughness, float partLambdaV) { float a2 = Sq(roughness); // Original formulation: // lambda_v = (-1 + sqrt(a2 * (1 - NdotL2) / NdotL2 + 1)) * 0.5 // lambda_l = (-1 + sqrt(a2 * (1 - NdotV2) / NdotV2 + 1)) * 0.5 // G = 1 / (1 + lambda_v + lambda_l); // Reorder code to be more optimal: float lambdaV = NdotL * partLambdaV; float lambdaL = NdotV * sqrt((-NdotL * a2 + NdotL) * NdotL + a2); // Simplify visibility term: (2.0 * NdotL * NdotV) / ((4.0 * NdotL * NdotV) * (lambda_v + lambda_l)); return 0.5 / (lambdaV + lambdaL); } float V_SmithJointGGX(float NdotL, float NdotV, float roughness) { float partLambdaV = GetSmithJointGGXPartLambdaV(NdotV, roughness); return V_SmithJointGGX(NdotL, NdotV, roughness, partLambdaV); } // Inline D_GGX() * V_SmithJointGGX() together for better code generation. float DV_SmithJointGGX(float NdotH, float NdotL, float NdotV, float roughness, float partLambdaV) { float a2 = Sq(roughness); float s = (NdotH * a2 - NdotH) * NdotH + 1.0; float lambdaV = NdotL * partLambdaV; float lambdaL = NdotV * sqrt((-NdotL * a2 + NdotL) * NdotL + a2); float2 D = float2(a2, s * s); // Fraction without the multiplier (1/Pi) float2 G = float2(1, lambdaV + lambdaL); // Fraction without the multiplier (1/2) return (INV_PI * 0.5) * (D.x * G.x) / (D.y * G.y); } float DV_SmithJointGGX(float NdotH, float NdotL, float NdotV, float roughness) { float partLambdaV = GetSmithJointGGXPartLambdaV(NdotV, roughness); return DV_SmithJointGGX(NdotH, NdotL, NdotV, roughness, partLambdaV); } // Precompute a part of LambdaV. // Note on this linear approximation. // Exact for roughness values of 0 and 1. Also, exact when the cosine is 0 or 1. // Otherwise, the worst case relative error is around 10%. // https://www.desmos.com/calculator/wtp8lnjutx float GetSmithJointGGXPartLambdaVApprox(float NdotV, float roughness) { float a = roughness; return NdotV * (1 - a) + a; } float V_SmithJointGGXApprox(float NdotL, float NdotV, float roughness, float partLambdaV) { float a = roughness; float lambdaV = NdotL * partLambdaV; float lambdaL = NdotV * (NdotL * (1 - a) + a); return 0.5 / (lambdaV + lambdaL); } float V_SmithJointGGXApprox(float NdotL, float NdotV, float roughness) { float partLambdaV = GetSmithJointGGXPartLambdaVApprox(NdotV, roughness); return V_SmithJointGGXApprox(NdotL, NdotV, roughness, partLambdaV); } // roughnessT -> roughness in tangent direction // roughnessB -> roughness in bitangent direction float D_GGXAnisoNoPI(float TdotH, float BdotH, float NdotH, float roughnessT, float roughnessB) { float a2 = roughnessT * roughnessB; float3 v = float3(roughnessB * TdotH, roughnessT * BdotH, a2 * NdotH); float s = dot(v, v); return a2 * Sq(a2 / s); } float D_GGXAniso(float TdotH, float BdotH, float NdotH, float roughnessT, float roughnessB) { return INV_PI * D_GGXAnisoNoPI(TdotH, BdotH, NdotH, roughnessT, roughnessB); } float GetSmithJointGGXAnisoPartLambdaV(float TdotV, float BdotV, float NdotV, float roughnessT, float roughnessB) { return length(float3(roughnessT * TdotV, roughnessB * BdotV, NdotV)); } // Note: V = G / (4 * NdotL * NdotV) // Ref: https://cedec.cesa.or.jp/2015/session/ENG/14698.html The Rendering Materials of Far Cry 4 float V_SmithJointGGXAniso(float TdotV, float BdotV, float NdotV, float TdotL, float BdotL, float NdotL, float roughnessT, float roughnessB, float partLambdaV) { float lambdaV = NdotL * partLambdaV; float lambdaL = NdotV * length(float3(roughnessT * TdotL, roughnessB * BdotL, NdotL)); return 0.5 / (lambdaV + lambdaL); } float V_SmithJointGGXAniso(float TdotV, float BdotV, float NdotV, float TdotL, float BdotL, float NdotL, float roughnessT, float roughnessB) { float partLambdaV = GetSmithJointGGXAnisoPartLambdaV(TdotV, BdotV, NdotV, roughnessT, roughnessB); return V_SmithJointGGXAniso(TdotV, BdotV, NdotV, TdotL, BdotL, NdotL, roughnessT, roughnessB, partLambdaV); } // Inline D_GGXAniso() * V_SmithJointGGXAniso() together for better code generation. float DV_SmithJointGGXAniso(float TdotH, float BdotH, float NdotH, float NdotV, float TdotL, float BdotL, float NdotL, float roughnessT, float roughnessB, float partLambdaV) { float a2 = roughnessT * roughnessB; float3 v = float3(roughnessB * TdotH, roughnessT * BdotH, a2 * NdotH); float s = dot(v, v); float lambdaV = NdotL * partLambdaV; float lambdaL = NdotV * length(float3(roughnessT * TdotL, roughnessB * BdotL, NdotL)); float2 D = float2(a2 * a2 * a2, s * s); // Fraction without the multiplier (1/Pi) float2 G = float2(1, lambdaV + lambdaL); // Fraction without the multiplier (1/2) return (INV_PI * 0.5) * (D.x * G.x) / (D.y * G.y); } float DV_SmithJointGGXAniso(float TdotH, float BdotH, float NdotH, float TdotV, float BdotV, float NdotV, float TdotL, float BdotL, float NdotL, float roughnessT, float roughnessB) { float partLambdaV = GetSmithJointGGXAnisoPartLambdaV(TdotV, BdotV, NdotV, roughnessT, roughnessB); return DV_SmithJointGGXAniso(TdotH, BdotH, NdotH, NdotV, TdotL, BdotL, NdotL, roughnessT, roughnessB, partLambdaV); } //----------------------------------------------------------------------------- // Diffuse BRDF - diffuseColor is expected to be multiply by the caller //----------------------------------------------------------------------------- float LambertNoPI() { return 1.0; } float Lambert() { return INV_PI; } float DisneyDiffuseNoPI(float NdotV, float NdotL, float LdotV, float perceptualRoughness) { // (2 * LdotH * LdotH) = 1 + LdotV // float fd90 = 0.5 + 2 * LdotH * LdotH * perceptualRoughness; float fd90 = 0.5 + (perceptualRoughness + perceptualRoughness * LdotV); // Two schlick fresnel term float lightScatter = F_Schlick(1.0, fd90, NdotL); float viewScatter = F_Schlick(1.0, fd90, NdotV); // Normalize the BRDF for polar view angles of up to (Pi/4). // We use the worst case of (roughness = albedo = 1), and, for each view angle, // integrate (brdf * cos(theta_light)) over all light directions. // The resulting value is for (theta_view = 0), which is actually a little bit larger // than the value of the integral for (theta_view = Pi/4). // Hopefully, the compiler folds the constant together with (1/Pi). return rcp(1.03571) * (lightScatter * viewScatter); } float DisneyDiffuse(float NdotV, float NdotL, float LdotV, float perceptualRoughness) { return INV_PI * DisneyDiffuseNoPI(NdotV, NdotL, LdotV, perceptualRoughness); } // Ref: Diffuse Lighting for GGX + Smith Microsurfaces, p. 113. float3 DiffuseGGXNoPI(float3 albedo, float NdotV, float NdotL, float NdotH, float LdotV, float roughness) { float facing = 0.5 + 0.5 * LdotV; // (LdotH)^2 float rough = facing * (0.9 - 0.4 * facing) * (0.5 / NdotH + 1); float transmitL = F_Transm_Schlick(0, NdotL); float transmitV = F_Transm_Schlick(0, NdotV); float smooth = transmitL * transmitV * 1.05; // Normalize F_t over the hemisphere float single = lerp(smooth, rough, roughness); // Rescaled by PI float multiple = roughness * (0.1159 * PI); // Rescaled by PI return single + albedo * multiple; } float3 DiffuseGGX(float3 albedo, float NdotV, float NdotL, float NdotH, float LdotV, float roughness) { // Note that we could save 2 cycles by inlining the multiplication by INV_PI. return INV_PI * DiffuseGGXNoPI(albedo, NdotV, NdotL, NdotH, LdotV, roughness); } #endif // UNITY_BSDF_INCLUDED