Shader用的太少,老是忘,必须得记录一下了。

一、Shaderlab,Unity CG,NVIDIA CG,HLSL,GLSL

        首先,shaderlab不是一种语言,而是unity的着色器框架,使用它来组织着色器的属性,配置渲染状态等。

        CG,HLSL,GLSL三者都是高级着色器语言,他们是不同厂商制作的,他们是独立的。

        UNITY CG。它可以被理解为unity经过定制的HLSL语言,并且兼容部分CG语法,并通过unity的编译器实现跨平台转换,他不是纯粹的NivindaCG语言,也不能编写GLSL。

        1.1.Shader结构

Shader "Custom/C5-SimpleShader"
{
    Properties
    {
        //属性块
        _FloatVariable ("Float类型变量",Float)=1.5
        _MainTex("主纹理",2D)="white"{}
    }
    SubShader
    {
        [Tags]//标签,比如Queue,RenderType,DisableBatching,ForceNoShadowCasting,IgnoreProjector,CanUseSpriteAtlas,PreviewType

        [RenderSetup]//状态,比如Cull,ZTest,ZWrite,Blend

        Pass{
            [Name]//可以使用 Name "MYPassName"来定义Pass的名称,之后可以使用 UsePass "PassName"来直接调用别的pass,实现代码复用
            
            [Tags]//Pass中也可以使用Tags,但是基本只有LightModeRequireOptions两种。
            [Rendersetup]//和SubShader中的pass一样,但是subshader中设置的状态作用范围更广,pass中设置的状态可以覆盖subshaer中设置的状态
            

            CGPROGRAM//开始编写具体代码

            #pragma vertex vert//定义函数
            #pragma fragment frag
            
            //关联Properties
            float _FloatVariable;
            sampler2D _MainTex;            

            struct a2v{
                float4 vertex:POSITION;
                float3 normal:NORMAL;
                float4 texcoord:TEXCOORD0 ;
            };
            
            struct v2f{
                float4 pos:SV_POSITION;
                fixed3 color:COLOR0;
            };

            v2f vert(a2v v) {
                v2f o;
                o.pos=UnityObjectToClipPos(v.vertex);
                o.color=v.normal*0.5+fixed3(0.5,0.5,0.5);
                return o; 
            }
            
            fixed4 frag(v2f i) :SV_Target{
                return fixed4(i.color,1.0); 
            }
            ENDCG
        }
        
        Pass{
            //另外的pass代码
        }
    }
    SubShader{
        //另外的subshader代码
    }
    FallBack "Diffuse"//上面所有subshader均不可用时使用这个shader。通常我们不需要自己编写阴影,因为只要fallback的shader中包含了阴影的pass就unity就可以使用fallback中的阴影。
}

二、渲染流程

        渲染流程分为三阶段:应用阶段、几何阶段、光栅化阶段

        2.1.应用阶段(cpu):

        准备相机,模型,配置材质和shader,然后把渲染图元传递给下一个阶段:几何阶段

        2.2.几何阶段(gpu):

        决定要渲染哪些片元以及如何渲,执行逐顶点逐片元得操作。几何阶段把图元坐标变换到屏幕空间,再交给光栅器进行处理,除了位置外,还包括每个对应的深度值,着色等信息。

        2.3.光栅阶段(gpu)

        主要是使用上一个阶段传递过来的得数据来决定如何产生屏幕上的像素并渲染最终图像。

