关于Unity的表面着色器的学习

来源:互联网 发布:江苏域名备案查询 编辑:程序博客网 时间:2024/05/21 09:32

前言

由于刚学习Shader不久,并且是断断续续。外加上需要对Unity自带的一个叫“Mobile/Diffuse”的shader进行扩充。所以在研究了一下以后进行一个记录,方便以后复习时使用。为了使更多的人可以简单的写shader,表面着色器将vert/frag封装了起来。但对于一个刚学习shader的二把刀来讲,如果想对表面着色器写的shader进行修改也只能去了解表面着色器。因为一开始时不知道如何查看表面着色器所对应的vert/frag版本源码。后来知道是在Inspector面板中相应的表面着色器shader有一个“Show generated code”选项,然而进去后发现因为太过复杂还是看不懂……

在熟悉了基本的使用方法后就会发现,确实表面着色器还是很好用的。只需要指定相应的光照模型(系统自带的或者由开发者自定义都可以),之后只需要传入一些贴图或者数值就可以完成一个效果不错的shader了。

基础代码展示

以下就使用“Mobile/Diffuse”为示例:
Shader "Mobile/Diffuse"{    Properties{        _MainTex("Base(RGB)", 2D) = "white"{}    }    SubShader{        Tags { "RenderType"="Opaque" }        LOD 150        CGPROGRAM        //指定了着色方法的名字为surf        //指定光照模型为Lambert        //可选的调整部分:        //       noforwardadd(不适用正向渲染额外的通道)        #pragma surface surf Lambert noforwardadd        sampler2D _MainTex;        //自定义的需要参与计算的数据        struct Input{            //贴图的uv值            float2 uv_MainTex;        };        //SurfaceOutput是输出结构        void surf(Input IN, inout SurfaceOutput o){            fixed4 c = tex2D(_MainTex, IN.uv_MainTex);            //输出的颜色是贴图中相应的颜色            o.Albedo = c.rgb;            //输出的透明度是贴图中相应的透明度            o.Alpha = c.a;        }        ENDCG    }    Fallback "Mobile/VertexLit"}   //end shader
通过上面的代码已经可以对表面着色器有一个初步的认识了。表面着色器的代码同样是位于CGPROGRAM和ENDCG中间,但是不同的是表面着色器的代码必须写在SubShader中,而不是Pass中。表面着色器会自动将其编译到多个通道中,但是如果在书写时就将其置于Pass中则会编译报错。

表面着色器的流水线

表面着色器同样是通过顶点(vertex)/片元(fragment)的方式来完成渲染任务的。所以最大的框架还是 vertex -> fragment只不过在fragment部分又细分出了表面处理函数surf,光照模型函数lightModel,和最终的颜色值处理函数ColorFunction。vertexFunction -> surf -> lightModel -> ColorFunction其中只有surf部分是在写shader时必须有的,其他均为可选部分。lightModel可以使用Unity默认的种类,也可以使用用户自己写的光照模型。其中Input结构体传入surf,并在经过处理后,传出一个SurfaceOutput结构体。SurfaceOutput结构体会传入光照计算的lightModel部分。

一个完整的表面着色器大体由以下几部分构成:

1. 标签

此部分和vert/frag着色器类似,硬件将会通过不同的标签来决定什么时候调用该着色器。例如:
//渲染非透明物体时使用Tags { "RenderType" = "Opaque" }//渲染带有透明效果的物体时使用Tags { "RenderType" = "Transparent" }

2. 表面着色器编译指令

#pragma surface surfacefunction lightModel [optionalparams]
通过这个一般的格式可以看出,surfacefunction和lightModel这两部分是必须有的。其中surfacefunction是指表面着色函数的名字,通常都会写成surf。lightModel是用来指定使用的光照模型。系统默认的光照模型有以下几种:基于物理的Standard和StandardSpecular模型。没有基于物理的比较传统的Lambert和BlinnPhong模型。如果想要自定义光照模型,需要一个以Lighting开头与名字组合在一起的合乎规范的函数。
//正向渲染路径中使用的光照模型,不会取决于视图方向(view direction)half4 LightName(SurfaceOutput s, half3 lightDir, half atten){    //具体的光照模型}//正向渲染路径中使用的光照模型,取决于视图的方向(view direction)half4 LightingName(SurfaceOutput s, half3 lightDir, half3 viewDir, half atten){    //具体的光照模型}//延时光照路径中使用half4 LightingName_PrePass(SurfaceOutput s, half4 light){    //具体的光照模型}//当定向解码光照贴图用于前向/延迟光照中的光照函数,或类似的函数中时,需要进行定制(此部分还不太明白是什么意思)//使用不依赖于视图方向的光照模式half4 LightingName_DirLightmap(SurfaceOutput s, fixed4 color fixed4 scale, bool surfFuncWritesNormal){    //具体的光照模型}//使用依赖于视图方向的光照模式half4 LightingName_DirLightmap(SurfaceOutput s, fixed4 color,                                 fixed4 scale, half3 viewDir,                                 bool surfFuncWritesNormal, out half3 specColor){    //具体的光照模型}
以上方法中的Name部分都需要替换为自定义的光照模型的名字。其中,光照函数中的atten参数经过查询,可能是代表着光照衰减的系数。并且可以在很多使用表面着色器的Shader中看到会有atten * 2的用法,这个原因似乎是个历史遗留问题导致的。经过在网上的初步查询,可能是由于早期使用固定管线着色器时。由于内部原因,为了增大光强度,所以采用了“*2”的解决方法,而现在虽然已经不再需要这样了。但是由于还有很多Shader可能是基于此种方式编写的。为了使这些Shader可以正常工作,而不是突然间亮度变为两倍,所以依然保留了原先的这个问题。

optionalparams部分有很多种选项,可以同时应用产生效果,只需要将相应的选项间用空格间隔开就可以了。以下为选项中的一些,更多可参考官方文档。
  • exclude_path:deferred,forward,prepass- 不要为已经给定的渲染路径生成Pass。
  • noshadow-不支持阴影接受。
  • noambient-不适用于环境光或者light probes。
  • novertexlights-不适用于正向渲染的任何light probes或者逐顶点光照。
  • nolightmap-不支持光照贴图。
  • nodynlightmap-不支持动态的全局光照。
  • nodirlightmap-shader不支持定向光照贴图。
  • nofog-不支持所有的内置无效。
  • nometa-不产生”meta”通道,通常用于lightmapping和动态全局照明去提取表面信息。
  • noforwardadd-不适用正向渲染额外的通道。
  • softvegetation-使表面着色器仅仅在软植被上的时候被渲染。
  • interpolateview-计算在顶点着色器上视觉方向并且进行插值来代替在像素着色器中计算。
  • halfasview-通过half-direction向量进入光照函数来代替视角方向,half-direction会被逐顶点计算并归一化。
  • dualforward-在正向渲染路径中使用dual lightmaps。
//一个自定义的光照模型#pragma surface surf MyLightModel//通过参数可以看出,自定义的光照模型接受了表面着色器方法中的输出参数half4 LightingMyLightModel(SurfaceOutput s, half3 lightDir, half3 viewDir, half atten){    half3 h = normalize(lightDir + viewDir);        half diff = max(0, dot(s.Normal, lightDir));    float nh = max(0, dot(s.Normal, h));    //此处为高光的处理,光照模型会受到表面着色器方法中传递过来的高光值得影响    float spec = pow(nh, 48.0) * s.Specular;    //经过光照模型处理后输出的颜色    half4 c;    c.rgb = (s.Albedo * _LightColor0.rgb * diff + _LightColor0.rgb * spec ) * atten;    c.a = s.Alpha;    return c;}

3. Input结构体

Input结构体通常负责将纹理的坐标传入表面着色器方法中。纹理坐标的命名必须以uv开头。例如:纹理_MainTex的纹理坐标为uv_MainTex。如果使用第二个纹理坐标则需要以uv2为开头进行命名。此外,还有一些数值可以通过Input结构进行传递:
  • float3 viewDir-含有视角的方向,用于计算视差效果、边缘光照等。
  • float4加上COLOR语义-将会包含经过差值的逐顶点颜色。
  • float4 screenPos-包含屏幕空间位置,用于进行反射或屏幕空间特效的处理。
  • float3 worldPos-包含世界坐标系的位置信息。
  • float3 worldRefl-如果表面着色器方法中没有赋值o.Normal,worldRefl将会包含世界坐标系下的反射向量。
  • flaot3 worldNormal-如果表面着色器方法中没有赋值o.Normal,worldNormal将会包含世界坐标系下的法线向量。
  • float3 worldRefl; INTERNAL_DATA-如果表面着色器方法中没有赋值o.Normal,将会包含世界坐标系下的反射向量。
    此值需要使用方法WorldReflectionVector(IN, o.Normal)对法线贴图进行逐像素的获取。
  • float3 worldNormal; INTERNAL_DATA-如果表面着色器方法中没有赋值o.Normal,将会包含世界坐标系下的法线向量。
    此值需要使用方法WorldNormalVector(IN, o.Normal)对法线贴图进行逐像素的获取。

4.表面着色器输出结构

在定义完光照模型和输入结构体后,就是表面着色器的主体——surf方法了。在surf方法的参数中需要定义一个表面输出结构。经过surf处理过后的信息都将会通过这个输出结构传入到下一个过程——光照模型,进行进一步的处理。表面着色器的输出结构和光照模型的选择有关。通常情况下,当使用Lambert和BlinnPhong光照模型时,输出结构为标准的SurfaceOutput:
struct SurfaceOutput{    fixed3 Albedo;      //漫反射颜色    fiexd3 Normal;      //法线向量    fixed3 Emission;    //自发光颜色    half Specular;      //镜面反射度,Range(0,1)    fixed Gloss;        //光泽度    fixed Alpha;        //透明度};
当光照模型为Standard时,使用SurfaceOutputStandard。此时Specular和Gloss被替换成了Metallic、Smoothness、Occlusion。
struct SurfaceOutputStandard{    fixed3 Albedo;    fixed3 Normal;    half3 Emission;    half Metallic;      //金属性,0=非金属,1=金属    half Smoothness;    //平滑度,0=粗糙,1=平滑    half Occlusion;     //遮挡率,默认值为1    fixed Alpha;};
当光照模型为StandardSpecular时,使用SurfaceOutStandardSpecular。
struct SurfaceOutputStandardSpecular{    fixed3 Albedo;    fixed3 Specular;    fixed3 Normal;    half3 Emission;    half Smoothness;    half Occlusion;    fixed Alpha;};
以上即为表面着色器的基本知识信息。

对基础代码的改造

接下来对"Mobile/Diffuse"进行魔改造:首先是加入边缘光Rim效果:
//改变了shader的名字Shader "Everness/Diffuse_Rim"{    Properties{        //纹理颜色        _MainTex("MainTex", 2D) = "white"{}        //边缘光颜色        _RimColor("Rim Color", Color) = (1, 1, 1, 1)        //边缘光强度        _RimPower("Rim Power", Range(0.5, 8.0)) = 3.0    }    SubShader{        //渲染不透明物体        Tags { "RenderType"="Opaque" }        CGPROGRAM        //使用兰伯特光照模型        #pragma surface surf Lambert        struct Input{            //纹理贴图坐标            float2 uv_MainTex;            //观察方向            float3 viewDir;        };        sampler2D _MainTex;  //纹理        float4 _RimColor;    //边缘颜色        float _RimPower;     //边缘颜色强度        void surf(Input IN, inout SurfaceOutput o){            //获取纹理贴图信息            fixed4 mainTex = tex2D(_MainTex, IN.uv_MainTex);            //表面反射颜色为纹理颜色            o.Albedo = mainTex.rgb;            //边缘颜色及强度系数计算            float rim = 1.0 - saturate(dot(normalize(IN.viewDir), o.Normal));            o.Emission = _RimColor.rgb * pow(rim, _RimPower);        }        ENDCG    }    //备用着色器    Fallback "Mobile/VertexLit"}//end shader
使用此shader可以让物体周边浮现一层高光一样的效果,即使是非受光面也会有边缘光的效果。

更进一步:

现在我希望进一步改造上面的shader,从以下3方面着手。
  • 表面颜色可以收到一个自定义颜色的影响。
  • 添加法线贴图。
  • 对边缘光进行更细致的调整。
Shader "Everness/Diffuse_Rim"{    Properties{        _Color("MainColor", Color) = (1, 1, 1, 1)        _MainTex("MainTex", 2D) = "white"{}        _BumpMap("BumpMap", 2D) = "white"{}        _RimColor("Rim Color", Color) = (1, 1, 1, 1)        _RimPower("Rim Power", Range(0.5, 8.0)) = 3.0        //新增加的对边缘光控制用的变量        _RimDrag("Rim Drag", Range(0.5, 3)) = 2    }    SubShader{        Tags { "RenderType" = "Opaque" }        CGPROGRAM        #pragma surface surf Lambert        struct Input{            float2 uv_MainTex;            float2 uv_BumpMap;            float3 viewDir;        };        float4 _Color;        sampler2D _MainTex;        sampler2D _BumpMap;        float4 _RimColor;        float _RimPower;        float _RimDrag;        void surf(Input IN, inout SurfaceOutput o){            fixed4 mainTex = tex2D(_MainTex, IN.uv_MainTex);            //表面反射颜色为纹理颜色和自定义颜色的混合            o.Albedo = mainTex.rgb * _Color;            //表面法线为凹凸纹理的颜色            o.Normal = UnpackNormal(tex2D(_BumpMap, IN.uv_BumpMap));            //处理边缘光            float rim = 1.0 - saturate(dot(normalize(IN.viewDir), o.Normal);            //更加细致的控制边缘光            o.Emission = _RimColor.rgb * pow(rim, _RimPower)/_RimDrag;        }        ENDCG    }    Fallback "Diffuse"}//end shader

更进一步:

对以上shader进一步改造,从以下2方面入手。
  • 使用自定义的光照模型。
  • 添加一张新的贴图,并将高光信息存储在此贴图的r通道,使shader对象的高光效果受到这张新贴图的影响。
Shader "Everness/Specular_Rim"{    Properties{        _Color("MainColor", Color) = (1, 1, 1, 1)        _MainTex("MainTex", 2D) = "white"{}        _BumpMap("BumpMap", 2D) = "white"{}        _SpecualrMap("SpecularMap", 2D) = "white"{}        _RimColor("Rim Color", Color) = (1, 1, 1, 1)        _RimPower("Rim Power", Range(0.5, 8.0)) = 3.0        _RimDrag("Rim Drag", Range(0.5, 3)) = 2.0    }    SubShader{        Tags { "RenderType" = "Opaque" }        CGPROGRAM        #pragma surface surf MyLightModel        //一个自定义的光照模型        half4 LightingMyLightModel(SurfaceOutput s, half3 lightDir, half3 viewDir, half atten){            half3 h = normalize(lightDir + viewDir);            half diff = max(0, dot(s.Normal, lightDir));            float nh = max(0, dot(s.Normal, h));            //通过表面着色器中的高光值对最终的效果进行干涉            float spec = pow(nh, 48.0) * s.Specular;            half4 c;            c.rgb = (s.Albedo * _LightColor0.rgb * diff + _LightColor0.rgb * spec) * atten;            c.a = s.Alpha;            return c;        }        struct Input{            float2 uv_MainTex;            float2 uv_BumpMap;            float2 uv_SpecularMap;            float3 viewDir;        }        float4 _Color;        sampler2D _MainTex;        sampler2D _BumpMap;        sampler2D _SpecualrMap;        float4 _RimColor;        float _RimPower;        float _RimDrag;        void surf(Inpu IN, inout SurfaceOutput o){            fixed4 mainTex = tex2D(_MainTex, IN.uv_MainTex);            o.Albedo = mainTex.rgb * _Color;            o.Normal = UnpackNormal(tex2D(_BumpMap, IN.uv_BumpMap));            //对高光值进行控制            fixed4 specularMap = tex2D(_SpecualrMap, IN.uv_SpecularMap);            o.Specular = specularMap.r;            o.Gloss = mainTex.a;            float rim = 1.0 - saturate(dot(normalize(IN.viewDir), o.Normal));            o.Emission = _RimColor.rgb * pow(rim, _RimPower)/_RimDrag;        }        ENDCG    }    Fallback "Diffuse"}//end shader

更进一步:

经过测试发现和顶点/片元着色器可以并存多个Pass不同,表面着色器的surf方法只能有一个。即使写了多个surf方法,最后也会进行相互间的覆盖,起作用的会是最后一个surf方法。另外,可以在surf方法所在的CGPROGRAM/ENDCG之前添加额外的Pass过程。以下便使用此种形式为上面的shader增加一个描边效果。虽然不知道正常的写法是否是这样,但总之效果还是有的。
Shader "Everness/Outline_Rim"{    Properties{        _Color("MainColor", Color) = (1, 1, 1, 1)        _MainTex("MainTex", 2D) = "white"{}        _BumpMap("BumpMap", 2D) = "white"{}        _SpecualrMap("SpecularMap", 2D) = "white"{}        _RimColor("Rim Color", Color) = (1, 1, 1, 1)        _RimPower("Rim Power", Range(0.5, 8.0)) = 3.0        _RimDrag("Rim Drag", Range(0.5, 3)) = 2.0        _OutlineColor("Outline Color", Color) = (0, 0, 0, 1)        _OutlineWidth("Outline Width", Range(0, 1)) = 0.005    }    SubShader{        Tags { "RenderType" = "Opaque" }        //描边Pass        Pass{            Cull Front            Blend SrcAlpha OneMinusSrcAlpha            CGPROGRAM            #pragma vertex vert            #pragma fragment frag            #include "UnityCG.cginc"            float4 _OutlineColor;            float _OutlineWidth;            struct v2f{                float4 pos : POSITION;                float4 color : COLOR;            };            v2f vert(appdata_base v){                v2f o;                o.pos = mul(UNITY_MATRIX_MVP, v.vertex);                float3 normal = mul((float3x3)UNITY_MATRIX_IT_MV, v.normal);                float2 offset = TransformViewToProjection(normal.xy);                o.pos.xy += offset * _OutlineWidth;                o.color = _OutlineColor;                return o;            }            fixed4 frag(v2f i) : SV_Target{                return i.color;            }            ENDCG        }//end pass        //表面着色器部分        CGPROGRAM        #pragma surface surf MyLightModel        //一个自定义的光照模型        half4 LightingMyLightModel(SurfaceOutput s, half3 lightDir, half3 viewDir, half atten){            half3 h = normalize(lightDir + viewDir);            half diff = max(0, dot(s.Normal, lightDir));            float nh = max(0, dot(s.Normal, h));            //通过表面着色器中的高光值对最终的效果进行干涉            float spec = pow(nh, 48.0) * s.Specular;            half4 c;            c.rgb = (s.Albedo * _LightColor0.rgb * diff + _LightColor0.rgb * spec) * atten;            c.a = s.Alpha;            return c;        }        struct Input{            float2 uv_MainTex;            float2 uv_BumpMap;            float2 uv_SpecularMap;            float3 viewDir;        }        float4 _Color;        sampler2D _MainTex;        sampler2D _BumpMap;        sampler2D _SpecualrMap;        float4 _RimColor;        float _RimPower;        float _RimDrag;        void surf(Inpu IN, inout SurfaceOutput o){            fixed4 mainTex = tex2D(_MainTex, IN.uv_MainTex);            o.Albedo = mainTex.rgb * _Color;            o.Normal = UnpackNormal(tex2D(_BumpMap, IN.uv_BumpMap));            //对高光值进行控制            fixed4 specularMap = tex2D(_SpecualrMap, IN.uv_SpecularMap);            o.Specular = specularMap.r;            o.Gloss = mainTex.a;            float rim = 1.0 - saturate(dot(normalize(IN.viewDir), o.Normal));            o.Emission = _RimColor.rgb * pow(rim, _RimPower)/_RimDrag;        }        ENDCG    }    Fallback "Diffuse"}//end shader
经过这几次的修改,一个有着边缘光效、描边效果的surface shader就完成了,也对表面着色器有了一个初步的了解。虽然表面着色器只需要简单的一些参数赋值即可生成很好的光照效果shader,但相对于顶点/片元着色器而言还是有很多不足的。其中之一就是耗能问题,表面着色器的效率没有顶点/片元着色器高。另外就是所能制作的效果上,如果需要更加独特和自定义的效果还是需要进行顶点和片元着色器的编写,而表面着色器终究有所局限。以上便是目前对表面着色器的总结,更加具体的内容也可以参考Unity的官方文档,另外如果以后发现有存在问题的部分也需要重新进行修改。
0 0
原创粉丝点击