【项目记录】基于Unity的BRDF实现

项目背景

  这个项目应该是在去年(2022)暑假接近开学的时候做的,也是我正式开始入门图形学的时间,当时只对图形学渲染管线流程有基本的了解,Shader的编写也没有什么经验,更不清楚什么是BRDF。因为项目急着出进度,从只是实现项目需求来看,编写Shader和实现BRDF模型都挺简单。但在对基本知识缺少了解的情况下,确实出现了一些问题,比如在一开始实现菲涅尔项时给错了夹角,但因为在该项目中菲涅尔效应并不明显,甚至很长时间都没有发现…

  原项目中有相当一部分与BRDF无关的开发需求,本篇文章主要记录了BRDF部分的实现情况,基本都是一些基础内容。如果时间来得及,后续应该会有一篇文章专门讲一讲PBR材质的原理部分,也是去年后续学习的部分。

BRDF简单介绍

项目中实现的四种BRDF模型对比:Blinn-Phong、Cook-Torrance、GGX、Ward

  物体的渲染的颜色受自身的材质属性和光照影响,渲染方程可以用来描述如何对一个点进行着色,如果考虑从 ω o \omega_{o} ωo方向看向 p p p点,对 p p p点进行着色:
L o ( p , ω o ) = L e ( p , ω o ) + ∫ Ω   f r ( p , ω i , ω o ) L i ( p , ω i ) n ⋅ ω i d ω i L_{o}\left( {p,\omega_{o}} \right) = L_{e}\left( {p,\omega_{o}} \right) + \int_{\Omega}^{~}{f_{r}\left( {p,\omega_{i},\omega_{o}} \right)L_{i}\left( {p,\omega_{i}} \right)n \cdot \omega_{i}\mathbb{d}\omega_{i}} Lo(p,ωo)=Le(p,ωo)+Ω fr(p,ωi,ωo)Li(p,ωi)nωidωi

则该点的颜色分两部分:自发光 L e L_{e} Le和来自其他所有方向的光 L i L_{i} Li分配到 ω o \omega_{o} ωo的方向。公式中的 f r f_{r} fr便是描述了 p p p点在 ω i \omega_{i} ωi方向接收到的能量有多少分配到了 ω o \omega_{o} ωo方向上的函数BxDF,包括:

  • BRDF,双向反射分布函数,名字抽象但却很形象,也是本篇实现的内容
  • BTDF,双向透射分布函数
  • BSDF,双向散射分布函数,即BRDF+BTDF

  忽略自发光,在不考虑间接光照的情况下,其他方向的光便是来自光源的光(平行光、点光源等简单光照模型),即最终结果便是计算每个能够影响到该点的光源对该点的光照贡献(通常会加入环境光照弥补间接光照的缺失,以避免暗处完全为黑色)。

BRDF实现

  在Unity中创建一个Unlit shader,代码基本框架就已经有了,然后就是添加一些需要的参数,并在ps里按照公式实现对应的BRDF模型(注意不要在vs里实现,除开vs受场景顶点数影响的性能区别,在vs里计算光照结果线性插值后最终结果并不准确)。原项目中实现了Blinn-Phong、Neumann-Phong、Cook-Torrance、GGX、Ward五种BRDF模型,这里仅以近些年被采用较多的GGX模型为例介绍其实现,GGX分为漫反射项和镜面反射项,公式如下:
f r = f d i f f u s e + f s p e c u l a r = k d i f f π + k s p e c F ( i , h ) G ( i , o , h ) D ( h ) 4 ∣ i ⋅ n ∣ | o ⋅ n ∣ f_{r} = f_{diffuse} + f_{specular} = \frac{k_{diff}}{\pi} + k_{spec}\frac{F\left( { {\mathbf{i}},{\mathbf{h}}} \right)G\left( { {\mathbf{i}},{\mathbf{o}},{\mathbf{h}}} \right)D({\mathbf{h}})}{\left. 4\left| { {\mathbf{i}} \cdot {\mathbf{n}}} \right| \middle| {\mathbf{o}} \cdot {\mathbf{n}} \right|} fr=fdiffuse+fspecular=πkdiff+kspec4inonF(i,h)G(i,o,h)D(h)

