最近在改进Unity的PBR渲染技术,为了进一步提升渲染品质,计划把UE4的渲染库搬到Unity中,查阅了相关的资料,找到了UE4的PBR公式,UE4对原有的公式做了一些改进,效率和品质都提升了不少。后面把它们整到一个cginc作为库文件使用,换句话说就是封装一个类似Unity的UnityStandardBRDF.cginc库。下面把BRDF的渲染公式以及对应代码展示一下,后面直接将其放到Unity即可,关于材质贴图参考UE4的进行设置。
UE4的BRDF渲染 使用 Cook-Torrance 反射模型
其中D、G、F分别是三个函数:
D:微平面在平面上的分布函数
G:计算微平面由于互相遮挡而产生的衰减
F:菲涅尔项
虚幻4采用 Trowbridge-Reitz GGX 模型:
n 为表面的宏观法向量
h hh入射光和观察方向的中间向量
α 为表面的粗糙度参数
代码实现:
// Trowbridge-Reitz GGX Distribution /UE4采用算法
inline float NormalDistributionGGX(float3 N, float3 H, float roughness)
{
// more: http://reedbeta.com/blog/hows-the-ndf-really-defined/
// NDF_GGXTR(N, H, roughness) = roughness^2 / ( PI * ( dot(N, H))^2 * (roughness^2 - 1) + 1 )^2
const float a = roughness * roughness;
const float a2 = a * a;
const float nh2 = pow(max(dot(N, H), 0), 2);
const float denom = (PI * pow((nh2 * (a2 - 1.0f) + 1.0f), 2));
if (denom < EPSILON) return 1.0f;
#if 0
return min(a2 / denom, 10);
#else
return a2 / denom;
#endif
}
虚幻4使用 Schlick 模型结合 Smith 模型计算此项,具体公式为:
对应的代码如下所示:
inline float Geometry_Smiths_SchlickGGX(float3 N, float3 V, float roughness)
{
// G_ShclickGGX(N, V, k) = ( dot(N,V) ) / ( dot(N,V)*(1-k) + k )
// for direct lighting or IBL
// k_direct = (roughness + 1)^2 / 8
// k_IBL = roughness^2 / 2
//
const float k = pow((roughness + 1.0f), 2) / 8.0f;
const float NV = max(0.0f, dot(N, V));
const float denom = (NV * (1.0f - k) + k) + 0.0001f;
//if (denom < EPSILON) return 1.0f;
return NV / denom;
}
代码如下:
inline float3 Fresnel_UE4(float3 N, float3 H, float3 F0)
{
return F0 + (float3(1, 1, 1) - F0) * pow(2, ((-5.55473) * dot(V, H) - 6.98316) * dot(V, H));
}
BRDFShader渲染:
float3 BRDF(float3 Wi, BRDF_Surface s, float3 V, float3 worldPos)
{
// vectors
const float3 Wo = normalize(V);
const float3 N = normalize(s.N);
const float3 H = normalize(Wo + Wi);
// surface
const float3 albedo = s.diffuseColor;
const float roughness = s.roughness;
const float metalness = s.metalness;
const float3 F0 = lerp(float3(0.04f, 0.04f, 0.04f), albedo, metalness);
// Fresnel_Cook-Torrence BRDF
const float3 F = Fresnel(H, V, F0);
const float G = Geometry(N, Wo, Wi, roughness);
const float NDF = NormalDistributionGGX(N, H, roughness);
const float denom = (4.0f * max(0.0f, dot(Wo, N)) * max(0.0f, dot(Wi, N))) + 0.0001f;
const float3 specular = NDF * F * G / denom;
const float3 Is = specular * s.specularColor;
// Diffuse BRDF
const float3 kS = F;
const float3 kD = (float3(1, 1, 1) - kS) * (1.0f - metalness) * albedo;
const float3 Id = F_LambertDiffuse(kD);
return (Id + Is);
}
float3 ImportanceSampleGGX(float2 Xi, float3 N, float roughness)
{
const float a = roughness * roughness;
const float phi = 2.0f * PI * Xi.x;
const float cosTheta = sqrt((1.0f - Xi.y) / (1.0f + (a * a - 1.0f) * Xi.y));
const float sinTheta = sqrt(1.0f - cosTheta * cosTheta);
// from sphreical coords to cartesian coords
float3 H;
H.x = cos(phi) * sinTheta;
H.y = sin(phi) * sinTheta;
H.z = cosTheta;
// from tangent-space to world space
const float3 up = abs(N.z) < 0.999 ? float3(0, 0, 1) : float3(1, 0, 0);
const float3 tangent = normalize(cross(up, N));
const float3 bitangent = cross(N, tangent);
const float3 sample = tangent * H.x + bitangent * H.y + N * H.z;
return normalize(sample);
}
float2 IntegrateBRDF(float NdotV, float roughness)
{
float3 V;
V.x = sqrt(1.0f - NdotV * NdotV);
V.y = 0;
V.z = NdotV;
float F0Scale = 0; // Integral1
float F0Bias = 0; // Integral2
const float3 N = float3(0, 0, 1);
for (int i = 0; i < SAMPLE_COUNT; ++i)
{
const float2 Xi = Hammersley(i, SAMPLE_COUNT);
const float3 H = ImportanceSampleGGX(Xi, N, roughness);
const float3 L = normalize(reflect(-V, H));
const float NdotL = max(L.z, 0);
const float NdotH = max(H.z, 0);
const float VdotH = max(dot(V, H), 0);
if(NdotL > 0.0f)
{
const float G = GeometryEnvironmentMap(N, V, L, roughness);
const float G_Vis = (G * VdotH) / ((NdotH * NdotV) + 0.0001);
const float Fc = pow(1.0 - VdotH, 5.0f);
F0Scale += (1.0f - Fc) * G_Vis;
F0Bias += Fc * G_Vis;
}
}
return float2(F0Scale, F0Bias) / SAMPLE_COUNT;
}
上述代码对应的引用函数实现代码如下所示:
float3 ImportanceSampleGGX(float2 Xi, float3 N, float roughness)
{
const float a = roughness * roughness;
const float phi = 2.0f * PI * Xi.x;
const float cosTheta = sqrt((1.0f - Xi.y) / (1.0f + (a * a - 1.0f) * Xi.y));
const float sinTheta = sqrt(1.0f - cosTheta * cosTheta);
// from sphreical coords to cartesian coords
float3 H;
H.x = cos(phi) * sinTheta;
H.y = sin(phi) * sinTheta;
H.z = cosTheta;
// from tangent-space to world space
const float3 up = abs(N.z) < 0.999 ? float3(0, 0, 1) : float3(1, 0, 0);
const float3 tangent = normalize(cross(up, N));
const float3 bitangent = cross(N, tangent);
const float3 sample = tangent * H.x + bitangent * H.y + N * H.z;
return normalize(sample);
}
// Smith's Schlick-GGX for Environment Maps
inline float Geometry_Smiths_SchlickGGX_EnvironmentMap(float3 N, float3 V, float roughness)
{ // describes self shadowing of geometry
//
// G_ShclickGGX(N, V, k) = ( dot(N,V) ) / ( dot(N,V)*(1-k) + k )
//
// k : remapping of roughness based on wheter we're using geometry function
// for direct lighting or IBL
// k_direct = (roughness + 1)^2 / 8
// k_IBL = roughness^2 / 2
//
const float k = pow(roughness, 2) / 2.0f;
const float NV = max(0.0f, dot(N, V));
const float denom = (NV * (1.0f - k) + k) + 0.0001f;
//if (denom < EPSILON) return 1.0f;
return NV / denom;
}
float GeometryEnvironmentMap(float3 N, float3 V, float3 L, float k)
{ // essentially a multiplier [0, 1] measuring microfacet shadowing
float geomNV = Geometry_Smiths_SchlickGGX_EnvironmentMap(N, V, k);
float geomNL = Geometry_Smiths_SchlickGGX_EnvironmentMap(N, L, k);
return geomNV * geomNL;
}
float2 Hammersley(int i, int count)
{
#ifdef USE_BIT_MANIPULATION
return float2(float(i) / float(count), RadicalInverse_VdC(uint(i)));
#else
// note: this crashes for some reason.
return float2(float(i) / float(count), VanDerCorpus(uint(i), 2u));
#endif
}
渲染效果:
参考网址:
https://neil3d.github.io/assets/pdf/s2013_pbs_epic_notes_v2.pdf
http://blog.selfshadow.com/publications/s2012-shading-course/hoffman/s2012_pbs_physics_math_notes.pdf