1/3
2/3
3/3
[笔记]URP-我教我自己写一个PBR
艺术菌毯
2022年04月11日 03:36
收录于文集
共9篇
  • 前言

        这个应该是URP系列的最后一篇了,造完PBR的轮子以后就学去学一些特效的知识吧。

        PBR的前置知识请参考这两篇吧,PBR理论​PBR光照​,原文就是OpenGL教程中的PBR。

        复刻知识请参考这篇文章,https://zhuanlan.zhihu.com/p/298223622 它整合了很多大佬的知识,大佬们的文章链接他都有挂,比较全面。

        此外,如果遇到看不懂的源码,可以查看这篇文章,https://zhuanlan.zhihu.com/p/371395846

        此外最官方的其实是Unity自己的实现,所以我们结合LitForwardPass.hlsl中的内容和上面的链接来造一个PBR的轮子。

  • Shader骨架与参数

        要传入的参数只有五个,_MainTex是物体的贴图,_Tint是贴图要乘上的颜色,_Metallic是金属度,_Smoothness是材质的光滑度,_LUT是一张作为查找表的贴图,具体作用会在后面讲到。

代码块
JavaScript
自动换行
复制代码
Shader "URP/URPPBR"
{
	Properties
	{
		_MainTex("Texture", 2D) = "white" {}
		_Tint("Tint", Color) = (1 ,1 ,1 ,1)
		[Gamma] _Metallic("Metallic", Range(0, 1)) = 0 //金属度要经过伽马校正
		_Smoothness("Smoothness", Range(0, 1)) = 0.5
		_LUT("LUT", 2D) = "white" {}
	}

	SubShader
	{
		Tags { 
		"RenderType" = "Opaque" 
		"RenderPipeline" = "UniversalPipeline"
		}

		Pass
		{
			Tags {
				"LightMode" = "UniversalForward"
			}

			HLSLPROGRAM
			#pragma target 3.0
			#pragma vertex vert
			#pragma fragment frag
			
			#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
			#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"

			CBUFFER_START
				float4 _Tint;
				float _Metallic;
				float _Smoothness;
				float4 _MainTex_ST;
			CBUFFER_END

			TEXTURE2D(_MainTex);
			SAMPLER(sampler_MainTex);
			TEXTURE2D(_LUT);
			SAMPLER(sampler_LUT);

			struct a2v
			{
				float4 vertex : POSITION;
				float3 normal : NORMAL;
				float2 uv : TEXCOORD0;
			};

			struct v2f
			{
				float4 vertex : SV_POSITION;
				float2 uv : TEXCOORD0;
				float3 normal : TEXCOORD1;
				float3 worldPos : TEXCOORD2;
			};


			v2f vert(a2v v)
			{
				v2f o;
				o.vertex = UnityObjectToClipPos(v.vertex);
				o.worldPos = mul(unity_ObjectToWorld, v.vertex);
				o.uv = TRANSFORM_TEX(v.uv, _MainTex);
				o.normal = UnityObjectToWorldNormal(v.normal);
				o.normal = normalize(o.normal);
				return o;
			}

			fixed4 frag(v2f i) : SV_Target
			{
				float3 diffColor = 0;
				float3 specColor = 0;
				float3 DirectLightResult = diffColor + specColor;

				float3 iblDiffuseResult = 0;
				float3 iblSpecularResult = 0;
				float3 IndirectResult = iblDiffuseResult + iblSpecularResult;

				float4 result = float4(DirectLightResult + IndirectResult, 1);

				return result;
			}

			ENDHLSL
		}
	}
}
复制成功

        金属度是一个0到1范围内的浮点数,表示被渲染物的表面材质是不是金属,0表示非金属,1表示金属,0和1之间的值的作用是表现诸如沾有沙子的金属表面之类的复杂材质。

        光滑度也是一个0到1范围内的浮点数,表示被渲染物表面材质的光滑程度,1表示光滑,0表示粗糙。

        PBR其实远不止这几个参数,只不过是前人在使用的过程中给我们优化总结了。

        金属度前面有一个[Gamma],就是告诉我们这个值是gamma空间的,它会转化到线性空间,如果不加,那就和Unity的Shader效果不一样了。

        这里提一个小Tricks,我们可以在Library\PackageCache\com.unity.render-pipelines.universal@11.0.0\ShaderLibrary和Library\PackageCache\com.unity.render-pipelines.universal@11.0.0\Shaders这两个目录下的hlsl和shader复制到一个文件夹,然后用VS打开这个文件夹,就可以方便地搜索了,不然Unity的源码太难啃了。

  • 效果比对场景搭建

        我们首先来看直接光部分。

        下面是Unity关于直接光计算的源码。