三、CPU和GPU之间的通信

        3.1.渲染流水线的起点是CPU,即应用阶段。

        应用阶段大致分为三个阶段:把数据加载到显存中,设置渲染状态,调用Drawcall。

        其中Drawcall是重点,drawcall表示一次发送到gpu的渲染指令,这个命令指向一个渲染图元列表。他是从CPU发起的,它的工作可以理解为整合数据并发送指令,如果需要整合的次数太多就会占用太多cpu资源导致卡顿掉帧。

        unity中的批处理就是把一个物体发送一次Drawcall优化成多个物体打包在一起发送一次Drawcall,所以可以优化性能。

        cpu和gpu是通过命令队列来交互的,避免等待。

        3.2.GPU流水线

        本来想挨个介绍的,但是想到只是总结,就不挨个介绍了。

        其中裁剪是降不在视野内的顶点裁剪掉,并剔除某些三角图元的面片。然后屏幕映射要注意是在几何阶段而不是光栅化阶段,他负责把图元的坐标转换到屏幕坐标系中。

        3.2.1.三角形设置和三角形遍历

        在几何阶段传来的屏幕空间的数据不止包含了顶点位置,还包括了深度,法线和视角方向等等各种信息。然后三角形设置是根据顶点信息得到三角面覆盖的区域。然后三角形遍历就是查找像素是否被三角网格覆盖并进行信息处理,比如深度插值等然后每个像素得到一个片元,片元不只是一个像素,而是包含了很多状态的集合,包括深度,发现,纹理坐标等。这个过程也叫做扫描变换。

        注意图元和片元的区别,图元是指点,线,三角面等,而片元指的是每个像素对应的状态集合。

        3.2.2.片元着色器

        片元着色器的到的数据是插值后的数据,比如纹理坐标。

        3.2.3.逐片元操作

        这一阶段的主要任务是决定每个片元的可见性,比如模板测试,深度测试等,如果测试没有通过这个片元就会被舍弃掉,如果痛过了就会和颜色缓冲区进行混合。

四、Drawcall,Batches和setpass calls

        

        在渲染优化时,关注的主要就是这三个参数。

        其中Batches其实就是Drawcall,因为旧版unity没有考虑过批处理,所以一直叫drawcall,后面引入批处理后就改名成batches。

        所以batches就是发出一次渲染指令,为了优化我们需要使用批处理或者gpu instancing.他们的原理都是打包数据来减少Drawcall,比较简单就不说了。

        除此之外,unity在没有使用批处理和Gpu Instancing的时候unity也会通过材质一致性来优化Setpass call数量。简单来说就是使用一样的材质的物体可以被合并提交来减少SetPass call(注意是同一个材质不是同一个shader)。这个单次的意思是着色器切换次数。它指的是渲染状态切换的次数,由于GPU无法同时保存多个状态配置,所以要渲染不同材质的话需要切换状态后再进行渲染。

        举个例子就是工厂里制作产品的时候模具更换是需要时间的,所以要统一生产相同类型的产品和再更换模具生成下一种产品。      

        实机测试的时候会发现初始场景有两个batches和setpass calls,其实是天空盒的batches和其余的内置渲染。如果去掉天空盒会发现就只剩一个了。然后添加一个cube,会发现增加了5Batches和setpasscalls,其实这是因为默认材质中有多个pass,包括forwardbase,forwardAdd,shadowCaster等。继续复制CUbe,Bathces继续增加,但是setpass calls不会再增加,因为他们使用的相同的材质。

        总结:由上面的知识可以得到优化方案。

        4.1.批处理

        批处理分为动态批处理和静态批处理,动态批处理需要在unity设置中打开,仅适用于顶点数 ≤ 300 的网格,且缩放一致。反正这个对小物体来说效果很显著。

        静态批处理,把场景里不动的东西设置为静态就可以了。

        4.2.GPU Instancing

        就是把实例的变换矩阵和颜色等属性打包到GPU缓冲区然后一次性生成,有点麻烦也有些限制,个人更喜欢动态批处理

        4.3 使用相同材质加图集

        一个模型可能是由多个子模型构成的,比如地图,人物模型。如果它们都使用同一个贴图,每个子模型使用不同区域的uv,那setpass call就只会增加一次。

        比如:

        4.4 通过 MaterialPropertyBlock 修改材质属性,避免创建新材质实例,使用这个既可以减少着色器状态切换又可以顺利更改属性

