《Unity Shader入门精要》学习--扩展篇--Unity的表面着色器

发布于:2025-08-17 ⋅ 阅读:(11) ⋅ 点赞:(0)

·本专栏文章记录笔者阅读学习《Unity Shader入门精要》的感想与笔记,方便日后复习与查找


目录

一.什么是表面着色器

1.表面着色器的定义

2.表面着色器的简单例子

二.表面着色器的编译指令

1.指定表面着色器和光照函数

2.表面函数

3.光照函数

4.其他参数

4.1.自定义的顶点和颜色修改函数:

4.2.阴影

4.3.透明度混合和透明度测试

4.4.光照

4.5.控制生成的代码

三.表面着色器的两个结构体

1.数据来源:Input结构体

2.表面属性:SurfaceOutput结构体

四.表面着色器的转化规律

五.表面着色器的实例

1.代码实现与效果

2.查看生成的顶点/片元着色器代码

2.1.Pass设置与编译指令

2.2.一些自动生成的注释

2.3.一些辅助计算的宏

2.4.复制过来的表面着色器的函数与结构体

2.5.顶点着色器到片元着色器的插值结构体

2.6.生成顶点着色器

2.7.生成片元着色器

六.表面着色器的缺点和使用选择

七.总结


一.什么是表面着色器

1.表面着色器的定义

表面着色器是在顶点/片元着色器的层面上的更高一层的抽象和封装。它试图把渲染的可控制过程变得更加容易让人类理解。

大致分为三个部分:

  • 表面着色器:用于定义模型表面的反射率,法线和高光等
  • 光照模型:用于选择是用哪种光照模型(兰伯特模型等)进行光照计算
  • 光照着色器:用于计算光照衰减,阴影等值

这样划分的好处是:①我们基本上只用和表面着色器打交道,而光照模型和光照着色器一般不用我们再去操心

2.表面着色器的简单例子

Shader "Custom/BumpSpecular2"
{
    Properties
    {
        _Color ("Color", Color) = (1,1,1,1)
        _MainTex ("Albedo (RGB)", 2D) = "white" {}
        _BumpMap ("Normalmap", 2D) = "bump" {}
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 300

        //表面着色器可以直接在SubShader中写的
        CGPROGRAM
         //指定表面着色器用的函数(这里选surf),以及它要用的光照模型(这里选的是Lambert模型)
        #pragma surface surf Lambert   
        #pragma target 3.0

        fixed4 _Color;
        sampler2D _MainTex;
        sampler2D _BumpMap;

        //定义输入结构体
        struct Input{
           float2 uv_MainTex;       //包含我们需要的采样纹理坐标: uv_纹理名
           float2 uv_BumpMap;
        };

        void surf (Input IN, inout SurfaceOutput o)
        {
            fixed4 tex = tex2D(_MainTex, IN.uv_MainTex); 
            //然后填充表面着色器的输出结构体
            o.Albedo = tex.rgb * _Color.rgb;    //填充折射率
            o.Alpha = tex.a;        //填充透明度
            o.Normal = UnpackNormal(tex2D(_BumpMap, IN.uv_BumpMap));    //填充发现之
        }

        ENDCG
    }
    FallBack "Diffuse"
}
表面着色器实现光照与法线纹理应用的效果

可以看得出我们可以用很少的代码就去实现之前通过顶点/片元着色器需要很多的步骤才能够实现的效果。接下来我们会具体来了解表面着色器中的各个部分


二.表面着色器的编译指令

1.指定表面着色器和光照函数

格式如下:

#pragma surface surfaceFunction lightModel [optionalparams]
  • #pragam surface是固定开头,表示要指定一个表面着色器
  • surfaceFunction是要指定的表面函数
  • lightModel是要指定的光照模型
  • [optionalparams]是其他可选择参数

2.表面函数

表面函数用于一些表面属性:反射率,光滑度,透明度

通常命名为surf(其实可以任意名字,但是按照规范建议还是用这个名字)

格式如下:

void surf (Input IN, inout SurfaceOutput o)
void surf (Input IN, inout SurfaceOutputStandard o)
void surf (Input IN, inout SurfaceOutputStandardSpecular o)
  • Input:是输入结构体,用于设置各种表面属性
  • SurfaceOutput/SurfaceOutputStandard/SurfaceOutputStandardSpecular:是Unity内置的结构体,用于接收在表面着色器中计算变换得到的表面属性(通过Input输入),并把这些属性传递给光照函数

官方文档中有更详细地对表面函数的定义和使用的方式: 表面着色器示例 

3.光照函数

光照函数会通过表面着色器中计算传递的表面属性来计算光照颜色值

Unity有内置的光照模型:

  • 基于物理的光照模型:StandardStandardSpecular (在UnityPBSLighting.cginc文件中定义)
  • 非基于物理的光照模型:Lambert BlinnPhong(在Lighting.cginc文件中定义)

自定以光照函数:

//用于不依赖视角的光照模型
half4 Lighting<Name> (SurfaceOutput s, half3 lightDir, half atten)

//用于以来视角的光照模型,例如高光反射
half4 Lighting<Name> (SurfaceOutput s, half3 lightDir, half3 viewDir, half atten)

官方文档中有更详细地对自定义光照模型的例子: 表面着色器中的自定义光照模型

4.其他参数

4.1.自定义的顶点和颜色修改函数:

顶点修改函数(vertex:VertexFunction) :用于自定义一些顶点属性(顶点颜色传递给表面着色器,或者修改顶点位置实现顶点动画)

最后的颜色修改函数(finalcolor:ColorFunction):用于在颜色绘制到屏幕上之前最后对其进行一次修改操作(实现一些屏幕后处理效果,比如自定义全局雾效之类的)

4.2.阴影

  • 添加阴影投射Pass指令(addshadow):为表面着色器生成一个光照模式为ShadowCaster的Pass。用于将物体正确渲染到深度和阴影纹理中

注意:对于一些进行了特殊的操作(顶点动画,透明度测试)的物体,则需要对他们进行特殊的处理以适配阴影。

  • 前向渲染路径中支持所有光源类型的指令(fullforwardshadows):让点光源和聚光灯在前向渲染中也要有阴影投射投射效果就用这个指令
  • 禁用阴影(noshadow):不让这个物体进行任何阴影的计算

4.3.透明度混合和透明度测试

控制透明度混合(alpha):

控制透明度测试(alphatest:VariableName):使用名字为VariableName的变量来剔除不满足条件的片元

4.4.光照

用于控制光照对物体的影响

  • noambient:不要使用任何环境光照或者光照探针
  • novertexlights:不要应用任何逐顶点光照
  • noforwardadd:会去掉所有前向渲染中的额外的Pass(让它只支持一个逐像素平行光的计算,其他的按逐顶点和SH光照进行处理)
  • nolightmap:控制光照烘培
  • nofog:控制雾效

4.5.控制生成的代码

正常情况下表面着色器会生成相应的所有渲染路径的Pass(前向渲染,延迟渲染),这就导致生成的Shader文件比较大,但有的时候我们可能只会需要前向渲染的Pass。

  • exclude_path:deferred:不要生成延迟渲染的Pass
  • exclude_path:forward:不要生成前向渲染的Pass
  • exclude_path:prepass :不要生成旧版延迟路径的Pass

其他更详细的参数可以去官方手册中查阅:编写表面着色器


三.表面着色器的两个结构体

结构体用于实现各个函数之间的通信

1.数据来源:Input结构体

Input中内置的变量

还有:uv_纹理名:对应纹理名字的采样坐标

使用方法:

①直接在Input中按需要的变量给他规范命名好,Unity就会自动帮助我们准备好

②如果自定义了顶点修改函数,并需要向表面着色器中传递自定义的数据,则我们需要在顶点修改函数中进行计算与传递

2.表面属性:SurfaceOutput结构体

struct SurfaceOutput {  
    fixed3 Albedo;  //颜色反射率
    fixed3 Normal;  //表面法线方向
    fixed3 Emission;      //自发光颜色
    half Specular;      //高光反射中指数部分
    fixed Gloss;        //高光反射的强度系数
    fixed Alpha;        //透明通道
};