代码块
JavaScript
自动换行
复制代码
    color += LightingPhysicallyBased(brdfData, brdfDataClearCoat, mainLight, inputData.normalWS, inputData.viewDirectionWS, surfaceData.clearCoatMask, specularHighlightsOff);
复制成功

        我们先仿照Unity源码写一个直接光部分的内容。

        我累了,明天再写吧。

        休息好了,接着写~

        现在我们发现,我们需要brdfData和inputData,而brdfData由这个函数得到

代码块
JavaScript
自动换行
复制代码
    InitializeBRDFData(surfaceData.albedo, surfaceData.metallic, surfaceData.specular, surfaceData.smoothness, surfaceData.alpha, brdfData);
复制成功

        现在转化了为了对surfaceData和inputData的需求。

        而SurfaceData和InputData通过这两个函数得到,Unity封装的很好。

代码块
JavaScript
自动换行
复制代码
    SurfaceData surfaceData;
    InitializeStandardLitSurfaceData(input.uv, surfaceData);

    InputData inputData;
    InitializeInputData(input, surfaceData.normalTS, inputData);
复制成功

        读了下大佬的文章,我发现Built-in渲染管线里封装的更好,更简单方便于测试,我们就直接用Built-in里的PBR直接光部分吧,哈哈哈哈。

代码块
JavaScript
自动换行
复制代码
				// 从金属度生成漫反射颜色,镜面反射颜色等
				albedo = DiffuseAndSpecularFromMetallic(albedo, _Metallic, specularTint, oneMinusReflectivity);
复制成功

        直接光计算的部分如下

代码块
JavaScript
自动换行
复制代码
				return UNITY_BRDF_PBS(
					albedo, specularTint,
					oneMinusReflectivity, _Smoothness,
					i.normal, viewDir,
					light, indirectLight);
复制成功

        最终直接光部分的完整代码如下:

代码块
JavaScript
自动换行
复制代码
Shader "URP/URPPBRDirect"
{
    Properties
    {
		_Tint ("Tint", Color) = (1 ,1 ,1 ,1)
        _MainTex ("Texture", 2D) = "white" {}
		[Gamma] _Metallic("Metallic", Range(0, 1)) = 0
		_Smoothness("Smoothness", Range(0, 1)) = 0.5
    }
    SubShader
    {
        Tags { 
			"RenderType" = "Opaque" 
			"RenderPipeline" = "UniversalPipeline"
			}

	    Pass
	    {
			Tags {
			"LightMode" = "UniversalForward"
			}
	
	        CGPROGRAM
		    #pragma target 3.0
	
	        #pragma vertex vert
	        #pragma fragment frag
	
		    #include "UnityPBSLighting.cginc"
	
	        struct appdata
		    {
		        float4 vertex : POSITION;
				float3 normal : NORMAL;
		        float2 uv : TEXCOORD0;
		    };
	
	        struct v2f
		    {
		        float4 vertex : SV_POSITION;
				float2 uv : TEXCOORD0;
				float3 normal : TEXCOORD1;
				float3 worldPos : TEXCOORD2;
		    };
	
		    float4 _Tint;
		    float _Metallic;
		    float _Smoothness;
	        sampler2D _MainTex;
	        float4 _MainTex_ST;
	
	        v2f vert (appdata v)
		    {
		        v2f o;
		        o.vertex = UnityObjectToClipPos(v.vertex);
				o.worldPos = mul(unity_ObjectToWorld, v.vertex);
		        o.uv = TRANSFORM_TEX(v.uv, _MainTex);
				o.normal = UnityObjectToWorldNormal(v.normal);
				o.normal = normalize(o.normal);
		        return o;
		    }
	
	        fixed4 frag (v2f i) : SV_Target
		    {
				i.normal = normalize(i.normal);
				float3 lightDir = _WorldSpaceLightPos0.xyz;
				float3 viewDir = normalize(_WorldSpaceCameraPos - i.worldPos);
				float3 lightColor = _LightColor0.rgb;

				float3 specularTint;
				float oneMinusReflectivity;
				float3 albedo = tex2D(_MainTex, i.uv).rgb * _Tint.rgb;

				// 从金属度生成漫反射颜色,镜面反射颜色等
				albedo = DiffuseAndSpecularFromMetallic(albedo, _Metallic, specularTint, oneMinusReflectivity);
					
				UnityLight light;
				light.color = lightColor;
				light.dir = lightDir;
				light.ndotl = DotClamped(i.normal, lightDir);
				UnityIndirect indirectLight;
				indirectLight.diffuse = 0;
				indirectLight.specular = 0;

				//生成直接光pbr结果
				return UNITY_BRDF_PBS(
					albedo, specularTint,
					oneMinusReflectivity, _Smoothness,
					i.normal, viewDir,
					light, indirectLight);
		    }
	        ENDCG
	    }
	}
}
复制成功

        我们尽量让手写PBR接近Unity的效果。

  • Shader编写

        接下来我们提前准备好用于计算的参数:

        我们需要入射光方向、观察方向、物体法线方向、半角方向。