MaterialPropertyBlock props = new MaterialPropertyBlock();
props.SetColor("_Color", Color.red);
GetComponent<Renderer>().SetPropertyBlock(props);

        五、数学基础

        5.1.屏幕映射坐标系的不同

        OPENGL和DIRECTX的屏幕坐标是不同的。

         (我只有实体书没有电子书,所以一些图片是从网上找的,所以这张图片水印是别人的。。)

        5.2.左手坐标系和右手坐标系

        不同软件使用的坐标系不同,简单来说就是unity绝大部分情况使用的是左手坐标,除了观察空间的坐标系用的是右手坐标系。左手坐标系就是右x,前z上y。右手坐标就是右x后z上y。

        5.3.矢量的点积和叉积

        点积的公式为a.b=|a||b|cos角度,由cos的大小变化关系可以知道,两个向量角度越小值越大,反之趋近于0,若角度大于180,则说明向量方向相反,为负数。所以点积一般用来判断向量的前后关系,趋近关系。比如光照模型中,面的法线和面到光源的向量的点积越小说明角度越小,则亮度应该越强。

        叉积常用于计算两个向量构成的平面的法向量

        5.4.矩阵

        5.4.1.转置矩阵

        转置矩阵就是把原矩阵从上到下把每一行在新矩阵里从左到右按列重新排好,或者说把列从新按行排好也是一样的。书上说的翻转一下就好了,我测试了是要沿着对角线反转才是正确的。

        两个性质:

         补充一个,单位矩阵的转置还是单位矩阵。

        5.4.2.单位矩阵

        5.4.3.矩阵的乘法

        矩阵的乘法是A矩阵的行和B矩阵的依次相乘,所以要求A矩阵的行数必须和B矩阵的列数相同。

        这里就不细说更多了,这里说到矩阵的乘法是想提醒自己矩阵乘法和点积的关联性。

        因为点积的公式也可以为a.b=a.x*b.x+a.y*b.y,而这和矩阵的乘法的步骤是一样的。

        

        还有,也可以用矩阵的乘法来表示点积:

        注意U旁边的转置符号,可能有点不够清晰。

        矩阵乘法 u⊤v 本质上是将点积表示为行向量和列向量的乘积

        矩阵的乘法不满足交换律,即AB!=BA。

        然后,有时候矩阵可以交换顺序相乘。接下来是重点。

        unityshader中的MVP,MV等内置矩阵都是列向量矩阵。所以我们一般用mul(M,v)即可。

         因为Unityshader是按行填充和读取,所以我们在声明列向量矩阵时都是用类似下面的方式进行列向量矩阵声明和使用:

//声明
o.TtoW0=float4(worldTangent.x,worldBinormal.x,worldNormal.x,worldPos.x);
o.TtoW1=float4(worldTangent.y,worldBinormal.y,worldNormal.y,worldPos.y);
o.TtoW2=float4(worldTangent.z,worldBinormal.z,worldNormal.z,worldPos.z);

