Unity ShaderLab-表面着色器

表面着色器结构

表面着色器不需要Pass,直接写在SubShader,实际上是取代了CGPROGRAM...ENDCG之间的顶点着色器的代码片段。

表面着色器会根据添加的编译指令自动生成多个Pass

表面着色器编译语法:

     #pragma surface surfaceFunction lightModel [optionalparams]


(1)surface:声明所使用的Shader是表面着色器。
(2)surfaceFunction:声明表面着色器的函数名称,被称为表面函数,一般使用surf作为表面函数的名称。
(3)lightModel:声明所使用的光照模型。Unity提供了四种光照模型,分别为非物理光照模型:Lambert和BlinnPhong;物理光照模型:Standard和StandardSpecular。
(4)[optionalparams]:其他的可选参数


    使用的过程中,首先需要定义一个输入结构体Input,通过结构体获取所有需要的数据(例如纹理坐标、法线向量等),然后传入表面函数中进行计算,最后将计算结果输出到结构体SurfaceOutput中,输出结构体中包含了物体的基本属性,例如Albedo、Normal、Specular等属性(可视化shader 也是表面着色器,最终一个输出)。剩下的工作Unity会自动完成。

表面着色器的透明

表面着色器的阴影

代码生成选项


因为表面着色器会自动生成Pass,编译各种版本的Shdaer,所以可以使用指令禁止某些代码生成,避免产生更多的shader变体,降低消耗

表面函数

void surf (Input IN,inout SurfaceOutput o)
{
    表面函数代码
}
输入参数 in 可以省略,Input表示预先定义好的结构体,unity提供的结构体有
输出结构体 inout,既有输入同时也有输出
不同渲染模型,输出结构体不同,Lambert光照模型,BlinnPhong光照模型,在表面函数中都可以使用SurfaceOutput
struct SurfaceOutput
{
    fixed3 Albedo;//颜色,漫反射
    fixed3 Normal;//法线
    fixed3 Emission;//自发光
    half Specular; //镜面反射
    fixed Gloss;//镜面反射强度
    fixed Alpha;//透明通道
}
除此之外,unity还有两种基于物理的光照模型(1)Standard光照模型:适用于金属工作流,使用SurfaceOutputStandard表面结构体。(2)StandardSpecular光照模型:适用于高光工作流,使用SurfaceOutputStandardSpecular表面结构体。

表面着色器Lambert

Shader "Custom/Lambert"
{
    Properties
    {
        _Color ("Color", Color) = (1,1,1,1)
        _MainTex ("Albedo (RGB)", 2D) = "white" {}
        
    }
    SubShader
    {
        CGPROGRAM
        #pragma surface surf Lambert //声明表面着色器 使用Lambert模型

        struct Input 
        {
            float2 uv_MainTex;
        };

        sampler2D _MainTex;
        fixed4 _Color;

        // 表面函数
        void surf(Input IN,inout SurfaceOutput o) 
        {
            fixed4 c=tex2D(_MainTex,IN.uv_MainTex)*_Color;

            o.Albedo=c.rgb;//根据lambert 模型需要输出的结构体,设置里面的值
        }
        ENDCG

    }
    FallBack "Diffuse"
}
可以看到表面着色器的lambert 自带阴影,且效果更好,因为表面着色器的Lambert unity自己处理了很多事情
法线贴图

在表面函数中对法线贴图进行采样,与其他贴图不同的是,此处并没有直接使用采样过后的法线贴图,而是通过UnpackNormal()函数又对采样过后的法线贴图进行了解包(Unpack)操作,然后才指定给表面属性结构体的Normal变量进行输出。

那什么是解包呢?像素颜色在程序中的数值范围为[0,1],而法线是有正反方向的,标准化的法线向量每个分量的数值区间为[-1,1]。因此在高低模烘焙法线贴图的时候,为了使像素能够存储下负数区间的数值,需要执行打包(Pack)操作,打包会将数值的区间从[-1,1]映射到[0,1],而解包其实就是打包操作的逆向操作,将数值区间从[0,1]重新映射回[-1,1]

