Shader 学习笔记:杂记

这篇博客算是个杂记,写一写关于一些自己老是忘记但有很重要的关于Shader的知识。自己总是感觉基础不牢固,写过的很多东西不求甚解,并且有时候一些语法很难和一些知识点联系起来,一些知识表面上一马平川,实际上都是孤魂野鬼,这样囫囵吞枣迟早要遭重。所以这篇文章我记一下平常很基础重要但是看了之后缺乏归纳的知识点。


杂记1:渲染流水线

关于渲染流水线,我画了这张图

我们在上篇描述深度的博客中就已经讲过从顶点着色器获得模型空间的顶点坐标以后对它做了什么了,这篇博客主要写写光栅化的一些具体的内容:

三角形设置——>三角形遍历:

我们最终映射到屏幕上的是一个个顶点,每三个顶点组成一个面片,三角形设置主要是为了得到每个三角形的边界。

然后三角形遍历阶段会检查屏幕上每一个像素是否被一个三角形网格所覆盖,如果被覆盖则生成一个片元。每个片元中的状态由三个顶点进行插值而获得(因为一个三角形内所有的片元一定在处于同一个平面上)。

片元并不是一个真正意义上的像素,每个片元内保存了很多信息,例如通过插值得到的片元深度信息、屏幕坐标、顶点信息、法线信息等等。以便于在片元着色器中计算需要被输出的颜色。

片元着色器——>逐片元操作:

逐片元操作主要就是对一个片元进行测试,然后再与当前的屏幕缓冲区进行混合。基本的测试有透明度测试,深度测试和模板测试三种。我们这里主要讲模板测试和深度测试。

对于一个即将显示的像素,对它来说,一般有如下缓存区:

  • 颜色缓存ColorBuffer:储存该点即将显示的颜色RGBA。
  • 深度缓存ZBuffer:储存该点的NDC的Z分量,即为深度。
  • 模板缓存StencilBuffer:为每个像素存储一个无符号的整数值。通常用作限制渲染区域的作用。
  • 积累缓存AccumulationBuffer:存储一个RGBA值,这个值与之前帧中的颜色值有关,当我们一个像素各种测试通过后,将会让颜色缓冲区与积累缓冲区反复混合,用于处理模糊或者抗锯齿、景深等效果。

模板Stencil

所谓的模板指的是在屏幕上每一个像素对应的一个整数值(范围为0~255,即8位二进制数)。而模板测试与模板缓冲即为对不同叠加的像素之间的一些处理工作。

当一个像素通过之前的三角形遍历和片元着色器后来到了逐片元操作时。如果开启了模板测试,GPU首先会从改像素的模板缓冲中读取需要进行测试的像素的模板值(renferenceValue),然后让该值与当前像素对应位置的屏幕像素的模板缓冲区中的模板值(stencilBufferValue)进行比较,再通过比较的结果决定是否更新当前对应位置的像素值。这句话转换为代码即为:

if(referenceValue & readMask  comparisonOperation  stencilBufferValue & readMask)
{    通过测试,使用WriteMask进行掩码操作    }
else
{    不通过测试,抛弃像素    }

这个表达式看起来比较复杂,我们把它从中间的比较操作comparisonOperation:比较方式来拆开看分为左右两个部分:

  • referenceValue:参考值。即为当前需要被比较的像素的模板值,这个值可以由我们自己定义。
  • readMasK:读取掩码,它与参考值进行按位与&操作,它的默认值为255(二进制即为11111111),那么与参考值按位与后的结果即为参考值本身。
  • stencilBufferValue:像素对应位置模板缓冲区的值。同样需要与掩码进行按位与操作。
  • comparisonFunction:比较方法,这个同样也可以手动定义,两边的结果通过它进行判断然后确定是否通过测试。
  • WriteMask:写入掩码,和读取掩码类似,默认值为255。

在代码中,在SubShader或者Pass里对应如上的公式有如下的定义方法:

            Stencil
            {
                Ref referenceValue
                Comp compareOperation
                ReadMask readMaskValue
                WriteMask writeMaskValue
                pass stencilOperation
                Fail stencilOperation
                ZFail stencilOperation
            }