其中 n \mathbf{n} n为法线, i \mathbf{i} i为入射方向(在只考虑直接光照时可以简单理解为光源方向 L \mathbf{L} L), o \mathbf{o} o为出射方向(在只考虑直接光照时可以简单理解为视角方向 V \mathbf{V} V),均为沿着物体表面向外的单位向量 h \mathbf{h} h i \mathbf{i} i o \mathbf{o} o的半角向量,即单位化 ( i + o ) (\mathbf{i}+\mathbf{o}) (i+o)

   F F F为菲涅尔项,通常采用其近似解,这里 m \mathbf{m} m可以认为与 h \mathbf{h} h含义相同,具体原因后续PBR材质分析的文章会谈到:
F S c h l i c k ( i , m ) = F 0 + ( 1 − F 0 ) ( 1 − ( i ⋅ m ) ) 5 F_{Schlick}\left( { {\mathbf{i}},{\mathbf{m}}} \right) = F_{0} + \left( 1 - F_{0} \right)\left( 1 - ({\mathbf{i}} \cdot {\mathbf{m}}) \right)^{5} FSchlick(i,m)=F0+(1F0)(1(im))5

法线分布函数 D D D和阴影遮蔽函数 G G G公式则分别如下:
D ( m ) = α 2 π c o s 4 θ ( α 2 + t a n 2 θ ) 2 , c o s θ = ( n ⋅ m ) D({\mathbf{m}}) = \frac{\alpha^{2}}{\pi{cos}^{4}\theta\left( \alpha^{2} + {tan}^{2}\theta \right)^{2}},cos\theta = ({\mathbf{n}} \cdot {\mathbf{m}}) D(m)=πcos4θ(α2+tan2θ)2α2,cosθ=(nm)

G ( i , o , m ) = 4 ( 1 + 1 + α 2 t a n 2 θ 1 )   ( 1 + 1 + α 2 t a n 2 θ 2 ) , c o s θ 1 = ( i ⋅ n ) , c o s θ 2 = ( o ⋅ n ) G\left( { {\mathbf{i}},{\mathbf{o}},{\mathbf{m}}} \right) = \frac{4}{\left( 1 + \sqrt{1 + \alpha^{2}{tan}^{2}\theta_{1}} \right)~\left( 1 + \sqrt{1 + \alpha^{2}{tan}^{2}\theta_{2}} \right)},cos\theta_{1} = ({\mathbf{i}} \cdot {\mathbf{n}}),cos\theta_{2} = ({\mathbf{o}} \cdot {\mathbf{n}}) G(i,o,m)=(1+1+α2tan2θ1 ) (1+1+α2tan2θ2 )4,cosθ1=(in),cosθ2=(on)
这里也可以看到我们有四个参数 k d i f f k_{diff} kdiff k s p e c k_{spec} kspec F 0 F_{0} F0 α \alpha α用来控制材质的具体表现。 k d i f f k_{diff} kdiff k s p e c k_{spec} kspec用于控制漫反射强度、高光反射强度, F 0 F_{0} F0用于控制菲涅尔效应,在金属与非金属之间区别明显, α \alpha α则描述了物体的粗糙程度。

  附上实现代码,删除了多余的内容,相对来说比较直观易于理解。注意这里只考虑了主光源的光照、 F 0 F_{0} F0为单通道,同时如果需要投射阴影和接收阴影也需要额外添加代码进行处理(前段时间的另一个项目中也在URP管线中实现了GGX模型,在那篇文章中我也会附上相应的实现代码):