打包操作:PackNormal=0.5·UnpackNormal+0.5
解包操作:PackNormal=0.5·UnpackNormal+0.5

表面着色器中的顶点修改

既然表面着色器是对顶点-片段着色器的封装,那么在表面着色器中也是可以修改顶点的

表面着色器计算流程与顶点片段着色器是一样的
在编译指令里添加vertex:functionName指令 即可进行顶点操作,appdata_full 既是顶点输入也是输出
顶点法线膨胀
Shader "Custom/膨胀"
{
    Properties
    {
        _Expansion("Expansion",Range(0,0.1))=0
        _MainTex ("Albedo (RGB)", 2D) = "white" {}
        
    }
    SubShader
    {
        CGPROGRAM
        //声明表面着色器 使用Lambert模型,声明顶点修改函数
        #pragma surface surf Lambert vertex:vert 
        

        struct Input 
        {
            float2 uv_MainTex;
        };

        sampler2D _MainTex;
        fixed _Expansion;

        // 顶点函数
        void vert(inout appdata_full v) //appdata_full 既是顶点输入,也是输出
        {
            v.vertex.xyz+=v.normal*_Expansion;// 将顶点的xyz 加上法线乘以膨胀系数的值,结果是每个顶点向法线方向移动一段距离,即膨胀效果
        }

        // 表面函数
        void surf(Input IN,inout SurfaceOutput o) 
        {
            o.Albedo=tex2D(_MainTex,IN.uv_MainTex).rgb;//根据lambert 模型需要输出的结构体,设置里面的值
        }
        ENDCG

    }
    FallBack "Diffuse"
}

曲面细分

在blender 中可以选择表面细分修改器对模型进行细分,使其更精细,而在unity中,也可以手动控制细分,而不必所有资源全是细分好的,
这样在untiy中手动控制细分,好处是,可以减小包体大小

unity有5中曲面细分算法

固定数量的曲面细分。

这种细分方式仅在模型网格比较均匀的情况下效果才会比较好,而在很多情况下效果会非常糟。
适合当带顶点数量不够时,而又需要顶点偏移的情况,适合一些带顶点偏移效果的特效情况使用例如:平面变成海洋,平面变成山脉,均匀布线的人体等

基于边长的曲面细分

适合不均匀布线的模型使用基于边长的布线,会根据布线的顶点密集程度,去进行细分。避免固定数量,较密集细分过于密集,而稀疏缺没有足够的细分

视锥剔除曲面细分

属于性能考量:在基于边长细分的基础上,在视锥内的才进行细分,超出的顶点会被剔除,节约性能使用UnityEdgeLengthBasedTessCull()

基于距离的曲面细分

属于性能考量:当摄像机足够接近才进行细分,当摄像机距离很远不细分(类似于模型的Lod,近用高模,远用低模)

Phong曲面细分

会使模型更圆润光滑,新增顶点沿原始法线方向移动一段距离,blender Zb中的细分就是此类细分,
前面的固定和基于边长细分,都是为了能够适配高度图,使模型(多数是面片)有足够的顶点进行顶点偏移,如果单纯想要模型更光滑,Phong曲面细分合适
使用方法:需要结合固定/基于边长 曲面细分,因为单独使用Phong曲面细分是没有效果的(猜测是固定/基于边长 能够增加顶点,而Phong曲面细分 是在增加了顶点之后,进行顶点偏移,所以需要配合固定/基于边长 曲面细分)
固定数量-Phong细分参考代码
Shader "Custom/Phong_固定曲面细分"
{
    Properties
    {
        
        _MainTex ("Albedo (RGB)", 2D) = "white" {}
        _Tessellation("Tessellation",Range(1,32))=1
        _Phong("Phong_Tessellation",Range(0,1))=0.2
    }
    SubShader
    {
        CGPROGRAM
        #include "Tessellation.cginc"
        half _Tessellation;
        fixed _Phong;
        //声明曲面细分函数  和Phong 细分编译指令
        #pragma surface surf Lambert tessellate:tessellation tessphong:_Phong

        float4 tessellation()
        {
            return _Tessellation;
        }
        struct Input 
        {
            float2 uv_MainTex;
            float2 uv_NormalMap;
        };

        sampler2D _MainTex;
        
        // 表面函数
        void surf(Input IN,inout SurfaceOutput o) 
        {
            o.Albedo=tex2D(_MainTex,IN.uv_MainTex).rgb;//根据lambert 模型需要输出的结构体,设置里面的值
        }
        ENDCG

    }
    FallBack "Diffuse"
}
测试模型必须具有光滑法线信息才能突出Phong细分,否则不会出现圆润效果

