[译]Unity3D Shader教程(一)ShaderLab结构

[译]Unity3D Shader教程(一)ShaderLab结构

2021年6月6日 0 作者 老王

原文地址
Shader Tutorials by Ronja


1 Shader结构

在用Shader实现出我们想要的效果前,我们需要先知道Shader代码的基本结构(就像我们用C#编程需要先知道C#的语法一样)。
大多数现代着色器都有一个可变的渲染管线,它至少由一个顶点着色器和一个片段着色器组成,还可以添加几何着色器和曲面细分器,但是这两者我们很少使用。

上面的管线是指什么?可以简单理解为我们工厂中的流水线,那流水线是怎么样的?原材料准备好后,先给工位1的员工进行加工,工位1加工完成后的半成品给工位2的员工继续加工,工位2的员工加工完成后的半成品继续给工位3的员工加工……直到最后一道工序加工完毕,原材料也就变成了产品。那么什么是可变的呢?这里的可变可以理解为一些工位是必需的一些工位不是必需的,可要可不要。比如之前原材料需要经过工位1、工位2、工位3的加工才能做出产品,现在我们把工位2去掉,只留工位1和工位3,但是也能加工出完整的产品。
渲染管线中的顶点着色器和片段着色器就相当于咱们上面流水线的工位1和工位3,这两个在我们的Shader代码中是必需的。而几何着色器和曲面细分器就相当于上面流水线的工位2,虽然它们不是必需的,但是写Shader时用它们可以做出一些特殊的效果,这里不再多说。

顶点着色器(也称为顶点阶段或顶点函数)的作用是将模型的数据变换到屏幕空间下,以便后面的工序(如片元着色器)使用。
模型数据是什么?包括模型网格的顶点的局部坐标、法线方向、uv坐标等。
用什么方法将模型的数据变换到屏幕空间的呢?矩阵变换。由于矩阵变换的原理不是一时半会儿能说清楚的,这就不多说了,暂且记住就行。

除了将模型的数据变换到屏幕空间这一基本功能外,我们可以用自定义的顶点着色器让顶点产生动画而不用我们手动去修改模型的网格,同时可以也将更多数据输出(比如顶点坐标对应的世界坐标等)给片段着色器。经过顶点着色器后,我们得到了屏幕空间上的顶点,顶点之间构成的三角形被光栅化器转换为像素。同时,光栅化器对顶点着色器的输出进行线性的插值,因此网格中间的像素也获得了输出值。在决定渲染哪些像素后,片段着色器(也称为像素着色器)决定像素的颜色。
光栅器的插值过程可以参考下面这张图理解:左边是顶点着色器的输出,右边是光栅器插值后的输出。
光栅化插值过程
大致的管线过程如下:
渲染流水线
从上面我们可以也看出,Shader的作用其实就是以模型的数据为顶点着色器的输入,最后通过片元着色器输出像素颜色。所以Shader代码中必须包含顶点着色器和片元着色器两部分。无论使用什么语言来编写Shader,顶点着色器和片元着色器都是必需的,即使没有顶点和片元阶段概念的基于节点的着色器(如Shader Graph)仍然会在内部生成这些阶段(顶点着色器、片元着色器)。

2 ShaderLab

2.1 创建Shader

Unity 中的着色器代码是一个文本文件,文件名以 .shader 结尾,与 C# 脚本以 .cs 结尾的方式类似。我们可以通过右键单击Assets区域 > Create > Shader > 选择一个模板 来轻松创建其中一个模板。由于我们刚开始学习,所以我们从创建一个最简单的无光照的Shader开始,右键单击 Assets> Create > Shader > Unlit Shader。长下面这个样子。

Shader "Unlit/NewUnlitShader"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            // make fog work
            #pragma multi_compile_fog

            #include "UnityCG.cginc"

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

            struct v2f
            {
                float2 uv : TEXCOORD0;
                UNITY_FOG_COORDS(1)
                float4 vertex : SV_POSITION;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                UNITY_TRANSFER_FOG(o,o.vertex);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                // sample the texture
                fixed4 col = tex2D(_MainTex, i.uv);
                // apply fog
                UNITY_APPLY_FOG(i.fogCoord, col);
                return col;
            }
            ENDCG
        }
    }
}

2.2 ShaderLab

Unity的Shader是一种被称为“ShaderLab”的自定义语言,它定义了Shader中常用的输入输出数据以及渲染管线中的一些状态,同时用一个代码块将真正的Shader代码(顶点着色器、片元着色器等,用hlsl、glsl或cg编写)包含进来。
上面这句话可能没怎么表达清楚,看图。
在这里插入图片描述
从上面可以看出,真正的ShaderLab现在只占用我们着色器文件的一小部分。它只是以更抽象的形式描述事物,Unity最终会将ShaderLab的部分编译成hlsl、glsl或cg。

2.3 Shader/SubShader/Pass

ShaderLab文件中,由多个大括号 {} 来划分代码块。