//基于物理的光照模型用的输入结构体(金属工作流)
struct SurfaceOutputStandard  
{  
    fixed3 Albedo;      // 基础颜色(漫反射或高光反射的) 
    fixed3 Normal;      // 切线空间中的法线  
    half3 Emission;  
    half Metallic;      // 0表示非金属,1表示金属  
    half Smoothness;    // 0表示粗糙,1表示光滑
    half Occlusion;     // occlusion (default 1)  
    fixed Alpha;        // alpha for transparencies  
};  

//基于物理的光照模型用的输入结构体(高光反射工作流)
struct SurfaceOutputStandardSpecular  
{  
    fixed3 Albedo;      // 漫反射颜色 
    fixed3 Specular;    // 高光反射颜色  
    fixed3 Normal;      // 切线空间下的法线  
    half3 Emission;  
    half Smoothness;    // 0表示粗糙,1表示光滑 
    half Occlusion;     // occlusion (default 1)  
    fixed Alpha;        // alpha for transparencies  
};  

使用方法:

①在一个表面着色器中,根据使用的光照模型对上述输出结构体进行三选一。(非基于物理的SurfaceOutput基于物理的SurfaceOutputStandard或SurfaceOutputStandardSpecular

其他:

BlinnPhong光照模型中的高光反射计算:float spec = pow (nh, s.Specular* 128.0) * s.Gloss;


四.表面着色器的转化规律

我们可以查看表面着色器为我们生成的顶点/片元着色器的代码:

生成过程如下:

①把表面着色器中CGPROGRAM和ENDCG中代码复制粘贴过去,当成是普通的函数和结构体在之后进行调用与传递

②生成顶点着色器的输出结构体--v2f_surf结构体:用于顶点和片元着色器之间传递数据

③生成顶点着色器:

  • 若定义了顶点修改函数:会先调用顶点修改函数对顶点数据进修改,或者填充自定义的Input结构体中的变量。
  • 计算v2f_surf中其他生成的变量值
  • 把v2f_surf传递给之后的片元着色器

④生成片元着色器:

  • v2f_surf变量填充到Input结构体
  • 调用表面函数填充SurfaceOutput结构体
  • 调用光照模型得到初始颜色值(如果是用Lambert模型或者BliinnPhong模型,还好计算动态全局关照,并把光照模型添加到计算中)
  • 进行颜色的叠加
  • 若定义了最终颜色修改函数:会调用它进行最后的颜色修改

总而言之:就是根据我们在表面着色器中的定义与设置,生成对应需要的可执行的顶点/片元着色器


五.表面着色器的实例

1.代码实现与效果

实现效果:在顶点修改器中沿着法线方向扩张顶点的位置

具体代码如下:

Shader "Custom/NormalExtrusion"
{
    Properties
    {
        _Color ("Color Tint", Color) = (1,1,1,1)        //颜色
        _MainTex ("Albedo (RGB)", 2D) = "white" {}      //主纹理
        _BumpTex ("Normalmap", 2D) = "bump" {}          //法线纹理
        _Amount ("Extrusion Amount", Range(-0.5, 0.5)) = 0.1    //膨胀程度
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 300

        CGPROGRAM
        //surf - 指定表面着色器
        //vertex:myvertex - 指定顶点修改器
        //finalcolor:mycolor - 指定最终颜色修改器
        //addshadow - 指定生成对应的阴影投射pass
        //exclude_path - 指定不要生成延迟渲染和老延迟渲染相关的pass,nometa用于提示不用生成提取元数据的pass
        #pragma surface surf CustomLambert vertex:myvertex finalcolor:mycolor addshadow exclude_path:deferred exclude_path:prepass nometa  
          

        // Use shader model 3.0 target, to get nicer looking lighting
        #pragma target 3.0

        fixed4 _Color;
        sampler2D _MainTex;
        sampler2D _BumpTex;
        half _Amount;

        struct Input
        {
            float2 uv_MainTex;
            float2 uv_BumpTex;
        };

        //定义顶点修改函数
        void myvertex (inout appdata_full v){
            v.vertex.xyz += v.normal * _Amount;     //让顶点沿着顶点法线方向移动(顶点+矢量 = 顶点沿着矢量方向的位移)
        }

        //定义表面着色器,接收Input结构体的输入,进行SurfaceOutput的输出
        void surf (Input IN, inout SurfaceOutput o)
        {
            // Albedo comes from a texture tinted by color
            fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
            o.Albedo = c.rgb;
            o.Alpha = c.a;
            o.Normal = UnpackNormal(tex2D(_BumpTex, IN.uv_BumpTex));
        }

        //定义光照模型,接收SurfaceOutput的输入,以及光照方向和光照衰减的输入
        half4 LightingCustomLambert (SurfaceOutput s, half3 lightDir, half atten){
            half NdotL = dot(s.Normal, lightDir);
            half4 c;
            c.rgb = s.Albedo * _LightColor0.rgb * (NdotL * atten);
            c.a = s.Alpha;

            return c;
        }

        //定义最后颜色处理函数
        void mycolor (Input IN, SurfaceOutput o, inout fixed4 color){
            color *= _Color;
        }

        ENDCG
    }

    FallBack "Legacy Shader/Diffuse"
}

①顶点修改函数中:我们把顶点往法线方向进行了偏移

②光照模型我们自定义实现了一个简单的兰伯特反射光照模型

③最后的颜色修改器中我们用颜色属性对最终的输出颜色进行最后的调整

④addshadow参数告诉Unity需要生成该表面着色器对应的投影Pass

⑤exclude_path:deferred和exclude_path:prepass告诉Unity不要为延迟渲染生成相应的路径

⑥nometa参数取消对提取元素据的pass的生成

左:顶点膨胀后    右:正常渲染

2.查看生成的顶点/片元着色器代码

点这里可以查看该表面着色器生成的对应顶点/片元着色器代码,下面我们来简单看一下生成的基础前向渲染的pass

2.1.Pass设置与编译指令

2.2.一些自动生成的注释

2.3.一些辅助计算的宏

#define INTERNAL_DATA half3 internalSurfaceTtoW0; half3 internalSurfaceTtoW1; half3 internalSurfaceTtoW2;
#define WorldReflectionVector(data,normal) reflect (data.worldRefl, half3(dot(data.internalSurfaceTtoW0,normal), dot(data.internalSurfaceTtoW1,normal), dot(data.internalSurfaceTtoW2,normal)))
#define WorldNormalVector(data,normal) fixed3(dot(data.internalSurfaceTtoW0,normal), dot(data.internalSurfaceTtoW1,normal), dot(data.internalSurfaceTtoW2,normal))

2.4.复制过来的表面着色器的函数与结构体

// Original surface shader snippet:
#line 13 ""
#ifdef DUMMY_PREPROCESSOR_TO_WORK_AROUND_HLSL_COMPILER_LINE_HANDLING
#endif
/* UNITY: Original start of shader */
        //surf - 指定表面着色器
        //vertex:myvertex - 指定顶点修改器
        //finalcolor:mycolor - 指定最终颜色修改器
        //addshadow - 指定生成对应的阴影投射pass
        //exclude_path - 指定不要生成延迟渲染和老延迟渲染相关的pass,nometa用于提示不用生成提取元数据的pass
        //#pragma surface surf CustomLambert vertex:myvertex finalcolor:mycolor addshadow exclude_path:deferred exclude_path:prepass nometa  
          

        // Use shader model 3.0 target, to get nicer looking lighting
        //#pragma target 3.0

        fixed4 _Color;
        sampler2D _MainTex;
        sampler2D _BumpTex;
        half _Amount;

        struct Input
        {
            float2 uv_MainTex;
            float2 uv_BumpTex;
        };

        //定义顶点修改函数
        void myvertex (inout appdata_full v){
            v.vertex.xyz += v.normal * _Amount;     //让顶点沿着顶点法线方向移动(顶点+矢量 = 顶点沿着矢量方向的位移)
        }

        //定义表面着色器,接收Input结构体的输入,进行SurfaceOutput的输出
        void surf (Input IN, inout SurfaceOutput o)
        {
            // Albedo comes from a texture tinted by color
            fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
            o.Albedo = c.rgb;
            o.Alpha = c.a;
            o.Normal = UnpackNormal(tex2D(_BumpTex, IN.uv_BumpTex));
        }

        //定义光照模型,接收SurfaceOutput的输入,以及光照方向和光照衰减的输入
        half4 LightingCustomLambert (SurfaceOutput s, half3 lightDir, half atten){
            half NdotL = dot(s.Normal, lightDir);
            half4 c;
            c.rgb = s.Albedo * _LightColor0.rgb * (NdotL * atten);
            c.a = s.Alpha;

            return c;
        }

        //定义最后颜色处理函数
        void mycolor (Input IN, SurfaceOutput o, inout fixed4 color){
            color *= _Color;
        }

2.5.顶点着色器到片元着色器的插值结构体

// vertex-to-fragment interpolation data
// no lightmaps:
#ifndef LIGHTMAP_ON
// half-precision fragment shader registers:
#ifdef UNITY_HALF_PRECISION_FRAGMENT_SHADER_REGISTERS
struct v2f_surf {
  UNITY_POSITION(pos);
  float4 pack0 : TEXCOORD0; // _MainTex _BumpTex
  float4 tSpace0 : TEXCOORD1;
  float4 tSpace1 : TEXCOORD2;
  float4 tSpace2 : TEXCOORD3;
  fixed3 vlight : TEXCOORD4; // ambient/SH/vertexlights
  UNITY_LIGHTING_COORDS(5,6)
  #if SHADER_TARGET >= 30
  float4 lmap : TEXCOORD7;
  #endif
  UNITY_VERTEX_INPUT_INSTANCE_ID
  UNITY_VERTEX_OUTPUT_STEREO
};
#endif
// high-precision fragment shader registers:
#ifndef UNITY_HALF_PRECISION_FRAGMENT_SHADER_REGISTERS
struct v2f_surf {
  UNITY_POSITION(pos);
  float4 pack0 : TEXCOORD0; // _MainTex _BumpTex
  float4 tSpace0 : TEXCOORD1;
  float4 tSpace1 : TEXCOORD2;
  float4 tSpace2 : TEXCOORD3;
  fixed3 vlight : TEXCOORD4; // ambient/SH/vertexlights
  UNITY_SHADOW_COORDS(5)
  #if SHADER_TARGET >= 30
  float4 lmap : TEXCOORD6;
  #endif
  UNITY_VERTEX_INPUT_INSTANCE_ID
  UNITY_VERTEX_OUTPUT_STEREO
};
#endif
#endif
// with lightmaps:
#ifdef LIGHTMAP_ON
// half-precision fragment shader registers:
#ifdef UNITY_HALF_PRECISION_FRAGMENT_SHADER_REGISTERS
struct v2f_surf {
  UNITY_POSITION(pos);
  float4 pack0 : TEXCOORD0; // _MainTex _BumpTex
  float4 tSpace0 : TEXCOORD1;
  float4 tSpace1 : TEXCOORD2;
  float4 tSpace2 : TEXCOORD3;
  float4 lmap : TEXCOORD4;
  UNITY_LIGHTING_COORDS(5,6)
  UNITY_VERTEX_INPUT_INSTANCE_ID
  UNITY_VERTEX_OUTPUT_STEREO
};
#endif
// high-precision fragment shader registers:
#ifndef UNITY_HALF_PRECISION_FRAGMENT_SHADER_REGISTERS
struct v2f_surf {
  UNITY_POSITION(pos);
  float4 pack0 : TEXCOORD0; // _MainTex _BumpTex
  float4 tSpace0 : TEXCOORD1;
  float4 tSpace1 : TEXCOORD2;
  float4 tSpace2 : TEXCOORD3;
  float4 lmap : TEXCOORD4;
  UNITY_SHADOW_COORDS(5)
  UNITY_VERTEX_INPUT_INSTANCE_ID
  UNITY_VERTEX_OUTPUT_STEREO
};
#endif
#endif
float4 _MainTex_ST;
float4 _BumpTex_ST;

可以看到里面有很多的#ifdef这种,是用于判断渲染的各种设置以生成合适的结构体

2.6.生成顶点着色器

// vertex shader
v2f_surf vert_surf (appdata_full v) {
  UNITY_SETUP_INSTANCE_ID(v);
  v2f_surf o;
  UNITY_INITIALIZE_OUTPUT(v2f_surf,o);
  UNITY_TRANSFER_INSTANCE_ID(v,o);
  UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o);
  myvertex (v);
  o.pos = UnityObjectToClipPos(v.vertex);
  o.pack0.xy = TRANSFORM_TEX(v.texcoord, _MainTex);
  o.pack0.zw = TRANSFORM_TEX(v.texcoord, _BumpTex);
  float3 worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
  float3 worldNormal = UnityObjectToWorldNormal(v.normal);
  fixed3 worldTangent = UnityObjectToWorldDir(v.tangent.xyz);
  fixed tangentSign = v.tangent.w * unity_WorldTransformParams.w;
  fixed3 worldBinormal = cross(worldNormal, worldTangent) * tangentSign;
  o.tSpace0 = float4(worldTangent.x, worldBinormal.x, worldNormal.x, worldPos.x);
  o.tSpace1 = float4(worldTangent.y, worldBinormal.y, worldNormal.y, worldPos.y);
  o.tSpace2 = float4(worldTangent.z, worldBinormal.z, worldNormal.z, worldPos.z);
  #ifdef DYNAMICLIGHTMAP_ON
  o.lmap.zw = v.texcoord2.xy * unity_DynamicLightmapST.xy + unity_DynamicLightmapST.zw;
  #endif
  #ifdef LIGHTMAP_ON
  o.lmap.xy = v.texcoord1.xy * unity_LightmapST.xy + unity_LightmapST.zw;
  #endif

  // SH/ambient and vertex lights
  #ifndef LIGHTMAP_ON
  #if UNITY_SHOULD_SAMPLE_SH && !UNITY_SAMPLE_FULL_SH_PER_PIXEL
  float3 shlight = ShadeSH9 (float4(worldNormal,1.0));
  o.vlight = shlight;
  #else
  o.vlight = 0.0;
  #endif
  #ifdef VERTEXLIGHT_ON
  o.vlight += Shade4PointLights (
    unity_4LightPosX0, unity_4LightPosY0, unity_4LightPosZ0,
    unity_LightColor[0].rgb, unity_LightColor[1].rgb, unity_LightColor[2].rgb, unity_LightColor[3].rgb,
    unity_4LightAtten0, worldPos, worldNormal );
  #endif // VERTEXLIGHT_ON
  #endif // !LIGHTMAP_ON

  UNITY_TRANSFER_LIGHTING(o,v.texcoord1.xy); // pass shadow and, possibly, light cookie coordinates to pixel shader
  return o;
}