这些命令实际上来说就是上文中的测试的一种写法,实际上这些对应的值为:

  • Ref:定义当前像素的模板参考值,即referenceValue。将用它来与模板缓冲区中的值进行比较。
  • Comp:定义比较操作符,默认值为Always(即不用管两边和值,模板测试直接通过)。
  • ReadMask:读取掩码,将与参考值referenceValue和模板缓冲区对应值stencilBufferValue进行按位与。
  • WriteMask:写入掩码,当一个像素通过了模板测试后,将用写入掩码与之进行按位与操作。最终输出成为新一个stencilBufferValue。
  • Pass:定义当像素模板测试和深度测试都通过时,根据stencilOperation值进对模板缓冲值stencilBufferValue进行处理。默认值为Keep。(注意这里必须是二者都通过)
  • Fail:定义当像素模板测试没通过时,根据stencilOperation值对模板缓冲值stencilBufferValue进行处理。默认值为Keep。
  • ZFail:定义当像素模板测试通过而深度测试失败时,根据stencilOperation值对stencilBufferValue进行处理,默认值为Keep。

对于comparisonOperation的“枚举”值,通过Comp指令定义,有如下的值:

Greater

相当于“>”操作,即仅当左边>右边,模板测试通过

GEqual

相当于“>=”操作,即仅当左边>=右边,模板测试通过

Less

相当于“<”操作,即仅当左边<右边,模板测试通过

LEqual

相当于“<=”操作,即仅当左边<=右边,模板测试通过

Equal

相当于“=”操作,即仅当左边=右边,模板测试通过

NotEqual

相当于“!=”操作,即仅当左边!=右边,模板测试通过

Always 不管公式两边为何值,模板测试总是通过
Never 不敢公式两边为何值,模板测试总是失败 ,像素被抛弃

并且,对于如果通过以后或者不通过的后的处理Pass、Fail、ZFail也有如下的值可以选择:

Keep 保留当前缓冲中的内容,即stencilBufferValue不变。
Zero 将0写入缓冲,即stencilBufferValue值变为0。
Replace 将参考值写入缓冲,即将referenceValue赋值给stencilBufferValue。
IncrSat stencilBufferValue加1,如果stencilBufferValue超过255了,那么保留为255,即不大于255。
DecrSat stencilBufferValue减1,如果stencilBufferValue超过为0,那么保留为0,即不小于0。
Invert 将当前模板缓冲值(stencilBufferValue)按位取反
IncrWrap 当前缓冲的值加1,如果缓冲值超过255了,那么变成0,(然后继续自增)。
DecrWrap 当前缓冲的值减1,如果缓冲值已经为0,那么变成255,(然后继续自减)  。

我们可以写一个关于模板测试的小例子,例如比较粗糙的法线外拓计算描边的效果,通过模板测试使得纯色与物体源颜色的位置被剔除。

1.原来绘制物体本体的Pass可以设置为:

            Stencil
            {
                Ref 1
                Comp Always
                pass replace
            }

设置参考值为1,并且总是通过模板测试(一个物体本身默认都是总是通过模板测试),然后通过后取代当前模板缓冲区的颜色。

2.需要被剔除的描边Pass的模板值可以设置为:

            Stencil
            {
                Ref 0
                Comp Equal
            }

即参考值为0,比较操作为等于,如果当前模板缓冲区中的值也为0,那么当前的模板测试通过,否则不通过。但是我们设置的物体本体参考值为1,所以看成总是不通过。

那么总代码为:

Shader "Hidden/OutLine"
{
    Properties
    {
        _MainTex("MainTex",2D)="white"{}
        _OutLineColor("OutLineColor",Color)=(0,0,0,0)
        _OutLineWidth("OutLineWidth",float)=1.0
    }
    SubShader
    {
        Tags
        {
            "DisableBatching"="True"
            "Queue"="Geometry"
        }
        Pass
        {
            Stencil
            {
                Ref 1
                Comp Always
                pass replace
            }
            CGPROGRAM
            #pragma vertex myVertex
            #pragma fragment myFragment
            #include "UnityCG.cginc"
            sampler2D _MainTex;
            float4 _MainTex_ST;

            struct VertexData
            {
                float4 vertex:POSITION;
                float2 uv:TEXCOORD0;
            };

            struct VertexToFragment
            {
                float4 pos:SV_POSITION;
                float2 uv:TEXCOORD0;
            };

            VertexToFragment myVertex(VertexData v)
            {
                VertexToFragment VToF;
                VToF.pos=UnityObjectToClipPos(v.vertex);
                VToF.uv=TRANSFORM_TEX(v.uv,_MainTex);
                return VToF;
            }

            fixed4 myFragment(VertexToFragment VToF):SV_TARGET
            {
                return tex2D(_MainTex,VToF.uv);
            }
            ENDCG
        }
        pass
        {
            Stencil
            {
                Ref 0
                Comp Equal
            }
            CGPROGRAM
            #pragma vertex myVertex
            #pragma fragment myFragment

            #include "UnityCG.cginc"

            float4 _OutLineColor;
            float _OutLineWidth;
            
            struct VertexData
            {
                float4 vertex:POSITION;
                float3 normal:NORMAL;
            };

            struct VertexToFragment
            {
                float4 pos:SV_POSITION;
            };

            VertexToFragment myVertex(VertexData v)
            {
                VertexToFragment VToF;
                VToF.pos=UnityObjectToClipPos(v.vertex);
                float3 worldNormal=UnityObjectToWorldNormal(v.normal);
                float3 clipNormal=mul(UNITY_MATRIX_VP,worldNormal);
                VToF.pos.xy+=clipNormal.xy*_OutLineWidth;

                return VToF;
            }

            fixed4 myFragment():SV_TARGET
            {
                return _OutLineColor;
            }
            ENDCG

        }
    }
}

第一个Pass可以非常单纯地输出模型,只需要设定模板测试指令就好。第二个Pass如果要均匀地让法线外拓,不能单纯地在模型空间中根据法线偏移(这样会导致效果非常差,因为经过了矩阵乘法之后一些值发生了变化),而是需要在裁剪空间中进行(这和上篇文章中的水下采样偏移有点类似),我们这里先让法线正确地转移到世界空间中,然后再通过矩阵转移到裁剪空间中。然后进行外拓,片元着色器常规地输出颜色就好。

最终我们输出一个很简单的描边效果(可以看到这个方法在正方体这种大范围相同法线的模型上面描边效果非常烂):

模板测试遇见的小坑:

模板测试也可以做一些遮罩的效果,例如可以让某个物体A只在物体B之后才能出现,就可以使用模板测试进行颜色的替换效果。他们有如下的设置:

物体A:物体A放在后面,如果当前模板缓冲区参考值与之相等,那么就替换此时的模板缓冲区。如果不相等,则让缓冲区保持原样(即A物体本身不写入)。指令如下:

        Stencil
        { 
            Ref 2
            Comp Equal
            Pass Replace
            Fail Keep
        }

物体B:物体B在前,如果要让A渲染出来,那么必须关闭物体B的深度写入(否则A物体无论咋设置都会在深度测试的时候被清理掉)。然后设置参考值,如果相等就保持缓冲区颜色不变,如果不相等就替换当前缓冲区的颜色。指令如下:

        ZWrite Off
        Stencil
        {
            Ref 2
            Comp NotEqual
            Pass Replace
            Fail Keep
        }

最终的效果就是,在后面的物体如果被前方物体遮挡才能被渲染出来:

这里我自己在测试的时候犯嘀咕,为什么物体B不能用Equal操作符呢?我们稍作修改,将上面NotEqual的例子逻辑反过来,将物体B改成如下的样子:

        ZWrite Off
        Stencil
        {
            Ref 2
            Comp Equal
            Pass Keep
            Fail Replace
        }

 但实际上,如果物体B这样做了,那么当在模板测试的时候,反而看不到自己的颜色了:

我个人分析,因为在渲染物体B的时候,场景中背景实际上是空无一物,所以无论如何,背景都没有参考值为5的物体,所以无论如何模板测试都不通过,则物体B所有的片元被抛弃。我们可以认为物体根据Pass指令进行的缓冲区更新操作,只涉及到没有被抛弃的像素,而此时即使使用Replace指令来命令模板测试失败的像素替代模板缓冲区的值,也不能正确地渲染当前物体的像素(这个就是坑),我们可以认为如果不符合模板测试的像素被抛弃后是不能用来替代缓冲区的。

并且,如果我们将上文中的Fail指令的值改为Keep,则不能正确渲染物体A:

因为物体B渲染在前,它的模板缓冲指令恒为Keep,那么对于物体B来说渲染的出来的像素值就恒为B的背景色,即使物体A符合参考值,想要将像素渲染在屏幕上,但由于物体B在物体A前方,所以物体A不能被渲染到物体B上。 

