SpecularGGX - 直接光高光整合
2025-11-10
| 2025-11-11
Words 1320Read Time 4 min
type
status
date
slug
summary
tags
category
icon
password

主代码:

half3 SpecularGGX(inout BxDFContext Context, half3 L, half3 V, half3 N, half Roughness, half3 SpecularColor) //LYY
{
float m = Roughness * Roughness;
half3 H = normalize(V + L);
Context.NoV = saturate(abs(dot(N, V)) + 1e-5);
Context.VoH = saturate(dot(V, H));
Context.NoH = saturate(dot(N, H));
float D = D_GGX_R2(m, Context.NoH);
float Vis = Vis_SmithJointApprox(m, Context.NoV, Context.NoL); // vis近似于G / (4 * NoV*Nol)
float3 F = F_Schlick(SpecularColor, Context.VoH);
return (D * Vis) * F;
}
这个函数近似于(D * G * F ) / (4 * NoV*NoL)
也就是标准pbr的算直接光高光的公式(优化+ 近似)

支持函数

float D_GGX_R2(half a, float NoH)
{
float a2 = a * a;
float d = (NoH * a2 - NoH) * NoH + 1; // 2 mad
return min(a2 / (d * d), 2048.0); // 4 mul, 1 rcp
}
float Vis_SmithJointApprox(float a, float NoV, float NoL)
{
float Vis_SmithV = NoL * (NoV * (1.0f - a) + a);
float Vis_SmithL = NoV * (NoL * (1.0f - a) + a);
float t = 1.0f / max((Vis_SmithV + Vis_SmithL), 0.001f);
return 0.5f * t;
}
float3 F_Schlick(half3 specular_color, float VoH)
{
float __fc = 1.0f - VoH;
float _fc = __fc * __fc;
float Fc = _fc * _fc * __fc;
float3 temp1 = float3(saturate( 50.0f * specular_color.g ) * Fc);
float3 temp2 = float3((1.0f - Fc) * specular_color);
return temp1 + temp2;
}

解析:

D的优化

这是标准DGF的D (α是粗糙度的平方)
notion image
那么我们的函数是:
float D_GGX_R2(half a, float NoH)
{
float a2 = a * a;
float d = (NoH * a2 - NoH) * NoH + 1; // 2 mad
return min(a2 / (d * d), 2048.0); // 4 mul, 1 rcp
}
优化包括:
  1. 平方肯定变成乘法 pow变成乘法链
  1. 1/PI 没有了
  1. 数值稳定性处理
使用min(..., 2048.0)限制最大值,防止在极端情况下(如粗糙度极低时)出现数值爆炸,这种截断处理对视觉效果影响极小,但能避免渲染管线中的异常值
  1. 指令优化。注释中标注了 "2 mad"(multiply-add,乘加指令)和 "4 mul, 1 rcp"(乘法和倒数指令)
其实就是把pow啊 平方啊 这些烦人的东西 变成乘法加法。。。
notion image

vis

//UE4.27 来自ue的函数吗
//roughness在外面Pow2(), UE是在外面Pow4,但是要在Vis项sqrt()
//context.NoV在外面做过大于0了,这里不需要再max
// Appoximation of joint Smith term for GGX
// [Heitz 2014, "Understanding the Masking-Shadowing Function in Microfacet-Based BRDFs"]
float Vis_SmithJointApprox(float a, float NoV, float NoL)
{
float Vis_SmithV = NoL * (NoV * (1.0f - a) + a);
float Vis_SmithL = NoV * (NoL * (1.0f - a) + a);
float t = 1.0f / max((Vis_SmithV + Vis_SmithL), 0.001f);
return 0.5f * t;
}
这个函数直接计算了G/(4NoVNoL)的近似值(即Vis)

G复习(G_SchlickGGX近似)

首先我们复习一下,G项是由两个G即几何遮蔽和几何阴影想成构成的,又是开方又是平方的。这个不划算
notion image
所以都是用近似,一种是G_SchlickGGX(Schlick 对 GGX 阴影函数的 “单项近似”(注意是 G1,不是完整的 G(l,v)))
公式形式:G_SchlickGGX(n,v,k) = (n·v) / [(n·v)(1−k) + k]
它的作用:近似单个方向(视角方向 v 或光照方向 l)的阴影遮挡(即 G1(v) 或 G1(l))。 用的时候是分布用L 和 V代入。再相乘的
总之,G项的核心就是 粗糙度 对高光遮挡的影响(当表面粗糙度较高时,G 项的变化趋势会更接近N·L的分布 —— 光线越垂直表面(N·L越大),亮度越高,这和兰伯特的 “正对光线更亮” 的直观感受一致。但是依然会收到视角v的轻微影响。而在高低糙度下,就不太一样了,和视角强关联 )

vis的近似

notion image
这个函数直接计算了G/(4NoVNoL)的近似值(即Vis)
其实没啥好说的,就是某哥们研究的近似值。用在ue4早期
都是近似,G_SchlickGGX近似 更耗费一点(所有一般pc啥的,我们项目用vis近似,主要还是为了这个移动平台来着)

F的近似

float3 F_Schlick(half3 specular_color, float VoH)
{
float __fc = 1.0f - VoH;
float _fc = __fc * __fc;
float Fc = _fc * _fc * __fc;
float3 temp1 = float3(saturate( 50.0f * specular_color.g ) * Fc);
float3 temp2 = float3((1.0f - Fc) * specular_color);
return temp1 + temp2;
}
我们知道:
标准 Schlick 近似公式为:
F(VoH) = F0 + (1 - F0) * (1 - VoH)^5
那么5次方这个太耗
所以我们函数的优化是:
  1. 5次方变成 乘法链
//这里的通过temp1 和 temp2的方式,理论上是避免分支来写的,但是为啥呢???? 怀疑是早期的什么遗留问题。

低画质下的选择

half3 SpecularGGXSimple(inout BxDFContext Context, half3 L, half3 V, half3 N, half Roughness, half3 SpecularColor)
{
half3 H = normalize(V + L);
Context.NoH = saturate(dot(N, H));
return (Roughness * 0.25h + 0.25h) * GGX_Mobile(Roughness, Context.NoH) * SpecularColor;
}
//便宜GGX,中低配使用
// Taken from https://gist.github.com/romainguy/a2e9208f14cae37c579448be99f78f25
// Modified by Epic Games, Inc. To account for premultiplied light color and code style rules.
half GGX_Mobile(half roughness, half NoH)
{
// Walter et al. 2007, "Microfacet Models for Refraction through Rough Surfaces"
float OneMinusNoHSqr = 1.0 - NoH * NoH;
float a = roughness * roughness;
float n = NoH * a;
float p = a / (OneMinusNoHSqr + n * n);
float d = p * p;
// clamp to avoid overlfow in a bright env
return min(d, 2048.0);
}
GGX_Mobile:对 D 项(法线分布函数)的粗暴简化(G和F都没了)
保留 “高光强度随NoH变化”(半程向量越接近法线,高光越亮)和 “粗糙度控制高光扩散范围” 这两个最直观的高光特征
  • HLSL
  • 渲染
  • 图形学
  • PreintegratedSkin预积分皮肤BxDFheightfield 地形节点学习笔记( 图片太多 还在搬运ing)
    Loading...