①显然它首先调用了我们自定义的顶点修改器

注意:

我们的顶点修改器中只是对顶点进行了一些修改,但是并没有向Input结构体中加入新的变量。如果需要向其中加入新的变量可以用另一个版本的函数声明:

void vert (inout appdata_full v, out Input o);

②之后的代码中是计算v2f_surf结构体中需要的数据

2.7.生成片元着色器

// fragment shader
fixed4 frag_surf (v2f_surf IN) : SV_Target {
  UNITY_SETUP_INSTANCE_ID(IN);
  UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(IN);
  // prepare and unpack data
  Input surfIN;
  #ifdef FOG_COMBINED_WITH_TSPACE
    UNITY_RECONSTRUCT_TBN(IN);
  #else
    UNITY_EXTRACT_TBN(IN);
  #endif
  UNITY_INITIALIZE_OUTPUT(Input,surfIN);
  surfIN.uv_MainTex.x = 1.0;
  surfIN.uv_BumpTex.x = 1.0;
  surfIN.uv_MainTex = IN.pack0.xy;
  surfIN.uv_BumpTex = IN.pack0.zw;
  float3 worldPos = float3(IN.tSpace0.w, IN.tSpace1.w, IN.tSpace2.w);
  #ifndef USING_DIRECTIONAL_LIGHT
    fixed3 lightDir = normalize(UnityWorldSpaceLightDir(worldPos));
  #else
    fixed3 lightDir = _WorldSpaceLightPos0.xyz;
  #endif
  #ifdef UNITY_COMPILER_HLSL
  SurfaceOutput o = (SurfaceOutput)0;
  #else
  SurfaceOutput o;
  #endif
  o.Albedo = 0.0;
  o.Emission = 0.0;
  o.Specular = 0.0;
  o.Alpha = 0.0;
  o.Gloss = 0.0;
  fixed3 normalWorldVertex = fixed3(0,0,1);
  o.Normal = fixed3(0,0,1);

  // call surface function
  surf (surfIN, o);

  // compute lighting & shadowing factor
  UNITY_LIGHT_ATTENUATION(atten, IN, worldPos)
  fixed4 c = 0;
  float3 worldN;
  worldN.x = dot(_unity_tbn_0, o.Normal);
  worldN.y = dot(_unity_tbn_1, o.Normal);
  worldN.z = dot(_unity_tbn_2, o.Normal);
  worldN = normalize(worldN);
  o.Normal = worldN;
  #ifndef LIGHTMAP_ON
  c.rgb += o.Albedo * IN.vlight;
  #endif // !LIGHTMAP_ON

  // lightmaps
  #ifdef LIGHTMAP_ON
    #if DIRLIGHTMAP_COMBINED
      // directional lightmaps
      fixed4 lmtex = UNITY_SAMPLE_TEX2D(unity_Lightmap, IN.lmap.xy);
      fixed4 lmIndTex = UNITY_SAMPLE_TEX2D_SAMPLER(unity_LightmapInd, unity_Lightmap, IN.lmap.xy);
      half3 lm = DecodeDirectionalLightmap (DecodeLightmap(lmtex), lmIndTex, o.Normal);
    #else
      // single lightmap
      fixed4 lmtex = UNITY_SAMPLE_TEX2D(unity_Lightmap, IN.lmap.xy);
      fixed3 lm = DecodeLightmap (lmtex);
    #endif

  #endif // LIGHTMAP_ON


  // realtime lighting: call lighting function
  #ifndef LIGHTMAP_ON
  c += LightingCustomLambert (o, lightDir, atten);
  #else
    c.a = o.Alpha;
  #endif

  #ifdef LIGHTMAP_ON
    // combine lightmaps with realtime shadows
    #ifdef SHADOWS_SCREEN
      #if defined(UNITY_LIGHTMAP_DLDR_ENCODING)
      c.rgb += o.Albedo * min(lm, atten*2);
      #else
      c.rgb += o.Albedo * max(min(lm,(atten*2)*lmtex.rgb), lm*atten);
      #endif
    #else // SHADOWS_SCREEN
      c.rgb += o.Albedo * lm;
    #endif // SHADOWS_SCREEN
  #endif // LIGHTMAP_ON

  #ifdef DYNAMICLIGHTMAP_ON
  fixed4 dynlmtex = UNITY_SAMPLE_TEX2D(unity_DynamicLightmap, IN.lmap.zw);
  c.rgb += o.Albedo * DecodeRealtimeLightmap (dynlmtex);
  #endif

  mycolor (surfIN, o, c);
  UNITY_OPAQUE_ALPHA(c.a);
  return c;
}