代码块
JavaScript
自动换行
复制代码
		Light mainLight = GetMainLight();
		float3 normalWS = normalize(i.normalWS);
		float3 lightDirWS = mainLight.direction;
		float3 viewDirWS = normalize(_WorldSpaceCameraPos.xyz-i.positionWS.xyz);
		float3 halfVector = normalize(lightDirWS+viewDirWS);
复制成功

        把这些向量提前准备好。

        然后计算好粗糙度相关的参数,一次方,平方和四次方。

代码块
JavaScript
自动换行
复制代码
		float roughness = 1 - _Smoothness;
		float roughness2 = roughness*roughness;
		float roughness4 = roughness2*roughness2;
复制成功

(上面这段代码修正一下,roughness=perpetualRoughness,roughness2=roughness,roughness2=roughness4,不然下文容易误会,然后手机改不了代码)

        然后准备好各种点乘的结果:

代码块
JavaScript
自动换行
复制代码
		float nDotL = max(0.00001, dot(normalWS, lightDirWS));
		float nDotV = max(0.00001, dot(normalWS, viewDirWS));
		float vDotH = max(0.00001, dot(halfVector, viewDirWS));
		float lDotH = max(0.00001, dot(halfVector, lightDirWS));
		float nDotH = max(0.00001, dot(normalWS, halfVector));
复制成功

        好了,接下来我们开始编写Brdf直接光的几个部分。

#1 直接光漫反射

        上述公式的左边就是直接光漫反射,把它简化一下就是下面的结果:

        它的本质就是兰伯特光照模型。

        就是多除了一个PI,为什么要除PI涉及到了立体角的定积分,这里可以去看GAMES101或者《Real-time Rendering》,不是很难。

代码块
JavaScript
自动换行
复制代码
		float Kd = 1.0f;
		float3 albedo = _Tint * SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, i.uv);
		float3 diffColor = Kd * albedo * mainLight.color * nDotL ;
复制成功

        Kd代表的是漫反射部分,又或者理解为折射部分,但是!Unity源码里没有除PI,原因如下:

代码块
JavaScript
自动换行
复制代码
// HACK: theoretically we should divide diffuseTerm by Pi and not multiply specularTerm!
// BUT 1) that will make shader look significantly darker than Legacy ones
// and 2) on engine side "Non-important" lights have to be divided by Pi too in cases when they are injected into ambient SH
复制成功

        两个原因:原因1是太暗了,原因2则是,当计算环境光的时候,非重要光源会被除PI,这里不做多余处理。

        那么我们也不除呗,然后现在就得到了直接光漫反射的部分了。

        得到了一个很标准的兰伯特模型,爱了爱了。

#2 直接光镜面反射

        照旧,先来看公式:

        右边的部分是直接光的镜面反射部分,我们主要关注DFG部分。

        我们首先来看D,Normal Distribution Function,这个式子被称为GGX

        α是roughness,n为法线,h为半角向量。

        注意,D中的分母有PI,但是实际BRDF计算是没有除以PI的。

        所以这一部分的计算如下:

代码块
JavaScript
自动换行
复制代码
		float divisorD = pow(nDotH*nDotH*(roughness2-1)+1.00001f,2);
		float D = roughness2/divisorD;