//使用:
bump=normalize(half3(dot(i.TtoW0.xyz,bump),dot(i.TtoW1.xyz,bump),dot(i.TtoW2.xyz,bump)));

        5.4.4.逆矩阵

        逆矩阵基本就是表示一个变换的逆操作,一般不用计算逆矩阵,都是调用库计算或者使用正交矩阵的转置矩阵直接得到。

        重要的是以下性质:

        5.4.5.正交矩阵

        正交矩阵是一种特殊的方阵,如果一个方阵其列向量(或行向量)构成一组标准正交基那么它就是正交矩阵,判断是否正交只需判断点积是否为0即可。标准正交则是要求基是单位向量。

        也可以说一个和它的转置矩阵的乘积是单位矩阵的方阵是正交矩阵。它的公式为

        

        而这正好和逆矩阵的公式替换掉,所以正交矩阵的逆矩阵就是正交矩阵的倒置,使用这个原理可以简化计算。

        有了上面的知识可以知道旋转,是正交矩阵,所以他们的逆矩阵就是他们的转置矩阵。

        那么其他的呢?实际上对于向量来说平移是无意义的,所以平移可以加上。然后看缩放矩阵,缩放矩阵分为统一缩放和非统一缩放,统一缩放是指各个基的缩放程度一致,比如(2,2,2),(7,7,7).非统一缩放则是不一致,比如(1,2,3)。那么(2,2,2)是否是正交矩阵呢?其实不是,因为他不是单位长度,但实际上转换成2(1,1,1)他就是标准正交,他就是正交矩阵了。所以对于向量来说,平移旋转统一缩放矩阵就是正交矩阵,他们的逆矩阵就是他们的转置矩阵。对于点来说,平移对其有影响,所以只有旋转统一缩放矩阵才是它们的正交矩阵,他们的逆矩阵就等于他们的转置矩阵。

        5.4.6.矩阵的几何意义:变换

        对一个向量或者点左乘一个矩阵即可进行变换,变换的本质是基变换。

        Pnew=MA,其中A表示点或者向量,M表示变换矩阵,Pnew表示变换后的结果。

        首先,线性变换是保留矢量加法和标量乘法的变换。简单来说就是变换后f(x)+f(y)=f(x+y),kf(x)=f(kx)

        齐次,平移变换时降坐标系进行了平移。

        然后我们可以得到仿射变换,仿射变换就是合并了线性变换和平移变换。放射变换用4x4的矩阵表示,因此,矢量被扩展到四维空间,这个空间被称为齐次坐标空间。

        齐次坐标变换时点的w分量被设置为1,因为他需要平移的影响,而向量的w分量被设置为0,因为它不需要平移的影响。

        对一个点或者向量应用一个矩阵M,就可以理解为对其进行变换M,三维矩阵中,表示把坐标系变换为目标矩阵的坐标系。四维矩阵中则加上了平移。

        我们可以把平移旋转和缩放组合起来形成一个复合变换。乘法顺序很重要,一般为pn=M平移M旋转M缩放。我们约定变换顺序为先缩放在旋转再平移。

        5.5.坐标空间

        编写shader时需要转换坐标空间来简化计算。

       

           需要讲讲的是空间变换。

        5.5.1.模型空间变换到世界空间

        就是用世界空间的基来描述模型空间的点。比如模型在世界坐标的(5,5,0),旋转为现在要把模型空间中的点(0,0,1)要转换到世界空间。那么就是先得到世界空间描述的模型的基,再用模型空间坐标*基,使用矩阵表示就是:

        如果有旋转和缩放的话就是:
        

         5.5.2.观察变换(重点)

        观察变换是把世界坐标变换到观察空间,观察空间就是摄像机空间。相机使用的右手坐标系,相机的前方是-z,后方是+z。

        观察空间和屏幕空间是不同的,观察空间是三维的,屏幕空间是二维的。观察空间到屏幕空间需要进行投影。

        世界坐标变换到观察空间,假设要把世界坐标的(0,0,1)变换到处于(0,10,-10),角度为(45,0,0)的相机空间中。

        首先要理清,世界坐标变换到观察空间,就是说用观察空间来描述世界空间。那么就要得到世界空间相对于观察空间来说是如何描述的。而我们根本没有这些信息,因为我们自己的坐标系是以相机为原点的坐标系,其基向量是相机的右、上、前轴。然后也无法直接计算世界空间的基向量相对自己来说是怎样的。所以此时我们需要通过逆矩阵来得到。世界空间知道我们(观察空间)相对于他是怎样的,因为他有我们(相机)的缩放,旋转和坐标,所以它可以构建TRS矩阵来得我们相对于他的变换。此时我们想知道世界空间相对于我们是怎样的,使用这个逆矩阵就可以了,然后我们就可以得到点相对于我们的表示了。

        要注意的是矩阵的描述方式。

        

          M的下标中表示是什么空间变换到什么空间,他的含义是前者相对于后者的位置和方向,即观察空间相对于世界空间来说是怎么样的。更通俗来说,M观察→世界​ 回答了这样一个问题:
“如果我是世界空间,观察空间的坐标系(相机的位置和朝向)对我来说是什么样子的?”

        所以,观察空间变换的公式为:

        

        5.5.3.投影变换

        投影变换是把观察空间变换到裁剪空间,用于判断点是否需要剔除。

        裁剪空间就是视锥体, 通过透视投影矩阵或者正交投影矩阵把点变换到视锥体后得到新的裁剪空间中的新的位置。在经过透视投影矩阵变换后,得到的齐次坐标的w分量就被赋予了更丰富的意义。此时w变成了变换前的-z,我们判断x,y,z是否在(-w,w)区间内就可以判断是否应该被裁剪。正交矩阵变换后w变为1.

         裁剪空间中是三维的,屏幕空间是二维的。屏幕映射就是把裁剪空间映射到二维的屏幕上。首先需要做的就是齐次除法,也被称为透视除法。它就是用齐次坐标的w除以xyz分量得到归一化的设备坐标,也就是NDC,此时观察空间被映射到一个立方体内,unity使用的DirectX的NDC,其中Z分量为(0,1),这个z分量常被用于深度缓冲用于后续判断深度等。

        5.5.4.屏幕映射(视口变换)

       得到NDC后在使用NDC的x和y映射到二维的视口空间就好了。

        注意视口空间和屏幕空间的区别:

        视口空间是屏幕空间除以屏幕分辨率得到的归一化空间,

        屏幕空间是 ​最终像素坐标,表示在物理屏幕或窗口中的绝对位置。

        5.5.5.ComputerScreenPos/Vpos/Wpos

        VPOS和WPOS都是获得片元在屏幕空间的像素位置。

        ComputeScreenPos是UNIty提供的函数,它返回的是裁剪空间映射到屏幕空间的未除以w的坐标。为什么不除以w呢,因为我们一般在顶点阶段调用它,如果在顶点阶段除以w后,之后在光栅化阶段插值的时候就会破坏w所表示的非线性投影,之后的阶段就会得到错误的值。所以这里没有进行透视除法。

        这里其实我不太理解为什么会破坏,不过先不管了,应该是算法导致的会被破坏吧,我不打算研究到太深。。。

        5.6.法线变换

        顶点中包含法线和切线信息,它们互相垂直,即点积为0.

        当进行变换时,由于切线是通过计算顶点之间的插值得到的,所以即使进行非统一变换切线的角度也是正常的,但法线不同,法线在进行非统一变换后角度会发生变化
        

         为了得到正确的法线效果,也就是变换的法线和变换后的切线正交,通过一系列推导可得原变换矩阵的逆转置矩阵来变换法线就可以的得到正确的结果。 

六、unity的基础光照

        书中提到了BRDF光照和标准光照模型,BRDF光照模型是指给定模型表面上的一个点,BRDF表示了对该点外观的完整描述。所以标准光照模型是BRDF光照模型的一种,并且他是经验模型。经验模型是指它的结果并不完全正确,知识看起来正确。

        6.1.标准光照模型

        标准光照模型只关心直接光照,它包含4部分:自发光,高光反射,漫反射,环境光。

        其中环境光使用全局变量来描述,比如unity中的固定颜色环境光,渐变环境光等。

        自发光是指材质的自发光颜色。

        6.1.1.漫反射使用兰伯特光照

        漫反射不关心视角方向只关心光照方向。其公式为:

        其中n和l的点积表示计算光线和法线的角度,角度越小亮度越强,反之越弱。非常简单。

        6.1.2.高光反射

        高光反射的计算主要有两种计算方法:

        6.1.2.1.Phong模型高光计算

        知道l和n可以计算出r

        

        

        然后使用以下公式得到高光效果:

        

        其中v和r的点积表示视角反向和反射光线的近似度,然后使用指数mGloss让亮度随着角度减小指数减少。mgloss数值越大,角度增大,点积指数减小,所以指数越大,亮斑越小,反之亮斑越大。

        6.1.2.2.Blinn模型高光计算

        Blinn模型也是一种计算高光的经验模型。

        它使用法线和(视线方向和光照法向的平均值)的点积来计算光照贡献度。

        

        

        6.2Phong模型和BlinnPhong模型

        上文中标准光照模型因为是裴祥风提出的,所以称之为Phong模型,而高光模型如果使用的是Blinn方法计算,那就叫做BlinnPhong模型。Phong模型和BlinnPhong模型的区别是高光是否使用Blinn方法计算。

        6.3.Phong着色和高洛德着色

        在计算光照时可以在片元着色器计算也可以在顶点着色器中计算。

        在片元着色器中计算称为逐像素光照,在顶点着色器中计算称为逐顶点光照。

        在片元着色器中获取片元法线进行光照模型计算称为Phong着色,它的法线信息可能来自顶点光栅化阶段法线的插值也可能来自法线贴图。

        在顶点着色器中直接通过顶点计算光照颜色,片元着色器获得的是光栅化阶段光照颜色的插值,则称之为高洛德着色。
        示例:高洛德着色(逐顶点光照)