并且,我们这里关闭了深度写入,所以每次物体A的像素如果符合参考值,那么也会符合Pass操作,因为在它前面的物体B并没有干扰到物体A的深度测试,并且二者参考值相等,所以使用Replace指令来替代它。

这里是我自己的理解,如果有奆奆看到了万望能指出我理解的问题的死结(说实话我感觉自己有点被绕进去了)。


深度Depth

我们其实在上一章就讲过了深度测试的一些内容了,例如NDC的Z轴是怎么来的,例如线性深度为视角空间Z分量什么的。这里实际上是对Shader里面关于深度的指令进行一个补完,因为ZTest和ZWrite两个指令看起来很简单,但实际上一些比较规范的说法比较绕,所以我自己每次看到那些说法都头晕。对于深度测试来说Shader存在两个指令:ZTest和ZWrite,即深度测试和深度写入,深度测试可以自己确定枚举,深度写入只能开启或关闭。

一般来说,深度测试比较的是本像素的深度值(左)深度缓冲区中的深度值(右)。默认值是LEqual,即小于等于深度缓冲区的值即通过。

深度测试的枚举有:

Greater

相当于“>”操作,即仅当>右边,深度测试通过

GEqual

相当于“>=”操作,即仅当左边>=右边,深度测试通过

Less

相当于“<”操作,即仅当左边<右边,深度测试通过

LEqual

相当于“<=”操作,即仅当左边<=右边,深度测试通过

Equal

相当于“=”操作,即仅当左边=右边,深度测试通过

NotEqual

相当于“!=”操作,即仅当左边!=右边,深度测试通过

Always 不管公式两边为何值,深度测试总是通过
Never 不敢公式两边为何值,深度测试总是失败 ,像素被抛弃

我们可以稍微测试一下这些指令,例如我们设置某个物体的深度测试方式为Greater,那么即为:只有当目前深度缓冲区的值大于本像素的深度值,本像素才能被渲染在屏幕上,那么上面的那个遮罩效果可以很轻松的用深度测试实现:

进而我们可以使用深度测试做出一些稍微比较有趣的画面效果,例如刺客信条中比较常见的鹰眼效果(起源之后就没有了,替换成了描边,这个效果感觉枭雄里用得印象最深,我们这里写的版本算是糙之又糙版):

Shader "Hidden/HawkEye"
{
    Properties
    {
        _MainTex("Texture",2D)="white"{}
        _HalkEyeColor("HalkEyeColor",Color)=(0,0,0,0)
    }
    SubShader
    {
        CGINCLUDE

        #include "UnityCG.cginc"
        sampler2D _MainTex;
        float4 _MainTex_ST;
        float4 _HalkEyeColor;

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

        struct VertexToFragmentNormal
        {
            float4 pos:SV_POSITION;
            float2 uv:TEXCOORD0;
        };

        struct VertexToFragmentEye
        {
            float4 pos:SV_POSITION;
            float EdgeSize:TEXCOORD0;
        };

        VertexToFragmentNormal vertexNormal(VertexData v)
        {
            VertexToFragmentNormal VToFN;
            VToFN.pos=UnityObjectToClipPos(v.vertex);
            VToFN.uv=TRANSFORM_TEX(v.uv,_MainTex);
            return VToFN;
        }

        fixed4 fragmentNormal(VertexToFragmentNormal VToFN):SV_TARGET
        {
            return tex2D(_MainTex,VToFN.uv);
        }

        VertexToFragmentEye vertexEye(VertexData v)
        {
            VertexToFragmentEye VToFE;
            VToFE.pos=UnityObjectToClipPos(v.vertex);
            float3 objectDir=normalize(ObjSpaceViewDir(v.vertex));
            VToFE.EdgeSize=1-dot(objectDir,v.normal);
            return VToFE;
        }

        fixed4 fragmentEye(VertexToFragmentEye VToFE):SV_TARGET
        {
            return fixed4(_HalkEyeColor.rgb*VToFE.EdgeSize,VToFE.EdgeSize);
        }
        ENDCG
        Pass
        {
            CGPROGRAM
            #pragma vertex vertexNormal
            #pragma fragment fragmentNormal
            ENDCG
        }
        pass
        {
            Tags
            {
                "Queue"="Transparent"
            }
            ZTest Greater
            Blend SrcAlpha OneMinusSrcAlpha
            ZWrite Off
            CGPROGRAM
            #pragma vertex vertexEye
            #pragma fragment fragmentEye
            ENDCG
        }
    }
}

