移植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

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