也许,你也可以写 Shader

引言

在游戏开发领域,貌似有句话叫做会写Shader的都是高手。这是因为Shader通常相当复杂,而且Unity的官方文档也很散乱,学习曲线十分陡峭,入门困难。 但只要你想做,没有什么是不行的。我找到一篇很好的入门教程,学成之后写了本文,本文将介绍一下Shader的入门知识,以及初学Shader可以实现什么效果。如果你想点亮你的Shader专精树,不妨来康康这篇文章。

介绍

Shader(着色器)实际上就是一小段程序,它负责将输入的Mesh(网格)以指定的方式和输入的贴图或者颜色等组合作用,然后输出。 以下是Shader代码的基本架构

Shader "Shader的路径和名称"{
    Properties{
        ...
    }

    //可能存在多个subshader。Unity会在所有的subshader列表中选中第一个硬件可以支持的subshader
    SubShader{
        
        //可能存在多个pass,每个pass都会引起一次渲染
        Pass{
            ...
        }
        //可以有其他的Pass
        [其他的Pass]
    }
    //可以有多个SubShader
    [其他的SubShader]
    //当所有的subshader失败时,使用Fallback指定的shader
    [Fallback]
    //当有自定义shader的设置UI时候用
    [CustomEditor]
}

看着比较抽象,这究竟是如何把Shader的渲染和代码联系起来呢?下面我通过一个例子来帮助你理清思绪。

初学成果

Shader "Unlit/Tutorial_Shader"
{
    Properties
    {
        _Colour ("Totally Rad Colour!", Color) = (1, 1, 1, 1)
        _MainTexture ("Main Texture", 2D) = "white" {}
        _DissolveTexture ("Dissolve Texture", 2D) = "white" {}
        _DissolveCutoff ("Dissolve Cutoff", Range(0, 1)) = 1
        _ExtrudeAmount ("Extrue Amount", float) = 0
    }
    SubShader
    {
        Pass
        {
            CGPROGRAM
            #pragma vertex vertexFunction
            #pragma fragment fragmentFunction

            #include "UnityCG.cginc"

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

            struct v2f
            {
                float4 position : SV_POSITION;
                float2 uv : TEXCOORD0;
            };

            // ****************************
            //Get our properties into CG
            // ****************************
            float4 _Colour;
            sampler2D _MainTexture;
            sampler2D _DissolveTexture;
            float _DissolveCutoff;
            float _ExtrudeAmount;

            v2f vertexFunction(appdata IN)
            {
                v2f OUT;
                IN.vertex.xyz += IN.normal.xyz * _ExtrudeAmount * sin(_Time.y);
                OUT.position = UnityObjectToClipPos(IN.vertex);
                OUT.uv = IN.uv;

                return OUT;
            }

            fixed4 fragmentFunction(v2f IN) : SV_TARGET
            {
                float4 textureColour = tex2D(_MainTexture, IN.uv);
                float4 dissolveColour = tex2D(_DissolveTexture, IN.uv);
                clip(dissolveColour.rgb - _DissolveCutoff);
                return textureColour * _Colour;
            }
            ENDCG
        }
    }
}

咱们先来看一段代码:

  • CGPROGRAM和ENDCG:CGPROGRAM 关键字用于标记Cg/HLSL代码段的开始。 ENDCG 关键字则标记这段代码的结束。在这里面你可以混写Cg和HLSL语言,这两种语言之间本身有很多共通之处

  • Properties代码块:里面定义的Property都可以在Unity Editor中找到。右键这个Shader然后创建材质,你就可以在材质的Inspector里面看到:

  • 代码中的两个Struct:

    • 首先看appdata,里面的三个字段后面的冒号加大写字母就示意了Unity要把POSITION(位置)、NORMAL(法线)等信息分别传递给这三个字段,并且 在vertextFunction中,形参是appdata IN,两者决定了一个可以拿到位置、材质、法线信息的appdata对象。#pragma vertex vertexFunction,这里告诉着色器编译器应该使用哪个函数作为顶点着色器的入口点

    • 然后是v2f,大体上和appdata一样,#pragma fragment fragmentFunction告诉编译器这个函数是片段着色器的入口。但仔细观察的同学可能发现了更多:这个“SV”意味着什么?

      SV_ 前缀是一个语义限定符,代表“System Value”。这个前缀用于标识那些特殊的变量,它们具有特定的系统级意义,而不仅仅是普通的用户定义数据。

到底有几种着色器?一下子顶点着色器一下子片段着色器脑袋都糊涂了(*´・д・)?

Shader大体上可以分为两类:

  • 简单来说表面着色器(Surface Shader) - 为你做了大部分的工作,只需要简单的技巧即可实现很多不错的效果。类比卡片机,上手以后不太需要很多努力就能拍出不错的效果。

  • 片段着色器(Fragment Shader) - 可以做的事情更多,但是也比较难写。使用片段着色器的主要目的是可以在比较低的层级上进行更复杂(或者针对目标设备更高效)的开发。

为什么有的地方在着色器代码中使用SV_前缀,而有的地方则不使用呢?这其实与着色器中的数据类型和用途有关。

SV_(System Value)前缀主要用于标记那些对渲染管线至关重要的系统级数据。例如:

  • SV_Position用于顶点着色器输出和片段着色器输入的顶点位置,它告诉渲染管线如何将顶点映射到屏幕上。

  • SV_Target用于片段着色器的输出,表示最终渲染到屏幕的像素颜色。

然而,并不是所有着色器变量都需要这样的系统级标记。普通的顶点数据,如位置(POSITION)、纹理坐标(TEXCOORD0)和法线(NORMAL),是直接从模型的网格数据中获取的,它们只是普通的用户定义数据,因此不使用SV_前缀。这样的区分确保了渲染管线可以正确地理解和处理着色器中的不同类型数据。

实现的效果

通过修改Dissolve Cutoff的值,我们可以实现这样的效果:
这样变成...
这样!一个快要溶解的碗

接下来...

现在你已经对Shader有了一定了解,如果你还想继续深入学习,可以去找一些教程自学!或者等我的下一篇夸父说(随缘更新(´・ω・`)。

Life is a Rainmeter