其实所谓的鹰眼的内描边就是通过法线和视角的夹角来确定颜色程度(这种写法在光照里面已经老生常谈了)我们这里需要注意一下,对于专门描述这个鹰眼效果的Pass需要关闭深度写入以免影响到其他的物体,其他的代码都很常规,效果一般般:

然后,关于这两个指令的作用,规范的说法比较绕,我总结一下:

  • 当ZTest通过后,将会写入颜色缓存。
  • 当ZWrite开启后,将会写入深度缓存。

这里两个指令看起来有四种组合,但实际上只是在ZTest中嵌套ZWrite指令罢了,我们可以认为二者的关系在视角空间中如下:

  • 物体ZTest通过,即保证物体前面(Z值较小)的物体能正确渲染。
  • 物体ZWrite通过,则保证物体后面(Z值较大)的物体能正确渲染。

这个是我自己总结的一句话,归纳起来叫测试保前,写入保后。这句话也是绕的不行,是我在写上一篇博客的深度图的时候想出来的,但是如果您非常了解了上一篇博客的深度信息的推导后,这句话可能对您有所帮助。

物体深度测试如果不进行,那么物体默认会渲染在屏幕最上面, 则不能保证其他更前方的物体的渲染,所以深度测试保证了一个物体前方物体的正确渲染。如果深度写入不进行,那么后面的物体无法确定当前物体的深度,就会渲染在当前物体的前方,所以深度写入保证了一个物体后方物体的正确渲染。

关于深度的应用还有很多(上面那个鹰眼是真的烂大街),而且应用起来特别炫,我在之前写水面的时候已经写过一些,之后会专门写一篇博客归纳一下深度的应用。


混合Blender

当我们的像素通过前两个测试后来到了最后这个阶段,混合,这个阶段也是一个可选阶段,一般用于透明物体“Transparent”上(不透明物体二话不说直接覆盖缓冲区就完事了),判断我们当前像素的颜色是否要和颜色缓冲区中的颜色进行混合。我们称当前通过各种测试的颜色为源颜色(SourceColor),称颜色缓冲区中的颜色为目标颜色(DestinationColor),它们两个混合之后就是输出颜色(OutputColor)。混合的指令结构为:

Blender  SrcFactor  DestFactor  SrcAlphaFactor  DestAlphaFactor

这个指令转换成两行代码:

OutputColor.rgb = (SourceColor.rgb)* SrcFactor  +(DestinationColor.rgb)*DestFactor

OutputColor.a = (SourceColor.a)* SrcAlphaFactor  +(DestinationColor.rgb)* DestAlphaFactor

在Shader中,混合因子Factor有如下的取值:

One 因子值为1
Zero 因子值为0
SrcColor 因子值为源颜色值,当混合rgb时,使用源颜色rgb分量,当混合a时,使用源颜色a的值。
SrcAlpha 因子值为源颜色Alpha值。
DstColor 因子值为目标颜色值,当混合rgb时,使用目标颜色rgb分量,当混合a时,使用目标颜色a分量。
DstAlpha 因子值为目标颜色Alpha值。
OneMinusSrcColor 因子值为(1-源颜色值),当混合rgb时,使用1-源颜色rgb分量,当混合a时,使用1-源颜色a的值。
OneMinusSrcAlpha 因子值为(1-源颜色Alpha值)。
OneMinusDstColor 因子值为(1-目标颜色值),当混合rgb时,使用1-目标颜色rgb分量,当混合a时,使用1-目标颜色a分量。
OneMinusDstAlpha 因子值为(1-目标颜色Alpha值)。

我们在之前的一些地方已经应用到了混合因子,例如在多光照前向渲染中,forwardAddPass需要叠加在forwardBasePass上,所以二者的权重比都为1,即:

Blend One One

例如在上文中的透明效果的混合指令,即为当前像素颜色和颜色缓冲区颜色根据透明度的混合,说白了就应该是一个线性插值,我们可以认为下面三个式子的意思是一样的:

  • Blender SrcAlpha OneMinusSrcAlpha 
  • SrcColor*(SrcAlpha)+DestColor*(1-SrcAlpha)
  • Lerp(SrcColor,DestColor,SrcAlpha)

我们可以尝试一下使用混合指令做一个一年前我之前做过的效果(我也不知道这个效果叫啥。。。好像是叫烟雾效果?反正挺糙的),这个效果非常简单