复制成功

        接下来计算G部分,也就是几何部分

        Built-in管线里是老老实实计算的,菲涅尔方程如下:

        但是URP里对这部分进行了优化。

        它将G和F以及分母统一变成V函数。

        所以这个时候就可以把D * V算出来了。

代码块
JavaScript
自动换行
复制代码
		float V = 1/(lDotH2*(roughness+0.5));
		float Fspec = D*V;
复制成功

         但是这里还有两个点,Ks和Kd没有计算。

        我们看下源码,得知了Unity的计算方式如下:

代码块
JavaScript
自动换行
复制代码
float Kd = kDieletricSpec.a - _Metallic * kDieletricSpec.a;
float Ks = lerp(kDieletricSpec.rgb, albedo, _Metallic);
复制成功

        所以我们把Kd和Ks带入最终的式子,就得到了直接光部分的光照。

  • 间接光

        间接光总公式还是一样的。

        URP里的间接光算法和Built-in里其实没有太大的区别。

        

#1 间接光漫反射

        这一部分的计算就是预积分,因为我们不可能在实时渲染去计算每一个像素的所有方向的积分,计算耗时太大。所以这里用的是预积分的处理方式,根据然后根据顶点的法线方向进行采样。

         预积分的核心就是知识是 蒙特卡洛积分。

        但是Unity里用球谐函数对预积分的结果进行了一个近似,球谐函数理解的核心是傅里叶变换,具体内容可以看这里:网页链接​ 

        所以Unity URP里会有这个函数:

代码块
JavaScript
自动换行
复制代码
half3 SampleSH(half3 normalWS)
{
    // LPPV is not supported in Ligthweight Pipeline
    real4 SHCoefficients[7];
    SHCoefficients[0] = unity_SHAr;
    SHCoefficients[1] = unity_SHAg;
    SHCoefficients[2] = unity_SHAb;
    SHCoefficients[3] = unity_SHBr;
    SHCoefficients[4] = unity_SHBg;
    SHCoefficients[5] = unity_SHBb;
    SHCoefficients[6] = unity_SHC;

    return max(half3(0, 0, 0), SampleSH9(SHCoefficients, normalWS));
}
复制成功

        所以我们调用这个函数进行采样即可。

        采样代码如下:

代码块
JavaScript
自动换行
复制代码
		half3 ambient_GI = SampleSH(normalWS);
		float3 iblDiffuseResult = ambient_GI * Kd * albedo;
复制成功

        已经很像了哦。

        截至到这一部分,完整代码如下。

        之前没保存,自动退出了,我好心痛!

