上一篇博客 介绍了Shader的基本结构,这里我们继续来说Shader的编写,也就是要在 CGPROGRAM 中写代码。首先我们把之前的Shader结构代码复制过来。

Shader "iMoeGirl/MyShader" {  // Shader 名字

    // 这里定义一些属性,可以显示在UI面板上用于调节
    Properties {
        // 属性名("Inspector面板上显示出来的属性名", 属性类型) = 默认值
        _Color("颜色类型", Color) = (1,1,1,1)
        _Vector("向量类型", Vector) = (1, 2, 3, 4)
        _Int("整型", Int) = 11111
        _Float("浮点型", Float) = 12.11
        _Range("范围类型", Range(100, 1000)) = 128
        _Tex2D("贴图类型", 2D) = "white"{}
        _Cube("立方体贴图类型", Cube) = "white"{}
        _Tex3D("3D纹理", 3D) = "white"{}
    }

    // 子 Shader,可以写多个,显卡运行时,
    // 从第一个SubShader开始,如果第一个里面的效果都支持,则使用第一个,
    // 如果发现这个SubShader里面某些效果不支持,则自动运行下一个SubShader
    SubShader {
        
        // 至少有一个Pass,相当于一个方法
        Pass {
            // 在Pass块里写Shader代码

            CGPROGRAM
            // 使用 CG语言编写Shader
            ENDCG
        }
    }

    // 如果发现所有的SubShader都不支持,则使用Fallback,相当于后备方案
    Fallback "VertexLit"
}

怎样使用 Properties 中定义的属性

Unity3D定义Shader属性所使用的语法,和CG所使用的说法是不一样的,所以我们要在一个Pass中使用Properties中定义的属性,需要在Pass中再以CG的语法再写一遍,其实就是变量名相同,而数据类型不同,在Shader在编译的时候,就会自动将两个变量关联起来。看下面的代码

Shader "iMoeGirl/MyShader" {  // Shader 名字

    // 这里定义一些属性,可以显示在UI面板上用于调节
    Properties {
        // 属性名("Inspector面板上显示出来的属性名", 属性类型) = 默认值
        _Color("颜色类型", Color) = (1,1,1,1)
        _Vector("向量类型", Vector) = (1, 2, 3, 4)
        _Int("整型", Int) = 11111
        _Float("浮点型", Float) = 12.11
        _Range("范围类型", Range(100, 1000)) = 128
        _Tex2D("贴图类型", 2D) = "white"{}
        _Cube("立方体贴图类型", Cube) = "white"{}
        _Tex3D("3D纹理", 3D) = "white"{}
    }

    // 子 Shader,可以写多个,显卡运行时,
    // 从第一个SubShader开始,如果第一个里面的效果都支持,则使用第一个,
    // 如果发现这个SubShader里面某些效果不支持,则自动运行下一个SubShader
    SubShader {
        
        // 至少有一个Pass,相当于一个方法
        Pass {
            // 在Pass块里写Shader代码

            CGPROGRAM
                float4 _Color;
                float4 _Vector;
                float _Int;
                float _Range;
                sampler2D _Tex2D;
                samplerCUBE _Cube;
                sampler3D _Tex3D;

            ENDCG
        }
    }

    // 如果发现所有的SubShader都不支持,则使用Fallback,相当于后备方案
    Fallback "VertexLit"
}

从上面代码可以看出,对于 Unity 的数据类型,有4个值的,都可以使用 float4 来对应,而有1个值的,可以使用 float 来对应,2D 贴图类型在 CG 中对应的是 sampler2D,3D 纹理对应的是 sampler3D,等等

对于CG来说,有float, float2, float3, float4, half, half2, half3, half4, fixed, fixed2, fixed3, fixed4 等等,最多不能超过4元向量,float 系列和 half 系列以及 fixed系列可以互相替换,但是有什么区别呢? 他们都是用来表示浮点数的,只是精度不同,float 系列是32位浮点数,而 half 系列是16位,fixed 系列是12位。例如,我们如果要表示颜色,完全可以用 fixed4 就足够了,因为Color的每一元素值最大就是1,fixed4 完全满足。在 Pass 中定义了 Properties 中的同名变量,也就可以在 CGPROGRAM 中使用了。

顶点处理函数和片元处理函数

这里我们先说另一个问题,两个重要的函数,一个是顶点处理函数,一个是片面处理函数。通俗来讲,顶点处理函数就是处理顶点的,一个模型的每一个顶点都会传入顶点处理函数被处理后返回。而片元处理函数是处理像素的,一个模型经过各种计算后被转换成屏幕的上像素,而每一个像素都是会经过片元处理函数处理的。而我们定义的顶点处理函数和片元处理函数,都是会被系统自动调用的,不需要我们来调用,就像写 MomoBehaviour 脚本时的 Awake、Start、Update 等函数。

Shader "iMoeGirl/02 Second Shader" {
    SubShader {
        Pass {
            CGPROGRAM
                // 这里声明了顶点函数的函数名
                // 顶点函数的主要作用是将顶点从模型空间转换到裁剪空间
                #pragma vertex vert

                // 这里声名了片元函数的函数名
                // 片元函数处理每一个像素,每一个像素都会经过片元函数处理
                // 返回模型对应屏幕上的每一个像素的颜色值
                #pragma fragment frag

                // POSITION 和 SV_POSITION 都是语义,告诉系统参数是干嘛的
                // POSITION 用来解释 v, SV_POSITION 用来解释返回值
                float4 vert(float4 v : POSITION) : SV_POSITION {
                    return UnityObjectToClipPos(v);
                }

                float4 frag() : SV_TARGET {
                    return fixed4(1,0.5,1,1);
                }

            ENDCG
        }
    }

    Fallback "VertexLit"
}

注意看上面代码中的注释,这里稍微做一下解释,#pragma vertex vert 就是告诉系统我们的顶点处理函数是什么,vert 就是函数名,当然不一定叫这个,可以随便命名,例如 #pragma vertex myvert 都可以。下面的 #pragma fragment frag 当然就是声明片元函数的。

float4 vert(float4 v : POSITION) : SV_POSITION {
    return UnityObjectToClipPos(v);
}

CG 中函数的定义和普通编程差不多,这里有一点不同就是 : 后面的,是语义,可以理解为告诉系统需要什么数据。例如参数 v 是用 POSITION 语义来描述的,就是告诉系统,这里的 v,表示需要坐标值,就是让系统把顶点的坐标传进来,而后面的 SV_POSITION 是描述返回值的,也就是需要返回裁剪空间中的定点坐标,这也是顶点函数做的最重要的事情,也就是把顶点从模型空间转换到裁剪空间。函数中的 UnityObjectToClipPos(v);是Unity的内置函数,也就是这个转换过程不需要我们自己写,直接调用 Unity 的函数就可以了。

再来看一下片元函数

float4 frag() : SV_TARGET {
    return fixed4(1,1,1,1);
}

这个函数返回的是一个 float4 类型的,而 SV_Target 语意则说明了返回的是一个颜色值。注意函数里面我们使用了 fixed4,而函数名前使用了 float4,这是完全可以的。

在Unity中新建一个材质,然后使用我们新编写的Shader,Shader名是 02 Second Shader,把材质赋予一个胶囊体或者立方体都行,就可以看到一个纯白的东西显示在屏幕中。可以尝试自己修改一下片元函数中的返回值,例如改成 return fixed4(1, 0, 0, 1) 这时就会变成红色。前三位分别代表RGB三通道的值,最后一位是Alpha,也就是透明通道的值。