Shader "Custom/TextureCombine"
{
    Properties
    {
        _MainTex("Maintex",2D)="white"{}
        _AlphaEdge("AlphaEdge",Range(1,20))=1
    }
    SubShader
    {
        Tags 
        {
            "Queue"="Transparent" 
        }
        pass
        {
            Tags
            {
                "LightMode"="ForwardBase"
            }
            Blend SrcAlpha OneMinusSrcAlpha
            CGPROGRAM

            #pragma vertex myVertex
            #pragma fragment myFragment

            #include "Lighting.cginc"

            sampler2D _MainTex;
            fixed4 _MainTex_ST;

            float _AlphaEdge;

            struct VertexData
            {
                fixed4 vertex:POSITION;
                fixed4 normal:NORMAL;
                fixed4 uv:TEXCOORD0;
            };

            struct VertexToFragment
            {
                fixed4 pos:SV_POSITION;
                fixed3 worldNormal:TEXCOORD0;
                fixed3 worldPos:TEXCOORD1;
                fixed2 uv:TEXCOORD2;
                fixed2 BumpUV:TEXCOORD4;
            };

            VertexToFragment myVertex(VertexData v)
            {
                VertexToFragment VToF;
                VToF.pos=UnityObjectToClipPos(v.vertex);
                VToF.uv=TRANSFORM_TEX(v.uv,_MainTex);
                VToF.worldPos=mul(unity_ObjectToWorld,v.vertex);
                VToF.worldNormal=UnityObjectToWorldNormal(v.normal);
                return VToF;
            }

            fixed4 myFragment(VertexToFragment VToF):SV_TARGET
            {
                fixed3 worldViewDir=normalize(UnityWorldSpaceViewDir(VToF.worldPos));
                fixed3 worldNormal=normalize(VToF.worldNormal);
                fixed3 albedo=tex2D(_MainTex,VToF.uv.xy).rgb;

                return fixed4(albedo,pow(max(0,dot(worldViewDir,worldNormal)),_AlphaEdge));
            }
            ENDCG
        }
    }
}

其他的代码都没有啥,主要是最后计算输出颜色的时候根据法线与视角的余弦值来判断边缘的透明度,为了让边界的效果更强烈,使用了自定义次幂来增强效果,并且使用max限定了最低的角度透明度值(否则因为余弦值为负边缘可能会有黑边,其实这里就是标准光照的高光模型)。然后效果如下(话说这个代码我在一年前完全看不懂):

Blender一般默认为加法,但是实际上还可以有其他的计算方式,由于我自己应用地比较少,这里就不多写了。其他的用法也就是更换计算方法来实现一些特定的画面效果。


杂记2:随便记一点点

渲染队列RenderQueue:

Unity中的渲染队列是一个很基础的东西,我在这里记一下以免以后忘个精光:

  • BackGround1000):在所有物体渲染之前被渲染,通常用来渲染那些需要被渲染在背景上的物体(并不是专指天空盒)
  • Geometry2000):默认不透明队列。
  • AlphaTest2450):透明度测试队列,对于需要有镂空等效果都可以放在这个队列中。
  • Transparent3000):透明队列,在所有不透明或者透明度测试队列之后开始渲染, 任何使用了透明度混合或者关闭了深度写入的物体都应该在这个队列。
  • OverLay4000):在最后被渲染,一般用于一些叠加的效果。

需要注意的是:

对于开启了深度写入的物体(即Geometry和AlphaTest队列)根据深度从前往后渲染,因为后面的物体往往深度较大,许多像素都需要剔除,如果先渲染后面的物体然后再用前面的物体覆盖就太浪费了。

对于透明队列Transparent的物体为从后往前渲染,因为需要使用混合命令让物体颜色与颜色缓冲区颜色叠加,所以为从后往前渲染。

Early-Z技术:

我们常常可以看到,片元着色器塞了大量的逻辑代码,如果电脑好不容易处理完一个像素的片元着色器在后面测试的时候却扔了确实挺浪费的,所以Unity使用了Early-Z技术来在提前进行深度测试,那么最上面那张渲染流水线的图,在Unity中实际上是这样:

这里我们看到:

  • 在片元着色器之前就已经进行了深度测试,如果没有通过测试就剔除掉这个片元。
  • 逐片元操作中的深度测试仍然存在,用于检查最终的深度关系是否正确。(可以说上了双保险)