代码块
JavaScript
自动换行
复制代码
Shader "URP/URPPBR"
{
	Properties
	{
		_MainTex("Texture", 2D) = "white" {}
		_Tint("Tint", Color) = (1 ,1 ,1 ,1)
		[Gamma] _Metallic("Metallic", Range(0, 1)) = 0 //金属度要经过伽马校正
		_Smoothness("Smoothness", Range(0, 1)) = 0.5
		_LUT("LUT", 2D) = "white" {}
	}

	SubShader
	{
		Tags { 
		"RenderType" = "Opaque" 
		"RenderPipeline" = "UniversalPipeline"
		}

		Pass
		{
			Tags {
				"LightMode" = "UniversalForward"
			}

			HLSLPROGRAM
			#pragma target 3.0
			#pragma vertex vert
			#pragma fragment frag
			
			#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
			#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"

			#define HALF_MIN_SQRT 0.0078125
			#define HALF_MIN 6.103515625e-5
			#define PI 3.1415926

			CBUFFER_START(UnityPerMaterial)
				float4 _Tint;
				float _Metallic;
				float _Smoothness;
				float4 _MainTex_ST;
			CBUFFER_END

			TEXTURE2D(_MainTex);
			SAMPLER(sampler_MainTex);
			TEXTURE2D(_LUT);
			SAMPLER(sampler_LUT);

			struct a2v
			{
				float4 positionOS : POSITION;
				float3 normalOS : NORMAL;
				float2 uv : TEXCOORD0;
			};

			struct v2f
			{
				float4 positionCS : SV_POSITION;
				float2 uv : TEXCOORD0;
				float3 normalWS : TEXCOORD1;
				float3 positionWS : TEXCOORD2;
			};


			v2f vert(a2v v)
			{
				v2f o;
				o.positionCS = TransformObjectToHClip(v.positionOS);
				o.positionWS = TransformObjectToWorld(v.positionOS);
				o.uv = TRANSFORM_TEX(v.uv, _MainTex);
				o.normalWS = TransformObjectToWorldNormal(v.normalOS);
				o.normalWS = normalize(o.normalWS);
				return o;
			}

			half4 frag(v2f i) : SV_Target
			{
				Light mainLight = GetMainLight();
				float3 normalWS = normalize(i.normalWS);
				float3 lightDirWS = mainLight.direction;
				float3 viewDirWS = normalize(_WorldSpaceCameraPos.xyz-i.positionWS.xyz);
				float3 halfVector = normalize(lightDirWS+viewDirWS);

				float nDotL = max(0, dot(normalWS, lightDirWS));
				float nDotV = max(0, dot(normalWS, viewDirWS));
				float vDotH = max(0, dot(halfVector, viewDirWS));
				float lDotH = max(0, dot(halfVector, lightDirWS));
				float lDotH2 = max(0.1f, pow(lDotH,2));
				float nDotH = max(0, dot(normalWS, halfVector));

				float3 albedo = _Tint * SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, i.uv);
				float Kd = kDieletricSpec.a - _Metallic * kDieletricSpec.a;

				float perceptualRoughness = 1 - _Smoothness;
				float roughness = max(perceptualRoughness * perceptualRoughness, HALF_MIN_SQRT);
				float roughness2 = max(roughness*roughness, HALF_MIN);

				float divisorD = pow(nDotH*nDotH*(roughness2-1)+1.00001f,2);
				float D = roughness2/divisorD;
				float V = 1/(lDotH2*(roughness+0.5));
				float Fspec = D*V;
				float Ks = lerp(kDieletricSpec.rgb, albedo, _Metallic);

				float3 diffColor = Kd * albedo * mainLight.color * nDotL;
				float3 specColor = Ks * albedo * Fspec * mainLight.color * nDotL;
				float3 DirectLightResult = diffColor + specColor;

				half3 ambient_GI = SampleSH(normalWS);
				float3 iblDiffuseResult = ambient_GI * Kd * albedo;

				float3 iblSpecularResult = 0;
				float3 IndirectResult = iblDiffuseResult + iblSpecularResult;

				float4 result = float4(DirectLightResult + IndirectResult, 1);

				return result;
			}
			ENDHLSL
		}
	}
}
复制成功

#2 间接光镜面反射

        间接光镜面反射方程如下:

        我们可以把它简化乘下面的样子:

        左边的括号是一张预积分贴图,它和两个参数有关,第一个参数是观察方向,第二个参数是粗糙度。为什么只有两个参数,因为我们假设观察方向和法线方向相等。理论知识请看这里:IBL镜面反射​

        然后LOD贴图则和粗糙度挂钩,对贴图进行三次线性采样来模拟粗糙度参数。

        然后再根据nDotV和Roughness对BRDF贴图进行采样。BRDF贴图就是根据nDotV和Roughness的值,然后对入射光的方向进行解积分得到的贴图,由于nDotV和Roughness的值域都是(0,1),所以恰好是一张类似下面的贴图。

        这里面还有很多的细节没讲,比如重要性采样,低差异序列,立方体贴图接缝问题等等。但是Unity都帮我们处理好了,我们直接来看Unity的实现吧。

        首先是对环境预积分贴图进行采样,它是通过GlossyEnvironmentReflection函数来实现,具体思路是:通过roughness去计算CubeMap的MipLevel,然后采样对应Level的CubeMap,最后将其解码,并乘以AO。

代码块
JavaScript
自动换行
复制代码
half3 GlossyEnvironmentReflection(half3 reflectVector, half perceptualRoughness, half occlusion)
{
#if !defined(_ENVIRONMENTREFLECTIONS_OFF)
    half mip = PerceptualRoughnessToMipmapLevel(perceptualRoughness);
    half4 encodedIrradiance = SAMPLE_TEXTURECUBE_LOD(unity_SpecCube0, samplerunity_SpecCube0, reflectVector, mip);

//TODO:DOTS - we need to port probes to live in c# so we can manage this manually.
#if defined(UNITY_USE_NATIVE_HDR) || defined(UNITY_DOTS_INSTANCING_ENABLED)
    half3 irradiance = encodedIrradiance.rgb;
#else
    half3 irradiance = DecodeHDREnvironment(encodedIrradiance, unity_SpecCube0_HDR);
#endif

    return irradiance * occlusion;
#endif // GLOSSY_REFLECTIONS

    return _GlossyEnvironmentColor.rgb * occlusion;
}
复制成功

        occlusion是AO,比如SSAO或者自带的AO贴图等,这里我们没有算,之后再有空再补上吧。