顶点阶段计算光照颜色
片元阶段直接使用颜色

        示例:Phong着色(逐像素光照)

片元着色器中计算光照​​​

        6.4.半兰伯特光照

        在兰伯特模型计算漫反射时背面会比较黑暗,这可以通过添加环境光或者使用半兰伯特模型来解决。

        

七、UNITY内置函数

        八、基础纹理

        8.1.纹理坐标,uv

        首先要理解UV,在导入的模型顶点中就包含uv,uv表示的是纹理坐标,表示当前顶点应该使用贴图中的那个位置的纹素来显示。

        

        8.2.使用贴图

        首先声明属性:_MainTex("主纹理",2D)="white"{}

        其中"white"表示默认纹理,也可以改成"gray","black"等。”white"后的{}以前可以填写某些属性,现在不能填写了,但是{}还是保留了下来。

        属性声明后还需要再声明一次变量,才能使用,因为它是2D类型,所以需要使用Sampler@d来声明,并且要求同名。

        

        然后是贴图的UV的缩放和平移属性。声明贴图变量的名称加_ST来获得在材质面板中设置的缩放和平移属性。

        

        _ST属性是操控uv的取值,使用以下代码来进行实际的缩放和平移。_ST属性的xy代表缩放,zw代表平移。

        

        代码等价于:

        第一个属性表示哪个纹理坐标,第二属性表示用哪个贴图的_ST属性来变换。

        最后用Tex2D来采样贴图代表兰伯特光照中的材质颜色进行光照模型计算:

        

        8.2.1.贴图的WrapMode

        九、法线纹理(凹凸贴图)

        凹凸映射主要有法线纹理和置换贴图两种形式,这里不讲置换贴图,只讲法线贴图

        9.1.模型空间法线贴图和切线空间法线贴图

        总的来说,法线贴图可以按照模型空间的坐标系来存储,也可以按照切线空间的坐标系来存储。模型空间存储的贴图看起来是五颜六色的的,而切线空间的贴图看起来基本都是浅蓝色。模型空间法线对于模型来说存储的是相对于模型原点的方向,当模型发生形变时法线效果就会错误,而切线空间存储的是相对顶点的的方向,所以模型变化后法线也是正确的。总的来说切线空间更适合应对变化。并且由于切线空间的法向量总是正方向,所以可以只存储xy方向,因此可以减少贴图大小。

        计算法线可以在切线空间中计算也可以在世界空间中计算。区别就是是把光照等向量转换到切线空间还是把法线从切线空间转换到世界空间。

        9.1.1.世界空间转切线空间法线光照计算

        在切线空间中计算光照,就是把光照方向,视线方向等都转化为切线空间然后计算光照。

        要注意的是使用法线贴图本质上是代替模型的法线计算光照,模型本身是有法线和切线的,顶点的切线空间就是切线,副切线和法线构成的,其中切线和法线已知,叉乘即可得到副切线。

        首先我们可以通过ObjSpaceLightDir或者ObjSpaceViewDir直接获得从世界空间转换到模型空间的向量。所以现在只需要把这两个转换到切线空间,我们需要得到模型转切线空间。而切线空间其实就是切线,副切线和法线构成的空间。我们需要的矩阵就是[切线,副切线,法线]构成的列矩阵。其实理出来还是挺简单的。

        之前说过,unity填充矩阵是按照行进行填充的,所以变换应该用转置后的矩阵进行变换,为了简写,可以直接进行左乘。

        总的步骤如下:

v2f vert(a2v v) {
    v2f o;
    o.pos = UnityObjectToClipPos(v.vertex);
    o.uv.xy = TRANSFORM_TEX(v.texcoord, _MainTex);
    o.uv.zw = TRANSFORM_TEX(v.texcoord, _BumpMap);
    
    // 计算TBN矩阵
    float3 tangentWS = normalize(mul(unity_ObjectToWorld, v.tangent).xyz);
    float3 normalWS = normalize(mul(v.normal, (float3x3)unity_WorldToObject));
    float3 bitangentWS = cross(normalWS, tangentWS) * v.tangent.w;
    
    // 构建切线→世界空间矩阵
    float3x3 tangentToWorld = float3x3(tangentWS, bitangentWS, normalWS);
    
    // 转换光线和视角方向到切线空间
    o.lightDir = mul(tangentToWorld, ObjSpaceLightDir(v.vertex));
    o.viewDir = mul(tangentToWorld, ObjSpaceViewDir(v.vertex));
    return o;
}

        片元着色器就是解析贴图的切线进行光照计算,主要是解析的过程要稍微说一下:

        如果法线贴图我们没有设置为法线贴图格式的话那我们需要手动解析才行,如果用UNpackNormal就会自动解析。原因如下图:

        然后,因为法线贴图中存储的只有x和y,又因为法线是单位向量,所有法线的z分量等于sqrt(1 - x² - y²)        

         9.1.2.切线空间转世界空间法线光照计算

        和上面对应,这里是把切线空间转到世界空间中进行计算,这种更常用,因为后续可能还会在世界空间进行其他操作。

        首先,我们要知道切线空间转换到世界空间的矩阵,然后把贴图得到的法线使用这个矩阵进行变换就可以了。

        由于切线空间是模型空间中相对于每个顶点的,所以我们对它使用模型空间到世界空间的矩阵变换就可以得到世界空间中的表示。注意这里的法线使用了Unity内置函数来获得,它考虑到了非统一缩放下的法线的不准确的情况。如果手动编写需要使用之前的知识(逆转置矩阵)来获得准确的法线。上面的切线空间中的法线计算没有用到逆转置矩阵是因为他是在切线空间中进行的,没有缩放。

        图中用插值寄存器构建了矩阵,可以看到这个矩阵是列矩阵。

        之后世界空间中的贴图法线由以下公式得到:

        十、渐变贴图

        这个说的是卡通着色的技术,很简单,就是根据半兰伯特把光照贡献度映射到(0,1)范围,然后根据这个贡献度从渐变材质中取色和模型颜色,光照颜色等相乘来达到根据明暗叠加颜色的效果,如果渐变材质做成分段那么明暗分界线则会更加明显。

        十一、遮罩贴图

        遮罩贴图和名字一样就是用来控制某些效果的显示范围和强度的,遮罩贴图的某个通道的值就可以用来表示效果的强弱程度,公式都是自定义的。

        十二、透明效果

        透明效果分为透明度测试和透明度混合两种。

        透明度测试非常简单,检查片元透明度是否符合条件,不符合则直接舍弃片元。

        透明度混合则要关闭深度写入,在使用透明度混合的时候我们需要关闭深度写入,因为如果不关闭深度写入的话,渲染在透明物体背后的物体会因为深度较高而被剔除,此时也就没有办法和后面的物体的颜色进行混合来达到半透明效果了。所以这是必须的,但是在关闭深度写入后渲染顺序就变得尤为重要。如果我们先渲染了半透明物体,此时因为关闭了深度写入,深度缓冲没有更新任何数据,之后再渲染不透明物体时,不透明物体发现深度缓冲没有深度值,那么就以为自己没有被遮挡,则会覆盖颜色缓冲区。此时半透明效果也就失效了,所以透明物体一定要在不透明物体之后渲染。

        为了做到能在不透明物体之后渲染,Unity使用渲染队列来控制渲染顺序,渲染队列值越高越晚渲染。

        12.1.透明度测试

        透明度测试需要设置渲染队列为AlphaTest表示在不透明物体之后渲染(我测试放在Geometry中也是一样的,好像是会破坏earlyZ浪费性能,所以最好还是放在alphaTest中),IgnoreProjector表示忽略Unity的Projector组件,projector组件是unity之前的一个用于投射材质的组件,几乎已经被抛弃了用不到。RenderType​ 是一个用于 ​分类着色器类型​ 的标签(Tag),它本身不直接影响渲染结果,但会告诉 Unity 引擎该 Shader 属于哪种类别(例如不透明、透明、植被等)。这一信息在 Unity 的渲染管线中用于实现某些特定功能,例如 ​屏幕后处理、替换着色器(Replacement Shader)​​ 或 ​渲染器功能(Renderer Features)​。

        透明度测试使用clip函数直接剔除不想渲染的片元:     

        然后是Fallback特殊的shader来获得阴影效果:

        ----------------------------------------------------------------------------

        透明度测试效果会导致背面看起来空空的,可以使用Cull命令Cull off关闭背面剔除,Cull Back|Front|Off 分别表示剔除背面,前面和都不剔除。

        

        12.2.透明度混合

        上面已经介绍了大部分透明度混合的原理,我们需要关闭深度写入得到正确的深度效果,要使用透明度混合还需要混合命令,因为半透明效果本质其实是自己的颜色和颜色缓冲区的颜色进行混合。我们需要用到Blend命令:

        在混合的时候我们所谓的颜色是包含了RGBA4个通道。使用Blend srcFactor DstFactor的时候就是颜色和透明度是用同样的srcFactor DstFactor混合,而使用Blend srcFactor DstFactor,SrcFactor DstFactor就表示颜色和透明度用两组不同的参数进行混合。

        我们在设置了Blend命令后就可以获得半透明效果了,在编写shader的时候我们不需要写特殊的代码,只需要关闭深度写入,开启混合,设置好渲染队列就可以获得透明度混合效果了,颜色输出只需要像写不透明shader一样进行输出就好了。

        

        12.3.实验证明

        制作半透明效果的时候其实Blend才是最主要的,不管开没开深度写入,什么渲染队列,片元透明度是多少,只要开启了Blend命令,并且使用合适的公式,就可以看到半透明效果。只不过会不稳定,比如看起来在闪烁。