在blender建一个猴头,不进行光滑。直接导出则不具有 phong细分的圆润效果

在blender建一个猴头,光滑,导出,然后使用Phong-固定数量细分,会发现模型沿法线方向膨胀,但是细分效果并不好

可以看到效果比如blender里面的细分差距太远了。。。
Phong_基于边长曲面细分
Shader "Custom/Phong_基于边长曲面细分"
{
    Properties
    {
        
        _MainTex ("Albedo (RGB)", 2D) = "white" {}
        _EdgeLength("_EdgeLength",Range(1,32))=1
        _Phong("Phong_Tessellation",Range(0,1))=0.2
    }
    SubShader
    {
        CGPROGRAM
        
        float _EdgeLength;
        fixed _Phong;
        //声明曲面细分函数  和Phong 细分编译指令
        #pragma surface surf Lambert tessellate:tessellateEdge  tessphong:_Phong
        #include "Tessellation.cginc"
        
        float4 tessellateEdge(appdata_full v0,appdata_full v1,appdata_full v2)
        {
            return UnityEdgeLengthBasedTess(v0.vertex,v1.vertex,v2.vertex,_EdgeLength);//传入的是三角形的三个顶点坐标,三角形在屏幕上的长度
        }

        struct Input 
        {
            float2 uv_MainTex;
            float2 uv_NormalMap;
        };

        sampler2D _MainTex;
        
        // 表面函数
        void surf(Input IN,inout SurfaceOutput o) 
        {
            o.Albedo=tex2D(_MainTex,IN.uv_MainTex).rgb;//根据lambert 模型需要输出的结构体,设置里面的值


        }
        ENDCG

    }
    FallBack "Diffuse"
}
效果比基于固定略好,但也没好多少

透明效果

与顶点片段着色器一样,具有混合透明,透明测试,模板测试三种


再次声明unity的绘制顺序

Unity将不透明模型的渲染顺序设定为:近处的物体优先绘制,然后再绘制远处的物体。
半透明物体在所有不透明物体绘制完成之后再进行绘制。

Unity将半透明队列的渲染顺序设定为:远处的物体优先绘制,然后再绘制近处的物体。

混合透明

Shader "Custom/surf混合透明"
{
    Properties
    {
        
        _MainTex ("Albedo (RGB)", 2D) = "white" {}
        
        _Color("Color",Color)=(1,1,1,1)
    }
    SubShader
    {

        Tags{"Queue"="Transparent"}
        CGPROGRAM

        #pragma surface surf Lambert alpha
        struct Input 
        {
            float2 uv_MainTex;
            
        };

        sampler2D _MainTex;
        fixed4 _Color;

        // 表面函数
        void surf(Input IN,inout SurfaceOutput o) 
        {
            o.Albedo=tex2D(_MainTex,IN.uv_MainTex).rgb*_Color.rgb;//根据lambert 模型需要输出的结构体,设置颜色
            o.Alpha=tex2D(_MainTex,IN.uv_MainTex).a*_Color.a;//采样贴图alpha通道,设置输出透明度

        }
        ENDCG

    }
    FallBack "Diffuse"
}

透明测试

