URP管线的自学HLSL之路 第三十七篇 造一个PBR的轮子
雪风carel
2020年09月06日 16:59
收录于文集
共38篇

官方LITshader和我的轮子近乎一样

        作为一个Bridge重度用户,我在使用hdrp管线完全离不开它了,自带的上万个扫描级材质贴图,大大降低了SP/SD里绘制制作周期,当然它也可以当作SD/SP的素材进行二次编辑。随着我想在URP管线里充分使用Bridge的资源,URP自带的LIT无法充分利用Mask图(它会额外多一次AO的采样的开销),这对于性能来说不太划算,我更希望的是像hdrp那样直接读取;还有个原因是我希望在后续能自由拓展LITshader的功能,目前URP不支持表面着色器虽然Colin神已经开源了一个URP的表面着色器(https://github.com/ColinLeung-NiloCat/UnityURP-SurfaceShaderSolution)有兴趣的可以去看看,但是表面着色器又和第一个原因冲突了。至此,打算自己造一个能直接使用Bridge的PBR的轮子,甚至后续支持bridge的基于PBR的NPRshader开发。

URP有多一次AO的采样

本次轮子的效果如下:

1.本PBR轮子效果近乎和LITshader的效果一直,自认为高达90%的相似度,下图读者可以区分出哪个是手写的,哪个是官方的么?

2.本PBR轮子尽量不增加新的功能,只保证基础效果一致,其他的pass比如depthonly的pass,shadowcaster的pass或者对lightmap的采样就交给读者去自己实现,方便读者自我拓展功能,或者把它作为URP的shader源码的学习资料。

3.只有3张贴图,一张纹理,一张mask,一张法线。mask图遵守HDRP的规则(即Bridge的导出规则),R通道为金属,G通道为AO,A为光滑度。

注意!本篇文章只注重实践,不会涉及PBR的各个公式的原理推导,想要了解的网上一大堆文章和论文或者书籍;PBR的理论部分也不会在本文里讲到,入门精要的理论部分就讲得很好。

核心方程如下,它分为2大部分,漫反射部分和高光部分。

在unity里对直接光照(直射光,点光源,聚光灯)和间接光照(反射探针灯光探针)的处理也不太一样:直接光照的计算和上式基本一致;间接光照则利用了各种trick,各种预计算和近似拟合来达到我们想要的效果。

直射光照部分:

1.高光

1.1法线分布函数GGX(Normal Distribution Function),即核心方程的D项,它是描述微观法线N和半角向量H的趋同性比重。

我们在unity里定义该函数,非常简单,照着公式写即可。

它的效果如下,随着粗糙度的变化而变化。

我们把粗糙度换成贴图后,它的效果如下。

1.2 几何函数G,(Geometry function),即核心方程的G项,它是描述入射射线(即光照方向)和出射射线(视线方向)被自己的微观几何形状遮挡的比重。

G项是由2个几乎一样的子项相乘得来,故我们可以先定义子项的函数。

再去定义真正的G项,把NL和NV代入子项,相乘即可。

注意这里的K系数,在直射光和间接光的计算方式略有不同,代码里是直接光的,而间接光的K=pow(roughness,2)/2,这里要注意。

随着粗糙度的变化

而我们把粗糙度换成贴图后,效果如下。

1.3 菲涅尔函数F(Fresnel equation),即核心方程的F项,如下图所示。

但是由于我们需要的法线方向并不是模型本身的宏观法线n,而是经过D项筛选通过的微观法线H,故需把N改成H。

后来unity对它进行了优化(https://link.zhihu.com/?target=http%3A//filmicworlds.com/blog/optimizing-ggx-shaders-with-dotlh/),视线方向V换成了L,如下所示,这是我们所使用的函数。

其中的5次方计算量比较大,把它变成自然对数函数进行计算可以节省计算量,后续文章里所有的5次方计算都可以换算成对数计算。

故最终,我们的菲涅尔函数如下。

我们把它输出看看,效果和平时简单的菲涅尔不太一样,但是它是正确的。

1.4直接光的高光部分:我们把D,G,F代入公式,先计算出高光部分。注意这里经过半球积分后会乘PI,不要丢了;注意这里并未再次乘KS,因为KS=F,已经计算过了一次不需要重复计算。

直接光_高光部分

2.漫反射

漫反射就超级简单了,本质就是NdotL,计算出KD。这里要注意由于分母带了PI,而半球积分后会乘PI,两者就约掉了这就没有写。

我们把漫反射和高光反射加起来,也就是整个直接光的结果输出,效果如下,至此,直接光完毕。

间接光照部分:

1.漫反射和球谐函数

        间接光照的漫反射本质是对光照探针进行采样,这里有篇文章讲得很好(网页链接​)

这部分我是抄的urp的Lighting.hlsl的源码,代码如下。

我们把它输出,它是取的最近的4个光照探针的值得到的结果。

得到颜色后,再通过计算间接光照的菲涅尔函数得到KD,其计算如下图。

2.高光反射和IBL

间接光照的高光反射本质是对于反射探针(360全景相机)拍的一张图进行采样,把采样到的颜色当成光照去进行计算,这种光照称为基于图像的光照IBL(Image-Based Lighting),前面的漫反射也是IBL,关于IBL有非常复杂的理论,本人才疏学浅就不展开了。

        这张全景相机拍的图是cube类型,我们先计算反射射线的方向对cube进行采样。我们还需要根据不同的粗糙度进行采样不同的模糊度,粗糙度需要修改一下曲线去拟合,它并非线性;模糊度从0-6总共7个模糊等级,算法我是参考URP的源码imageBasedLighting.hlsl和Lighting.hlsl改改写写的。

我们把得到的反射探针的图像输出,效果如下。

我们还需要计算一个间接高光的影响因子,一般来说是按照一张现成的LUT图像进行采样,采样UV的U方向为NdotV,V方向为粗糙度来得到我们需要的影响因子。

LUT,它的采样过程耗费性能不在考虑

但是采样贴图的过程非常耗,unity使用的曲线拟合去得到结果,其计算函数如下。计算过程也是白嫖的Lighting.hlsl的写法。

我们把间接光照和factor相乘输出,环境高光的强度就很弱了。

最后,把全部间接光输出,效果如下。

全部间接光

3.整合

最终把全部直接光和间接光加起来,输出,就是我们需要的最终结果。从Bridge里拉一个模型出来,丢上贴图,丢上一个官方的lit shader和自己写的pbr shader,对比图如下,近乎一模一样的效果(当然自己写的多了个AO有一些小的差异)。

喜欢卡渲的大佬欢迎入驻群聊950138189。

Git工程地址:https://github.com/NiceTimeJob/PBR_URP

参考文章如下:

https://zhuanlan.zhihu.com/p/68025039

https://zhuanlan.zhihu.com/p/33464301

函数库源码

#ifndef PBR_Function_INCLUDE

#define PBR_Function_INCLUDE

         //D项 法线微表面分布函数 

         float D_Function(float NdotH,float roughness)

         {

             float a2=roughness*roughness;

             float NdotH2=NdotH*NdotH;

             

             //直接根据公式来

             float nom=a2;//分子

             float denom=NdotH2*(a2-1)+1;//分母

             denom=denom*denom*PI;

             return nom/denom;

         }

         //G项子项

         float G_section(float dot,float k)

         {

             float nom=dot;

             float denom=lerp(dot,1,k);

             return nom/denom;

         }

         //G项

         float G_Function(float NdotL,float NdotV,float roughness)

         {

             float k=pow(1+roughness,2)/8;

             float Gnl=G_section(NdotL,k);

             float Gnv=G_section(NdotV,k);

             return Gnl*Gnv;

         }

         //F项 直接光

         real3 F_Function(float HdotL,float3 F0)

         {

             float Fre=exp2((-5.55473*HdotL-6.98316)*HdotL);

             return lerp(Fre,1,F0);

         }

         //F项 间接光

         real3 IndirF_Function(float NdotV,float3 F0,float roughness)

         {

             float Fre=exp2((-5.55473*NdotV-6.98316)*NdotV);

             return F0+Fre*saturate(1-roughness-F0);

         }

         //间接光漫反射 球谐函数 光照探针

         real3 SH_IndirectionDiff(float3 normalWS)

         {

             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;

             float3 Color=SampleSH9(SHCoefficients,normalWS);

             return max(0,Color);

         }

         //间接光高光 反射探针

         real3 IndirSpeCube(float3 normalWS,float3 viewWS,float roughness,float AO)

         {

             float3 reflectDirWS=reflect(-viewWS,normalWS);

             roughness=roughness*(1.7-0.7*roughness);//Unity内部不是线性 调整下拟合曲线求近似

             float MidLevel=roughness*6;//把粗糙度remap到0-6 7个阶级 然后进行lod采样

             float4 speColor=SAMPLE_TEXTURECUBE_LOD(unity_SpecCube0, samplerunity_SpecCube0,reflectDirWS,MidLevel);//根据不同的等级进行采样

             #if !defined(UNITY_USE_NATIVE_HDR)

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

             #else

             return speColor.xyz*AO;

             #endif

         }

         //间接高光 曲线拟合 放弃LUT采样而使用曲线拟合

         real3 IndirSpeFactor(float roughness,float smoothness,float3 BRDFspe,float3 F0,float NdotV)

         {

             #ifdef UNITY_COLORSPACE_GAMMA

             float SurReduction=1-0.28*roughness,roughness;

             #else

             float SurReduction=1/(roughness*roughness+1);

             #endif

             #if defined(SHADER_API_GLES)//Lighting.hlsl 261行

             float Reflectivity=BRDFspe.x;

             #else

             float Reflectivity=max(max(BRDFspe.x,BRDFspe.y),BRDFspe.z);

             #endif

             half GrazingTSection=saturate(Reflectivity+smoothness);

            float Fre=Pow4(1-NdotV);//lighting.hlsl第501行 

             //float Fre=exp2((-5.55473*NdotV-6.98316)*NdotV);//lighting.hlsl第501行 它是4次方 我是5次方 

             return lerp(F0,GrazingTSection,Fre)*SurReduction;

         }

#endif

Shader源码

Shader "WX/URP/PBR"

{

    Properties

    {

    _BaseColor("BaseColor",Color)=(1,1,1,1)

        _BaseMap("MainTex",2D)="white"{}

        [NoScaleOffset]_MaskMap("MaskMap",2D)="white"{}

        [NoScaleOffset][Normal]_NormalMap("NormalMap",2D)="Bump"{}

        _NormalScale("NormalScale",Range(0,1))=1

    }

    SubShader

    {

        Tags{

        "RenderPipeline"="UniversalRenderPipeline"

        }

        HLSLINCLUDE

        #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"

        #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"

        #include "PbrFunction.hlsl"

  

        CBUFFER_START(UnityPerMaterial)

        float4 _BaseMap_ST;

        real4 _BaseColor;

        float _NormalScale;

        CBUFFER_END

        TEXTURE2D(_BaseMap);    SAMPLER(sampler_BaseMap);

        TEXTURE2D(_MaskMap);    SAMPLER(sampler_MaskMap);

        TEXTURE2D(_NormalMap);  SAMPLER(sampler_NormalMap);

         struct a2v

         {

             float4 positionOS:POSITION;

             float4 normalOS:NORMAL;

             float2 texcoord:TEXCOORD;

             float4 tangentOS:TANGENT;

             float2 lightmapUV:TEXCOORD2;//一般来说是2uv是lightmap 这里取3uv比较保险

         };

         struct v2f

         {

             float4 positionCS:SV_POSITION;

             float4 texcoord:TEXCOORD;

             float4 normalWS:NORMAL;

             float4 tangentWS:TANGENT;

             float4 BtangentWS:TEXCOORD1;

         };

        ENDHLSL

        pass

        {

        Tags{

         "LightMode"="UniversalForward"

         "RenderType"="Opaque"

            }

            HLSLPROGRAM

            #pragma vertex VERT

            #pragma fragment FRAG

            v2f VERT(a2v i)

            {

                v2f o;

                o.positionCS=TransformObjectToHClip(i.positionOS.xyz);

                o.texcoord.xy=TRANSFORM_TEX(i.texcoord,_BaseMap);

                o.texcoord.zw=i.lightmapUV;

                o.normalWS.xyz=normalize(TransformObjectToWorldNormal(i.normalOS.xyz));

                o.tangentWS.xyz=normalize(TransformObjectToWorldDir(i.tangentOS.xyz));

                o.BtangentWS.xyz=cross(o.normalWS.xyz,o.tangentWS.xyz)*i.tangentOS.w*unity_WorldTransformParams.w;

                float3 posWS=TransformObjectToWorld(i.positionOS.xyz);

                o.normalWS.w=posWS.x;

                o.tangentWS.w=posWS.y;

                o.BtangentWS.w=posWS.z;

                return o;

            }

            real4 FRAG(v2f i):SV_TARGET

            {                

                //法线部分得到世界空间法线

                float4 nortex=SAMPLE_TEXTURE2D(_NormalMap,sampler_NormalMap,i.texcoord.xy);

                float3 norTS=UnpackNormalScale(nortex,_NormalScale);

                norTS.z=sqrt(1-saturate(dot(norTS.xy,norTS.xy)));

                float3x3 T2W={i.tangentWS.xyz,i.BtangentWS.xyz,i.normalWS.xyz};

                T2W=transpose(T2W);

                float3 N=NormalizeNormalPerPixel(mul(T2W,norTS));

                //return float4(N,1);

                //计算一些可能会用到的杂七杂八的东西

                float3 positionWS=float3(i.normalWS.w,i.tangentWS.w,i.BtangentWS.w);

                real3 Albedo=SAMPLE_TEXTURE2D(_BaseMap,sampler_BaseMap,i.texcoord.xy).xyz*_BaseColor.xyz;

                float4 Mask=SAMPLE_TEXTURE2D(_MaskMap,sampler_MaskMap,i.texcoord.xy);

                float Metallic=Mask.r;

                float AO=Mask.g;

                float smoothness=Mask.a;

                float TEMProughness=1-smoothness;//中间粗糙度

                float roughness=pow(TEMProughness,2);// 粗糙度

                float3 F0=lerp(0.04,Albedo,Metallic);

                Light mainLight=GetMainLight();

                float3 L=normalize(mainLight.direction);

                float3 V=SafeNormalize(_WorldSpaceCameraPos-positionWS);

                float3 H=normalize(V+L);

                float NdotV=max(saturate(dot(N,V)),0.000001);//不取0 避免除以0的计算错误

                float NdotL=max(saturate(dot(N,L)),0.000001);

                float HdotV=max(saturate(dot(H,V)),0.000001);

                float NdotH=max(saturate(dot(H,N)),0.000001);

                float LdotH=max(saturate(dot(H,L)),0.000001);

    //直接光部分

                

                float D=D_Function(NdotH,roughness);

                //return D;

                float G=G_Function(NdotL,NdotV,roughness);

                //return G;

                float3 F=F_Function(LdotH,F0);

                //return float4(F,1);

                float3 BRDFSpeSection=D*G*F/(4*NdotL*NdotV);

                float3 DirectSpeColor=BRDFSpeSection*mainLight.color*NdotL*PI;

                //return float4(DirectSpeColor,1);

                //高光部分完成 后面是漫反射

                float3 KS=F;

                float3 KD=(1-KS)*(1-Metallic);

                float3 DirectDiffColor=KD*Albedo*mainLight.color*NdotL;//分母要除PI 但是积分后乘PI 就没写

                //return float4(DirectDiffColor,1);

                float3 DirectColor=DirectSpeColor+DirectDiffColor;

                //return float4(DirectColor,1);

    //间接光部分

                float3 SHcolor=SH_IndirectionDiff(N)*AO;

                float3 IndirKS=IndirF_Function(NdotV,F0,roughness);

                float3 IndirKD=(1-IndirKS)*(1-Metallic);

                float3 IndirDiffColor=SHcolor*IndirKD*Albedo;

                //return float4(IndirDiffColor,1);

                //漫反射部分完成 后面是高光

                float3 IndirSpeCubeColor=IndirSpeCube(N,V,roughness,AO);

                //return float4(IndirSpeCubeColor,1);

                float3 IndirSpeCubeFactor=IndirSpeFactor(roughness,smoothness,BRDFSpeSection,F0,NdotV);

                float3 IndirSpeColor=IndirSpeCubeColor*IndirSpeCubeFactor;

                //return float4(IndirSpeColor,1);

                float3 IndirColor=IndirSpeColor+IndirDiffColor;

                //return float4(IndirColor,1);

                //间接光部分计算完成

                float3 Color=IndirColor+DirectColor;

                return float4(Color,1);

            }

            ENDHLSL

        }

    }

        

}