Tags { "Queue"="Geometry"  }
ZWrite on
Blend SrcColor OneMinusSrcColor,One Zero
// 片段着色器返回白色,alpha值为1
fixed4 frag (v2f i) : SV_Target
{
    return fixed4(0.5,0.5,0.5,1);
}

        比如使用不透明渲染队列,不关闭深度写入,透明度为1,只使用Blend命令进行混合。可以得到看着正常的效果。

        

        但是随着相机距离的变化,透明度效果也会变化。因为不透明物体在相机距离不同时渲染顺序是不同的,移动相机时有时候图中的半透明方块(实际上不透明,只是强行混合了)先渲染了,柱子在渲染的时候深度检测就无法通过,被舍弃掉。就看不到柱子了,而如果先渲染的柱子再渲染的半透明方块,那效果就是正常的。

改变了相机位置

        然后此时我们关闭深度写入。柱子就不会被遮挡了,但是又有了新的问题,就是柱子显示在了半透明方块之前。还是之前一样的问题,现在半透明方块和柱子都是Geometry的渲染队列,他们的渲染顺序是不固定的,就是有可能先渲染了半透明方块再渲染柱子,就出现了下图中的情况,所以半透明物体必须要在不透明物体之后渲染。

        

        我们将renderQueue设置为Transparent就可以获得正确的结果。

        -----------------------------------------------------------------------------------

        然后还要注意的是:半透明物体之间的排序。

        半透明物体设置renderqueu为Transparent后好像开不开深度写入效果都是一样的:

        而且相机随意移动位置也不会闪烁,其实这是因为unity渲染半透明物体的时候固定从远到近渲染,所以不会出现上面的闪烁问题。

        但是其实如果开启了深度写入就还是会有问题。假设有两个物体,当某个物体在最后显示并且深度检测失败的话片元就会被剔除:

        

所以还是要关闭深度写入才能获得正确的结果:

        

        12.4.开启深度写入的半透明效果

        我们在12.2中实现了基础的半透明效果,但是这个效果视觉方面有一些问题,就是模型内部片元的深度效果不正确,因为我们关闭了深度写入,透明模型内部无法正确排序。

        很简单,使用两个pass,一个pass负责深度写入,一个pass负责输出颜色。

        

        ------------------------------------------------------------------------------------------------------

        笔记做到这里就没做了,代码写shader还是费劲,我转到图形化shader了,因为内置管线也支持图形化shader了

        

        

        

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