代码块
JavaScript
自动换行
复制代码
real PerceptualRoughnessToMipmapLevel(real perceptualRoughness)
{
    return PerceptualRoughnessToMipmapLevel(perceptualRoughness, UNITY_SPECCUBE_LOD_STEPS);
}
复制成功

        看完这两个函数,代码就差不多会写了。       

(下面代码纠错,rgbm=encodingIrradiance) 

代码块
JavaScript
自动换行
复制代码
		float mip_roughness = perceptualRoughness * (1.7 - 0.7 * perceptualRoughness);
		float3 refDirWS = reflect(-viewDirWS, normalWS);

		half mip = mip_roughness * UNITY_SPECCUBE_LOD_STEPS;
		half4 rgbm = SAMPLE_TEXTURECUBE_LOD(unity_SpecCube0, samplerunity_SpecCube0, refDirWS, mip); 

		float3 iblSpecular = albedo * DecodeHDREnvironment(encodedIrradiance, unity_SpecCube0_HDR);
复制成功

        第一行是采样用的粗糙度的计算,Unity的粗糙度和采样的mipmap等级关系不是线性的,Unity内使用的转换公式为mip = r(1.7 - 0.7r),这是Unity shader的实现,只是个很接近实际值的拟合曲线,真正的计算方式如下:

代码块
JavaScript
自动换行
复制代码
float m = roughness*roughness;
const float fEps = 1.192092896e-07F;
float n =  (2.0 / max(fEps, m * m)) - 2.0;
n /= 4;
roughness = pow( 2 / (n + 2), 0.25);
复制成功

        计算方式来源于这里,学有余力的小伙伴去钻研一下吧http://jbit.net/~sparky/academic/mm_brdf.pdf

        第二行好说,就是根据视线方向和法线求出个反射向量留着以后用。

        第三行是用从0到1之间的mip_roughness函数换算出用于实际采样的mip层级,UNITY_SPECCUBE_LOD_STEPS是一个定义在UnityStandardConfig.cginc文件中的常量,没改的话就是6。

        最后一行使用DecodeHDR将颜色从HDR编码下解码。可以看到采样出的rgbm是一个4通道的值,最后一个m存的是一个参数,解码时将前三个通道表示的颜色乘上xM^y,x和y都是由环境贴图定义的系数,存储在unity_SpecCube0_HDR这个结构中。

        于是我们得到了iblSpeclar,也就是上面间接高光的方程里左边括号的值。

        右边括号是一个定值,OpenGL和虚幻的做法是将值放到一张查找图中(LUT),用的时候根据nv和粗糙度采样。

        但Unity的URP渲染管线里不太一样,请看下面这一段函数:

代码块
JavaScript
自动换行
复制代码
half3 EnvironmentBRDFSpecular(BRDFData brdfData, half fresnelTerm)
{
    float surfaceReduction = 1.0 / (brdfData.roughness2 + 1.0);
    return surfaceReduction * lerp(brdfData.specular, brdfData.grazingTerm, fresnelTerm);
}

half3 EnvironmentBRDF(BRDFData brdfData, half3 indirectDiffuse, half3 indirectSpecular, half fresnelTerm)
{
    half3 c = indirectDiffuse * brdfData.diffuse;
    c += indirectSpecular * EnvironmentBRDFSpecular(brdfData, fresnelTerm);
    return c;
}
复制成功

        现在我们需要知道fresnelTerm和grazingTerm的算法。

代码块
JavaScript
自动换行
复制代码
    outBRDFData.grazingTerm = saturate(smoothness + reflectivity);
复制成功

        reflectivity=1-Kd

        别忘了上面的brdfData.specular其实就是Ks

代码块
JavaScript
自动换行
复制代码
    half NoV = saturate(dot(normalWS, viewDirectionWS));
    half fresnelTerm = Pow4(1.0 - NoV);
