移植UE4的BRDR算法到Unity

最近在改进Unity的PBR渲染技术,为了进一步提升渲染品质,计划把UE4的渲染库搬到Unity中,查阅了相关的资料,找到了UE4的PBR公式,UE4对原有的公式做了一些改进,效率和品质都提升了不少。后面把它们整到一个cginc作为库文件使用,换句话说就是封装一个类似Unity的UnityStandardBRDF.cginc库。下面把BRDF的渲染公式以及对应代码展示一下,后面直接将其放到Unity即可,关于材质贴图参考UE4的进行设置。

BRDF算法

UE4的BRDF渲染 使用 Cook-Torrance 反射模型
在这里插入图片描述
其中D、G、F分别是三个函数:

D:微平面在平面上的分布函数
G:计算微平面由于互相遮挡而产生的衰减
F:菲涅尔项

D:微平面分布函数(Normal distribution function)

虚幻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
}

G:几何遮挡因子(Geometric Occlusion Factor)

虚幻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;
}

F:菲涅尔项(Fresnel Factor)

在这里插入图片描述

代码如下:

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);
}

Image-Based Lighting

在这里插入图片描述

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);
}

Environment BRDF

在这里插入图片描述
在这里插入图片描述

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章