摘自CSDN某奆奆的博客:Early-Z主要是通过一个Z-pre-pass实现,简单来说,对于所有不透明的物体,首先用一个非常的shader进行渲染,这个shader不写颜色缓冲区,只写深度缓冲区,第二个pass关闭深度写入,开启深度测试,用我们自己指定的shader进行渲染。

(这一点我仔细一想好像有点毛病,基本的深度指令好像不能完成只写入深度缓冲区而不写入颜色缓冲区这种操作,如果有奆奆看到了可以赐教)。 

并且,如果使用透明度测试的物体的一些像素被手动舍弃掉了,那么将会导致Early-Z的测试不能正常运行,所以GPU会判断是否存在透明度测试,如果存在就禁用提前测试。但是这样也导致需要被处理的片元更多,进而导致性能下降。

双重缓冲区:

一般来说,一个像素通过了透明度测试,模板测试,深度测试算是基本上修成正果了,此时它将会被带到后置缓冲区(BackBuffer)中。然后再与前置缓冲区(FrontBuffer)交换,前置缓冲区即为我们当前屏幕的颜色。二者交换后就能在屏幕前看到一帧一帧连续的颜色了。这时候我们就能在屏幕上看见好看的画面啦(棒读)。


批处理:

Unity中支持两种批处理方式来减少DrawCall,一种是静态批处理,一种是动态批处理。

动态批处理:

动态批处理的基本原理是将每一帧可以进行批处理的网格进行合并,然后再将合并的网格数据传递给GPU,并使用同一材质进行渲染。这样做实现很方便,并且被动态批处理的物体仍然可以自由移动,因为每一帧都单独地进行了合并网格

动态批处理虽然强大但也有诸多限制:

  • 被动态批处理的网格顶点属性规模数量应当小于900,注意这里说的是属性规模数量,例如Shader中用到了顶点坐标,纹理,法线纹理三种顶点属性,那么限制的数量规模应该是900/3=300个顶点。
  • 如果一个网格存在光照贴图,那么需要保证这些网格指向光照纹理中的同一个位置
  • 多Pass的Shader不能被批处理。

如果我们顶点过于密集,就不能使用这种方式了,就需要使用静态批处理。

静态批处理:

静态批处理适用于任何顶点模型。它的实现原理是:在场景运行开始阶段,将需要进行静态批处理的网格合并到一个新的网格结构中,这样的合并操作只进行一次,所以这些模型不能在运行时下移动。

这样做会导致要占用更多的内存来存储合并后的网格结构,而且,如果一个场景中有N个物体共用同一个网格数据,那么它们进行静态批处理后会在内存中占用N个相同数据的网格。所以静态批处理是一种用空间换时间的方法。

静态批处理在内存实现上,将这些网格数据都转换到世界空间中,然后为它们构建一个更大的顶点和索引缓存。所以Unity只需要一个DrawCall就可以将这些网格都打包给GPU了,但是这是以物体不能被移动,并且不能在Shader中在要被静态批处理物体的模型空间中进行操作为代价的。

如果我们一些Shader需要作出一些模型空间的操作,那么我们就不希望它被批处理,在Shader中可以使用DisableBathing来强制让这个Shader对应的材质不会被批处理:

        Tags
        {
            "DisableBatching"="True"
        }

共享纹理:

无论是静态批处理或者动态批处理,都要求被批处理的物体都共用同一个材质球(Material,而不是同一个Shader)。我们可以同一个材质球不同的纹理图片进行合并,生成一个图集,当使用同一个材质球的不同物体需要使用不同的图集时,只是需要对不同的纹理坐标采样即可。同时,用代码设置材质球属性也有两种情况:

  • MeshRender组件的sharedMaterial属性可以让我们对被多个物体共享的材质球进行设置。
  • MeshRender组件的Material属性可以单独设置某个物体本身唯一的材质球,即使这个材质球被其他物体所公用,Unity也会生成这个材质球的副本,但这样就破坏了这些材质的批处理。

这篇杂记就寄到这里,其实里面很多内容自己都说实话,没有整明白到“内心通透”的地步,只能说是应付,嗯,我觉得以后看了更多书有了更多长进后会回来再重写这篇博客的。我好几篇博客都是发布完成后反复改的那种。

参考:

https://blog.csdn.net/puppet_master/article/details/53900568(写得真的很不错,赞)

猜你喜欢

转载自blog.csdn.net/qq_38601621/article/details/103469905