①先用插值结构体v2f_surf来初始化Input结构体中的变量

②准备好Input结构体,并调用表面函数对他进行填充

③真正开始进行光照计算其中的c用于存储最终的颜色值。

  • #ifndef LIGHTMAP_ON意思是判Unity是否关闭了光照纹理。如果关闭了,就把逐顶点的光照结果叠加到颜色当中
  • #ifdef LIGHTMAP_ON意识是判断Unity是否开启了光照纹理。如果开启了,那就用计算好的光照纹理坐标进行采样计算
  • 如果是关闭了光照纹理的话,那就用我们指定的光照模型进行光照计算
  • 如果开启了光照纹理的话,那就用之前采样好的光照纹理上的颜色进行叠加。同时如果开启了动态光照映射(#ifdef DYNAMICLIGHTMAP_ON),还会对动态光照纹理进行采样

④最后把光照计算得到的颜色再用最终颜色修改器进行最后的处理

  • 下面UNITY_OPAQUE_ALPHA(c, a)这个宏是用于充值片元的透明通道(默认情况下a都会重置为1.0,而不论我们是否在光照函数中改变了它。如果要保留透明通道则在表面着色器的编译指令中添加keepalpha参数
  • 然后把颜色值c返回并输出

  1. 之后的ForwardAdd Pass和上述生成的代码基本差不多,只是去掉了对逐顶点光照和各种判断是否使用光照纹理的代码,更加简单了
  2. 最后一个Pass是ShadowCaster Pass。它更简单了的,原理是使用自定义的顶点修改函数来保证计算阴影的时候使用的是和之前相一致的顶点坐标它的计算同样是使用了V2F_SHADOW_CATSER ,  TRANSFER_SHADOW_CASTER_NORMALOFFSET, 和 SHADOW_CASTER_FRAGMENT来计算阴影投射。

六.表面着色器的缺点和使用选择

缺点:

①自由度更低,失去了的各种优化和特效实现的控制,无法实现一些自定义的效果

②对性能造成影响,无法进行更深层次的优化

优点:

①不必重复考虑复杂的光照计算就可以很快速地实现光照效果

使用选择:

①需要各种光源,尤其是需要Unity中的全局光照的时候,使用表面着色器(注意性能消耗)

②如果处理光源数目少(只有一个平行光这种),使用顶点/片元着色器会更好

③如果需要实现很多的自定义效果,使用顶点/片元着色器


七.总结

①表面着色器是基于顶点/片元着色器的更高一层抽象和封装。

②表面着色器把着色分为表面着色器,光照模型,光照着色器三部分。

③表面着色器适合Unity中需要处理多光源尤其是用到全局光照的地方,但是要注意它的性能开销

④表面着色器在使用的时候需要给他指定好对应的编译指令,指定好表面着色器对应的函数,使用的光照模型,以及其他的着色设置(是否开启投影,是否需要使用顶点修改器,是否需要使用最终颜色修改器)

⑤表面着色器的输入结构体是Input,它内置了很多已经准备好的变量名,当然我们也可以通过自定义的顶点修改器去给他传入我们需要的自定义的变量

⑥表面着色器的输出结构体是SurfaceOutput/SurfaceOutputStanard/SurfaceOutputStanardSpecular,它们的定义中包含了所有需要进行传入的变量,使用的时候只要根据需要(光照模型)去选择对应的结构体与在表面着色器中填充给他的数据即可

⑦表面着色器可以生成对应的顶点/片元着色器并查看。在基础前向Pass中:

  • 表面着色器中定义的结构体与函数复制粘贴进去作为普通的结构体与函数被调用
  • Unity根据分析生成v2f_surface(顶点到片元插值结构体)
  • 顶点着色器中先用顶点修改器修改顶点位置(如果有顶点修改器),然后再计算v2f_surface中需要的变量
  • 片元着色器中用v2f_surface初始化Input结构体,并调用表面着色器来计算表面属性值。之后通过表面属性值和光照模型去进行真正的光照计算【光照计算中如果开启了光照纹理,则可以不使用光照模型,而是通过光照纹理进行采样得到光照颜色值并叠加】
  • 最后在输出颜色之前再调用最终颜色修改器进行修改,并根据设置选择是否重置颜色的alpha值

⑧注意如果特别注重深度优化和需要高度的自定义特效的话,还是选择顶点/片元着色器最好的


网站公告

今日签到

点亮在社区的每一天
去签到