Shader "Custom/surf透明测试"
{
    Properties
    {
        
        _MainTex ("Albedo (RGB)", 2D) = "white" {}
        
        _AlphaTest("Color",Range(0,1))=0
    }
    SubShader
    {

        Tags{"Queue"="AlphaTest"}
        CGPROGRAM
        //开启透明测试,_AlphaTest控制透明度
        //开启阴影修正(透明部分不产生阴影)
        #pragma surface surf Lambert alphatest:_AlphaTest addshadow

        struct Input 
        {
            float2 uv_MainTex;
            
        };

        sampler2D _MainTex;
        // 表面函数
        void surf(Input IN,inout SurfaceOutput o) 
        {
            o.Albedo=tex2D(_MainTex,IN.uv_MainTex).rgb;//根据lambert 模型需要输出的结构体,设置颜色
            o.Alpha=tex2D(_MainTex,IN.uv_MainTex).a;//采样贴图alpha通道,设置输出透明度

        }
        ENDCG

    }
    FallBack "Diffuse"
}

模板测试

模板测试的使用语法
(1)Ref referenceValue:用来与缓存中已经存在的模板值进行比较的数值,被称为参照值,当比较之后符合某些设定条件,这个数值可以被写进缓存,数值的范围为0~255的整数。

(2)ReadMask readMask:是一个范围为0~255的整数,8位二进制11111111。当读取参照值与模板值可以使用的时候,模板会指定哪些位的数值可以读取,默认为255,也就是所有位都可以读取。
(3)WriteMask writeMask:同样也是8位二进制11111111,当往缓存中写入的时候可以使用,模板会指定哪些位的数值允许写入缓存,例如:当指定WriteMask为0,表示的是没有数值会被写入缓存,而不是将0写入。默认位数为255,也就是所有位都允许写入。

(4)Comp comparisonFunction:将参照值与缓存中的模板数值进行比较的方法,默认为always。
(5)Pass stencilOperation:如果模板测试和深度测试都通过,缓存中的模板值如何处理,默认为keep。

(6)Fail stencilOperation:如果模板测试没有通过,缓存中的模板值如何处理,默认为keep。
(7)ZFail stencilOperation:如果模板测试通过,但是深度测试没有通过,缓存中的模板值如何处理,默认为keep。
利用模板测试:做一个透明融合效果
Shader "Custom/surf模板测试"
{
    Properties
    {
        _Color("Color",Color)=(1,1,1,1)
    }
    SubShader
    {
 //选择AlphaTest 先绘制近处,远处模板值!=1才绘制,等于1的重叠部分不绘制
//从而达到无接缝透明效果(接缝处 模板值都为1,远处无法通过模板测试)
        Tags{"Queue"="AlphaTest"}
       
        //设置模板测试状态
        Stencil
        {
            Ref 1
            Comp NotEqual
            Pass replace
        }

        CGPROGRAM

        #pragma surface surf Lambert alpha
        struct Input 
        {
            float2 uv_MainTex;
        };
        fixed4 _Color;
        // 表面函数
        void surf(Input IN,inout SurfaceOutput o) 
        {
            o.Albedo=_Color.rgb;//根据lambert 模型需要输出的结构体,设置颜色
            o.Alpha=_Color.a;//采样贴图alpha通道,设置输出透明度
        }
        ENDCG

    }
    FallBack "Diffuse"
}
类似水滴融进大海,不出现透明边缘接缝,
原理:先绘制近处物体,写入深度1,后续模板值不等即未被遮挡才绘制,遮挡部分模板值为1,不绘制,因此不出现透明重叠接缝
如果是Transparent 可以不被剪裁,但是Transparent 先绘制远处物体,再绘制近处,效果错误
结论:
模板测试只适合做 材质重叠效果,不适合做独立效果,因为模板测试需要与其余材质模板值 对比

选择相机的延迟渲染可以解决 剪裁问题(相机默认前向渲染),但是半透明效果会大打折扣

延迟渲染
前向渲染

留下评论