Microfacet Theory — The Cook-Torrance Model微表面理论 — Cook-Torrance 模型

NeoX uses the standard Cook-Torrance microfacet specular BRDF, the same family used by UE5, Frostbite, and most modern PBR engines. The specular term is:

NeoX 使用标准的 Cook-Torrance 微表面镜面 BRDF,与 UE5、Frostbite 和大多数现代 PBR 引擎同族。高光项为:

D(h) * F(v,h) * G(l,v,h) f_specular = ----------------------------------- 4 * (N.L) * (N.V) Where: D(h) = Normal Distribution Function // how many microfacets align with H F = Fresnel reflectance // angle-dependent reflectivity G = Geometry / Visibility term // self-shadowing between microfacets H = half-vector = normalize(L + V) N.L = clamped dot(normal, light_dir) N.V = clamped dot(normal, view_dir)

D: GGX Normal Distribution FunctionD: GGX 法线分布函数

The D_GGX function (Trowbridge-Reitz distribution) determines what fraction of microfacets are oriented to reflect light toward the viewer. Higher roughness = wider distribution = blurrier highlights.

D_GGX 函数(Trowbridge-Reitz 分布)决定有多少微表面朝向将光线反射到观察者的方向。粗糙度越高 = 分布越宽 = 高光越模糊。

// [Walter et al. 2007, "Microfacet models for refraction through rough surfaces"]
float D_GGX(float a2, float NoH) {
  NoH = min(0.999, NoH);                      // prevent singularity at NoH=1
  float d = (NoH * a2 - NoH) * NoH + 1;       // 2 mad
  return a2 / (PI * d * d);                    // 4 mul, 1 rcp
}

// Optimized version with pre-squared roughness (used in isotropy.hlsl)
float GGXTerm_ApproxWithPrecomputedVar(float NdotH, float roughness_pow2) {
  float a = roughness_pow2 * roughness_pow2;   // a = roughness^4
  NdotH = min(0.9999, NdotH);
  float d = (NdotH * a - NdotH) * NdotH + 1.0;
  return a * rcp(max(PI * d * d, FLT_MIN));
}
Precision note: The source code comments warn that when roughness is very small and NoH approaches 1, consecutive squaring can produce precision loss leading to division by near-zero. The min(0.9999, NdotH) clamp prevents this. 精度注意:源码注释警告当粗糙度很小且 NoH 接近 1 时,连续平方可能产生精度损失导致接近零除法。min(0.9999, NdotH) 截断防止了这个问题。

F: Schlick Fresnel ApproximationF: Schlick 菲涅尔近似

The Fresnel term determines how much light is reflected vs transmitted at different viewing angles. At grazing angles, all surfaces become highly reflective.

菲涅尔项决定在不同观察角度下有多少光被反射而非透射。在掠射角度,所有表面都会变得高度反射。

float3 F_Schlick(float3 SpecularColor, float VoH) {
  float Fc = Pow5(1 - VoH);                    // (1-cosθ)^5
  // 2% minimum: anything below is physically impossible (shadowing)
  return saturate(50.0 * SpecularColor.g) * Fc
       + (1 - Fc) * SpecularColor;
}
The "50x green" trick: The saturate(50.0 * SpecularColor.g) factor ensures that even very dark specular colors (e.g. 0.02 F0 for dielectrics) still produce visible Fresnel at grazing angles. This is the same technique used by UE4/5, taken from Lazarov 2013. "50 倍绿色"技巧:saturate(50.0 * SpecularColor.g) 因子确保即使非常暗的高光颜色(如电介质 0.02 F0)在掠射角度仍能产生可见菲涅尔。这与 UE4/5 使用的技术相同,来自 Lazarov 2013。

G: Smith Joint GGX VisibilityG: Smith 联合 GGX 可见性

The geometry/visibility term accounts for microfacets blocking each other (masking and shadowing). NeoX implements the height-correlated Smith approximation:

几何/可见性项考虑微表面相互遮挡(遮蔽和阴影)。NeoX 实现了高度相关 Smith 近似:

float SmithJointGGXVisibilityTerm(float NdotL, float NdotV, float roughness_pow2) {
  float a = roughness_pow2;
  float lambdaV = NdotL * (NdotV * (1 - a) + a);
  float lambdaL = NdotV * (NdotL * (1 - a) + a);
  return 0.5 * rcp(max(lambdaV + lambdaL, FLT_MIN));
}

// Alternative: Schlick approximation (used for anisotropy)
float Vis_Schlick(float Roughness_pow2, float NoV, float NoL) {
  float k = Roughness_pow2 * 0.5;
  float Vis_SchlickV = NoV * (1 - k) + k;
  float Vis_SchlickL = NoL * (1 - k) + k;
  return 0.25 / (Vis_SchlickV * Vis_SchlickL);
}

Anisotropic GGX各向异性 GGX

For brushed metal and hair, NeoX supports anisotropic specular via D_GGXaniso (Disney's Burley 2012 formulation):

对于拉丝金属和头发,NeoX 通过 D_GGXaniso 支持各向异性高光(Disney Burley 2012 公式):

// [Burley 2012, "Physically-Based Shading at Disney"]
float D_GGXaniso(float RoughnessX, float RoughnessY,
                 float NoH, float3 H, float3 X, float3 Y) {
  float mx = RoughnessX * RoughnessX;
  float my = RoughnessY * RoughnessY;
  float XoH = dot(X, H);
  float YoH = dot(Y, H);
  float d = XoH*XoH / (mx*mx) + YoH*YoH / (my*my) + NoH*NoH;
  return 1 / (mx * my * d * d);
}

Environment BRDF — Split-Sum Approximation环境 BRDF — 分离求和近似

For IBL (Image-Based Lighting), evaluating the full integral is too expensive. NeoX uses the EnvBRDFApprox from Lazarov 2013 (same as UE4's original implementation):

对于 IBL(基于图像的光照),计算完整积分太昂贵。NeoX 使用来自 Lazarov 2013 的 EnvBRDFApprox(与 UE4 原始实现相同):

// [Lazarov 2013, "Getting More Physical in Call of Duty: Black Ops II"]
float3 EnvBRDFApprox(float3 SpecularColor, float Roughness, float NoV) {
  const float4 c0 = { -1, -0.0275, -0.572, 0.022 };
  const float4 c1 = {  1,  0.0425,  1.04, -0.04 };
  float4 r = Roughness * c0 + c1;
  float a004 = min(r.x * r.x, exp2(-9.28 * NoV)) * r.x + r.y;
  float2 AB = float2(-1.04, 1.04) * a004 + r.zw;
  return SpecularColor * AB.x + AB.y;
}

Full Specular Assembly — How It All Connects完整高光组装 — 如何组合

In the isotropy.hlsl shading model, the final specular term is assembled from D, G, and the Fresnel applied outside:

isotropy.hlsl 着色模型中,最终高光项由 D、G 和外部应用的菲涅尔组装:

// From isotropy.hlsl -- LightingSpecular()
float3 LightingSpecular(...) {
  return max(0.0,
    SmithJointGGXVisibilityTerm(n_dot_l, n_dot_v_abs, roughness_pow2)  // G term
    * GGXTerm_ApproxWithPrecomputedVar(n_dot_h, roughness_pow2)        // D term
    * n_dot_l                                                          // N.L
  );
}

// Called from DirectLighting():
light_specular = LightingSpecular(...) * data_rw.fresnel * data_rw.atten;
//                                       ^ F_Schlick      ^ shadow*light
Final Specular Per Light = D * G * N.L * F * attenuation * light_color D = GGXTerm_ApproxWithPrecomputedVar(NdotH, roughness²) G = SmithJointGGXVisibilityTerm(NdotL, NdotV, roughness²) F = F_Schlick(specularColor, VdotH) Note: The "/ 4*NdotL*NdotV" denominator from Cook-Torrance is already folded into the SmithJoint visibility term (the 0.5/rcp). The extra NdotL in LightingSpecular cancels with the one in the denominator.