其中Shader{}定义了整个着色器,每个Shader文件有且只能有一个Shader{}。Shader后面跟的是此Shader的名字,可以通过斜杠(/)来个各个Shader分门别类。比如我们如下设置

Shader "Tutorials/NewUnlitShader"
{
    // ...

    FallBack "VertexLit"
}

则此Shader在Unity菜单栏的位置就如下。
Shader在Menu中的位置
Shder{}代码块中有一个FallBack,其用来定义后备着色器,其作用相当于将后备着色器的所有子着色器粘贴到我们的着色器文件中。我们一般通过添加FallBack "VertexLit"来生成阴影 。为什么用“VertexLit”来生成阴影呢,因为它消耗比较少。
一个Shader{}块可以包含一个或多个SubShader{}。一般编写不同的子着色器供不同的硬件使用,但是关于如何定义以及何时使用哪个子着色器的文档非常缺乏,根据经验,大多数情况使用一个SubShader就已经足够了。
在SubShader中,我们还可以定义SubShader的标签Tags和属性Properties,这些标签和属性将作用于该SubShader中的所有Pass。

Pass 代表一次完整的渲染过程(即一个Pass就表示会完整执行一次渲染流水线,一个Pass绘制一次图像到屏幕上)。如果我们定义了多个Pass,它们将一个接一个地绘制(但URP 只会执行一次Pass)。有多个Pass的着色器被称为多Pass着色器。Pass代码块中可以设置此Pass的名称(可选的),也可以同SubShader一样设置标签Tags和属性(但Pass内的标签和属性只作用于这一个Pass,SubShader中的属性和标签作用于该SubShader中的所有Pass) ,最重要的它包含实际处理渲染的代码(即顶点着色器和片元着色器)。

2.4 Properties and Tags

现在我们来看看上面还没详细说明的Properties和Tags代码块。
Tags标签,用键值对表示。 Subshader 中的Tags标签主要定义带有着色器的材质如何在编辑器中显示,何时渲染或可以对它们应用哪些操作,而 pass 标签主要用于在遗留管道中定义哪些 pass 用于光计算的哪个步骤,SubShader的TagsPass的Tags具体可以设置哪些内容可以查看Unity的官方文档。
Properties属性用于在材质编辑器中显示变量。我们一旦更改这些属性的值,使用该材质的物体都将改变。如果我们想每个不同的物体都使用不同的属性,我们得用另外的方法了。由于默认情况下我们可以访问纹理坐标,并且我们可以通过这些属性设置纹理,这样我们可以实现很多效果。我将在下一个文章之一中更深入地解释属性。
总的来说,下面就是着色器的粗略结构。

Shader "Category/Name"{
    Properties{
        //Properties
    }
    Subshader{
        Tags{
            //Subshader Tags
        }

        //Settings for all passes

        Pass{
            Tags{
                //Pass Tags
            }

            //Settings for pass

            CGPROGRAM
            //shader code
            ENDCG
        }
    }
}

原作者的GitHub
本文Shader

Shader "Tutorial/001-004_Basic_Unlit"{
    //show values to edit in inspector
    Properties{
//  _Color ("Tint", Color) = (0, 0, 0, 1)
//  _MainTex ("Texture", 2D) = "white" {}
    }

    SubShader{
        //the material is completely non-transparent and is rendered at the same time as the other opaque geometry
        Tags{ "RenderType"="Opaque" "Queue"="Geometry" }

        Pass{
            CGPROGRAM
//
//      //include useful shader functions
//      #include "UnityCG.cginc"
//
//      //define vertex and fragment shader functions
//      #pragma vertex vert
//      #pragma fragment frag
//
//      //texture and transforms of the texture
//      sampler2D _MainTex;
//      float4 _MainTex_ST;
//
//      //tint of the texture
//      fixed4 _Color;
//
//      //the mesh data thats read by the vertex shader
//      struct appdata{
//          float4 vertex : POSITION;
//          float2 uv : TEXCOORD0;
//      };
//
//      //the data thats passed from the vertex to the fragment shader and interpolated by the rasterizer
//      struct v2f{
//          float4 position : SV_POSITION;
//          float2 uv : TEXCOORD0;
//      };
//
//      //the vertex shader function
//      v2f vert(appdata v){
//          v2f o;
//          //convert the vertex positions from object space to clip space so they can be rendered correctly
//          o.position = UnityObjectToClipPos(v.vertex);
//          //apply the texture transforms to the UV coordinates and pass them to the v2f struct
//          o.uv = TRANSFORM_TEX(v.uv, _MainTex);
//          return o;
//      }
//
//      //the fragment shader function
//      fixed4 frag(v2f i) : SV_TARGET{
//          //read the texture color at the uv coordinate
//          fixed4 col = tex2D(_MainTex, i.uv);
//          //multiply the texture color and tint color
//          col *= _Color;
//          //return the final color to be drawn on screen
//          return col;
//
//      }
            ENDCG
        }
    }
    Fallback "VertexLit"
}

ps:感觉翻译得很垃圾啊,捂脸。