Shader "BRDF/GGX"
{
    
    
    Properties
    {
    
    
        _Kdiff ("Kdiff", float) = 0.1
		_Kspec ("Kspec", float) = 0.4
		_Alpha ("Alpha", float) = 0.05
		_F0 ("F0", float) = 0.8
    }
    SubShader
    {
    
    
        Tags {
    
     "RenderType"="Opaque" }
        LOD 100

        Pass
        {
    
    
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "Lighting.cginc"

			#define PI 3.1415926535

            struct appdata
            {
    
    
                float4 vertex : POSITION;
                float3 normal : NORMAL;
            };

            struct v2f
            {
    
    
                float4 vertex : SV_POSITION;
				float3 posW : TEXCOORD0;
				float3 normW : TEXCOORD1;
            };

            float _Kdiff;
			float _Kspec;
			float _Alpha;
			float _F0;

            v2f vert (appdata v)
            {
    
    
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
				o.posW = mul(unity_ObjectToWorld, v.vertex);
				o.normW = mul(v.normal, (float3x3)unity_WorldToObject);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
    
    
				float3 V = normalize(_WorldSpaceCameraPos - i.posW);
				float3 L = normalize(_WorldSpaceLightPos0.xyz);
				float3 H = normalize(L + V);
				float3 N = normalize(i.normW);

				float NdotH = dot(N, H);
				float NdotL = dot(N, L);
				float NdotV = dot(N, V);
				float HdotV = dot(H, V);

				float alpha2 = _Alpha * _Alpha;
				float tanNL2 = 1 / (NdotL * NdotL + 1e-8) - 1;
				float tanNV2 = 1 / (NdotV * NdotV + 1e-8) - 1;

				float F = _F0 + (1 - _F0) * pow(1 - HdotV, 5);
				float D = alpha2 / (PI * pow(NdotH * NdotH * (alpha2 - 1) + 1, 2));
				float G = 4 / ((1 + sqrt(1 + alpha2 * tanNL2)) * (1 + sqrt(1 + alpha2 * tanNV2)));
				float fr = _Kdiff / PI +
						   _Kspec * F * D * G / (4 * NdotL * NdotV);

                return fixed4(max(0, fr) * _LightColor0.rgb * max(0, NdotL), 1.0f);
            }
            ENDCG
        }
    }
}

色彩空间Color Space

  如果想得到相对正确、真实的结果,Unity的Color Space(Project Settings->Player->Other Settings)应当设置为Linear,这与屏幕亮度的非线性输出有关。

  如果设置为Gamma,Unity不会对sRGB的伽马矫正进行移除,也不会在最后输出结果时进行伽马矫正,这会导致最终结果出现一些偏差,但会降低开销,因此Unity默认选择了Gamma Space(似乎新版改为了默认使用Linear Space?),毕竟很多时候图形学都是看起来没问题那就没问题,有时候过程讲究得太多,最后吃的都是性能。

HDR及Tone mapping

  因为项目中需要观察高光表现,因此开启了HDR并添加了Tone mapping保留更多高光细节。

  与HDR(High Dynamic Range,高动态范围)对应的是LDR(Low Dynamic Range,低动态范围),简单来说对于LDR颜色大于1的部分都被限制为1,而HDR的颜色则能够超过1(如IBL就是采用HDR实现)。但显示器的显示范围还是0到1的,因此为了显示超过1的部分就需要色调映射Tone mapping,把高亮度映射至低亮度,并保留更多的细节。

  实现是直接使用的网上找来的ACES Tone mapping代码

	float3 ACES(float3 color)
	{
    
    
		const float a = 2.51f;
		const float b = 0.03f;
		const float c = 2.43f;
		const float d = 0.59f;
		const float e = 0.14f;
	
		return (color * (a * color + b)) / (color * (c * color + d) + e);
	}

(理论上原本应该属于后处理过程,并可以添加手动或根据渲染画面动态调整曝光值,但项目不需要,就被我直接写在BRDF shader里了)

猜你喜欢

转载自blog.csdn.net/qq_43459138/article/details/129238890