复制成功

        源码是抓出来了,但是为什么要这么算我不是很懂,还是LUT好理解,我说实话。

        那么来吧,照着Unity的算一遍。

代码块
JavaScript
自动换行
复制代码
		float surfaceReduction = 1.0 / (roughness2 + 1.0);
		float grazingTerm = saturate(_Smoothness + 1 - Kd);
		float fresnelTerm = Pow(1.0 - nDotV, 4);
		half3 iblBrdf = surfaceReduction * lerp(Ks, grazingTerm, fresnelTerm);
		float3 iblSpecularResult = iblSpecular * iblBrdf;
复制成功

        最后应该能得到如下的结果:

        如果用LUT写的话,原理也是一样的,我们在采样得到了envBRDF之后,就开始重新计算Kd和Ks:

代码块
JavaScript
自动换行
复制代码
float2 envBDRF = tex2D(_LUT, float2(lerp(0, 0.99, nv), lerp(0, 0.99, roughness))).rg; // LUT采样

//添加到的分从这里开始
float3 Flast = fresnelSchlickRoughness(max(nv, 0.0), F0, roughness);
float kdLast = (1 - Flast) * (1 - _Metallic);
//添加的部分到这里结束

float3 iblDiffuseResult = iblDiffuse * kdLast * Albedo;
复制成功

        fresnelSchlickRoughness函数如下:

代码块
JavaScript
自动换行
复制代码
float3 fresnelSchlickRoughness(float cosTheta, float3 F0, float roughness)
{
	return F0 + (max(float3(1.0 - roughness, 1.0 - roughness, 1.0 - roughness), F0) - F0) * pow(1.0 - cosTheta, 5.0);
}
复制成功

        这里的菲涅尔系数计算和前面的有两点不同,第一点是这里没有用于计算微片元朝向的D函数,计算菲涅尔系数使用的是真正的nv而不是vh,第一点是这里的菲尼尔系数计算使用的是粗糙度而不是金属度。

        使用粗糙度而不是金属度其实是一种经验化的做法,方法来自Sébastien Lagarde:

        间接光和直射光的属性相同,因此我们期望较粗糙的表面在边缘上的反射较弱。但之前我们在直接光中计算的菲涅尔系数时完全没有把粗糙度考虑进去,所以表面边缘的反射率总会相对实际值偏高从而带来失真。这一点在粗糙的非金属表面边缘上十分明显,失真效果如下:

        根据新的菲涅尔系数即可算出新的kd,有了这些值就可以进行环境光部分的加和工作,代码如下:

代码块
JavaScript
自动换行
复制代码
float3 iblDiffuseResult = iblDiffuse * kdLast * Albedo;

//添加到的分从这里开始
float3 iblSpecularResult = iblSpecular * (Flast * envBDRF.r + envBDRF.g);
//添加的部分到这里结束

float3 IndirectResult = iblDiffuseResult + iblSpecularResult;
复制成功

        两种方法应该都能得到类似最后的结果。第一种是Unity的方法,第二种则是虚幻的算法。

        完整代码如下:

代码块
JavaScript
自动换行
复制代码
Shader "URP/URPPBR"
{
	Properties
	{
		_MainTex("Texture", 2D) = "white" {}
		_Tint("Tint", Color) = (1 ,1 ,1 ,1)
		[Gamma] _Metallic("Metallic", Range(0, 1)) = 0 //金属度要经过伽马校正
		_Smoothness("Smoothness", Range(0, 1)) = 0.5
		_LUT("LUT", 2D) = "white" {}
	}

	SubShader
	{
		Tags { 
		"RenderType" = "Opaque" 
		"RenderPipeline" = "UniversalPipeline"
		}

		Pass
		{
			Tags {
				"LightMode" = "UniversalForward"
			}

			HLSLPROGRAM
			#pragma target 3.0
			#pragma vertex vert
			#pragma fragment frag
			
			#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
			#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"

			#define HALF_MIN_SQRT 0.0078125
			#define HALF_MIN 6.103515625e-5
			#define PI 3.1415926

			CBUFFER_START(UnityPerMaterial)
				float4 _Tint;
				float _Metallic;
				float _Smoothness;
				float4 _MainTex_ST;
			CBUFFER_END

			TEXTURE2D(_MainTex);
			SAMPLER(sampler_MainTex);
			TEXTURE2D(_LUT);
			SAMPLER(sampler_LUT);

			struct a2v
			{
				float4 positionOS : POSITION;
				float3 normalOS : NORMAL;
				float2 uv : TEXCOORD0;
			};

			struct v2f
			{
				float4 positionCS : SV_POSITION;
				float2 uv : TEXCOORD0;
				float3 normalWS : TEXCOORD1;
				float3 positionWS : TEXCOORD2;
			};


			v2f vert(a2v v)
			{
				v2f o;
				o.positionCS = TransformObjectToHClip(v.positionOS);
				o.positionWS = TransformObjectToWorld(v.positionOS);
				o.uv = TRANSFORM_TEX(v.uv, _MainTex);
				o.normalWS = TransformObjectToWorldNormal(v.normalOS);
				o.normalWS = normalize(o.normalWS);
				return o;
			}

			half4 frag(v2f i) : SV_Target
			{
				Light mainLight = GetMainLight();
				float3 normalWS = normalize(i.normalWS);
				float3 lightDirWS = mainLight.direction;
				float3 viewDirWS = normalize(_WorldSpaceCameraPos.xyz-i.positionWS.xyz);
				float3 halfVector = normalize(lightDirWS+viewDirWS);

				float nDotL = max(0, dot(normalWS, lightDirWS));
				float nDotV = max(0, dot(normalWS, viewDirWS));
				float vDotH = max(0, dot(halfVector, viewDirWS));
				float lDotH = max(0, dot(halfVector, lightDirWS));
				float lDotH2 = max(0.1f, pow(lDotH,2));
				float nDotH = max(0, dot(normalWS, halfVector));

				float3 albedo = _Tint * SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, i.uv);
				float Kd = kDieletricSpec.a - _Metallic * kDieletricSpec.a;

				float perceptualRoughness = 1 - _Smoothness;
				float roughness = max(perceptualRoughness * perceptualRoughness, HALF_MIN_SQRT);
				float roughness2 = max(roughness*roughness, HALF_MIN);

				float divisorD = pow(nDotH*nDotH*(roughness2-1)+1.00001f,2);
				float D = roughness2/divisorD;
				float V = 1/(lDotH2*(roughness+0.5));
				float Fspec = D*V;
				float Ks = lerp(kDieletricSpec.rgb, albedo, _Metallic);

				float3 diffColor = Kd * albedo * mainLight.color * nDotL;
				float3 specColor = Ks * albedo * Fspec * mainLight.color * nDotL;
				float3 DirectLightResult = diffColor + specColor;

				half3 ambient_GI = SampleSH(normalWS);
				float3 iblDiffuseResult = ambient_GI * Kd * albedo;

				float mip_roughness = perceptualRoughness * (1.7 - 0.7 * perceptualRoughness);
				float3 refDirWS = reflect(-viewDirWS, normalWS);

				half mip = mip_roughness * UNITY_SPECCUBE_LOD_STEPS;
				half4 encodedIrradiance = SAMPLE_TEXTURECUBE_LOD(unity_SpecCube0, samplerunity_SpecCube0, refDirWS, mip); 

				float3 iblSpecular = albedo * DecodeHDREnvironment(encodedIrradiance, unity_SpecCube0_HDR);

				float surfaceReduction = 1.0 / (roughness2 + 1.0);
				float grazingTerm = saturate(_Smoothness + 1 - Kd);
				float fresnelTerm = pow(1.0 - nDotV, 4);
				half3 iblBrdf = surfaceReduction * lerp(Ks, grazingTerm, fresnelTerm);

				float3 iblSpecularResult = iblSpecular * iblBrdf;
				float3 IndirectResult = iblDiffuseResult + iblSpecularResult;

				float4 result = float4(DirectLightResult + IndirectResult, 1);

				return result;
			}
			ENDHLSL
		}
	}
}
复制成功

  • 结语

        关于PBR,这只是很少的一部分,Unity还有N多个分支,N多个不同的算法都等待着去探索。

        URP也新增了ClearCoatPBR,ShadowMask,SSAO,额外光照等功能,这里我都还没写,以后有需求了再慢慢补吧,URP的学习先暂停一下了。

        接下来开启特效篇章,扬帆起航!