一、创建Shader

在Unity中创建Shader,有Stander Shader、Unlit Shader、Image Effect Shader、Compute Shader、Ray Tracing Shader

Stander Shader:Unity内置的标准着色器,支持高光、透明度、法线贴图等特性,比如金属,塑料,木材,皮肤,也支持光照、阴影、反射、折射、透明雾化等...

Unlit Shader:不受光照影响的着色器,适用于简单的2d,2d游戏,或者性能优化,比如常用于UI、例子、线条,比较轻量级,在移动设备应用比较多

Image Effect Shader:用于在渲染场景后对画面的后处理,比如实现模糊,色彩调整,边缘检测等效果

Compute Shader:一种运行在GPU上的着色器,可以用于高性能的计算任务

Ray Tracing Shader:实现光追的着色器,实现高质量的反射,折射,阴影等

其中前两种着色器被广泛使用

二、编写

创建一个新的标准着色器,分块进行分析

Shader "Custom/MyFirstShader"
{
    //Properties声明了着色器的属性变量。这个着色器有四个属性,分别是颜色,二维纹理,光泽度,金属度
    Properties
    {
        //颜色属性
        _Color ("Color", Color) = (1,1,1,1)
        //二维纹理属性
        _MainTex ("Albedo (RGB)", 2D) = "white" {}
        //光泽度
        _Glossiness ("Smoothness", Range(0,1)) = 0.5
        //金属质感
        _Metallic ("Metallic", Range(0,1)) = 0.0
    }

     //这是一个子着色器,它定义了渲染该着色器时使用的材质类型和LOD
    SubShader
    {
        
        Tags { "RenderType"="Opaque" }

        //定义了该子着色器的LOD级别,它决定了在渲染远处的物体时使用的着色器质量。
        LOD 200


        //这个块包含了使用Cg/HLSL编写的着色器程序
        CGPROGRAM

        // 该段指定使用标准的表面着色器,并开启去所有类型光源的阴影
        #pragma surface surf Standard fullforwardshadows

        // 着色器模型3.0
        #pragma target 3.0

         //定义一个采样器,用于采样该着色器主纹理
        sampler2D _MainTex;

        //定义了一个输入结构体,它包含了从几何体传递到着色器的数据。
        struct Input
        {
            float2 uv_MainTex;
        };

        half _Glossiness;

        half _Metallic;

        fixed4 _Color;

        // 这两行代码包含了一个实例缓冲区,允许使用着色器来渲染大量的实例
        UNITY_INSTANCING_BUFFER_START(Props)
        UNITY_INSTANCING_BUFFER_END(Props)

        //这个函数使用传入的几何体数据和着色器变量来计算表面的属性输出
        void surf (Input IN, inout SurfaceOutputStandard o)
        {
            // 这行代码从主纹理中采样颜色值,并乘以着色器中定义的颜色值
            fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;

            //设置表面的属性输出,包括反射率、光泽度、金属度和透明度
            o.Albedo = c.rgb;
            o.Metallic = _Metallic;
            o.Smoothness = _Glossiness;
            o.Alpha = c.a;
        }
        ENDCG
    }
    //如果当前着色器不能被使用,就会使用Unity默认的"Diffuse"着色器来代替。这样可以确保在任何情况下都能够正常渲染场景。
    FallBack "Diffuse"
}

可以看出主要有两大部分

1:Properties(属性)

一个Shader可以包含多个属性,这些属性是可以暴漏在Inspector窗口的,常见的属性包括:

  • _Color:颜色属性,可以用来定义材质的基本颜色

在Unity中,颜色可以用四个字母RGBA来表示,每个值都在0-1之间

R:红色的强度

G:绿色的强度

B:蓝色的强度

A:透明度,0-1逐渐不透明 

  • _MainTex:纹理属性,可以用来定义材质的主纹理,常见的纹理贴图如下

BumpMap:设置法线贴图

MatallicGlossMap:设置金属度和光滑度贴图

ParallaxMap:视差贴图

OcclusionMap: 用于设置遮挡贴图。

EmissionMap: 用于设置发光贴图。

DetailMask: 用于设置细节纹理的遮罩贴图。

拿MainTex举例,在本例中我们的主贴图的设置如下:

_MainTex ("Texture", 2D) = "white" {}

Albedo(RGB):Albedo表示物体的基础颜色,RGB表示纹理包含RGB通道的颜色信息

2D是表示这是2D纹理

white是纹理的默认值

  • _Glossiness:光滑度属性,可以用来控制材质的表面光滑度

Glossiness 是用于控制物体表面光泽度的属性,值范围为 0 到 1,0 表示非常粗糙(无光泽),1 表示非常光滑(高光泽)。

  • _Metallic:金属度属性,可以用来控制材质的金属度

金属度和上面的光泽度都是可以在0-1范围进行调整的

  • _BumpMap:凹凸贴图属性,可以用来定义材质的凹凸程度
_BumpMap ("Normal Map", 2D) = "bump" {}
    _BumpScale ("Normal Scale", Range(0, 1)) = 1.0
  • _Parallax:视差贴图属性,可以用来定义材质的视差效果
_ParallaxMap ("Parallax Map", 2D) = "white" {}
        _Parallax ("Parallax", Range(0, 0.2)) = 0.05
  • _EmissionColor:自发光颜色属性,可以用来定义材质的自发光颜色
_EmissionColor ("Emission Color", Color) = (0, 0, 0, 0)
  • _RimColor:边缘颜色属性,可以用来定义材质边缘的颜色
  • _RimPower:边缘亮度属性,可以用来控制材质边缘的亮度
_RimPower ("Rim Power", Range(0.1, 10.0)) = 3.0
        _RimColor ("Rim Color", Color) = (1,1,1,1)

在Properties中加如RimPower和RimColor,控制边缘的颜色和亮度

在Properties中加入边缘属性以后,还需要在子着色器中进行使用

  • _OutlineColor:轮廓线颜色属性,可以用来定义材质的轮廓线颜色
  • _OutlineWidth:轮廓线宽度属性,可以用来定义材质的轮廓线宽度

比如它们可以这么设置:

Properties
    {
        // 设置轮廓宽度
        _OutlineWidth ("Outline Width", Range(0, 0.1)) = 0.01
        // 设置轮廓颜色
        _OutlineColor ("Outline Color", Color) = (0,0,0,1)
    }

就代表了轮廓的宽度和颜色

2:SubShader(子着色器)

在Shader文件中,Properties中声明的一些属性,还需要在子着色器中进行实现(比如贴图和颜色),而比如金属和光滑度则不需要,因为已经默认实现了

子着色器中是通过CG程序访问的,CG程序是unity内置的着色器语言,用于编写GPU程序

在一个子着色器中,我们同时使用了CG/HLSL语言来编写

同样一个Shader也可以包含多个子着色器

我们举一个边缘发光的例子来详细解释一下子着色器的作用

Shader "Custom/Outline" {
    Properties {
        _OutlineColor ("Outline Color", Color) = (1,1,1,1)
        _OutlineWidth ("Outline Width", Range(0, 0.1)) = 0.01
    }

    SubShader {
        Tags { "RenderType"="Opaque" }

        Pass {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            float4 _OutlineColor;
            float _OutlineWidth;

            struct appdata {
                float4 vertex : POSITION;
            };
            struct v2f {
                float4 vertex : SV_POSITION;
            };

            v2f vert (appdata v) {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);

                // Expand the vertex position by the outline width in screen space
                o.vertex.xy += _OutlineWidth * o.vertex.w * UnityObjectToClipPos(float4(1, 1, 0, 0));

                return o;
            }

            fixed4 frag (v2f i) : SV_Target {
                // If the pixel is not on the edge, make it transparent
                if (ddx(i.vertex.xy) != 0 || ddy(i.vertex.xy) != 0) {
                    discard;
                }

                // Otherwise, set the pixel color to the outline color
                return _OutlineColor;
            }
            ENDCG
        }
    }

    FallBack "Diffuse"
}

这段代码中,我们定义了两个属性,一个是边缘的深度,一个是边缘的颜色

然后Tag选择了RenderType(渲染类型),其实在Shader里面我们有很多种的Tag可以选择,而且在Tag里面可以多选,比如:

Tags {
    "Queue"="Transparent"
    "RenderType"="Opaque, MyRenderType"
}

Tag有很多种,常见的Tag比如:

  渲染类型(RenderType)、渲染队列(Queue)、光照模式(Lighting)、渲染状态(RenderState)

而RanderType有如下几种:

Opaque:用于不透明的对象和材质

Transparent:用于透明对象和材质。

TransparentCutout:用于具有透明度的对象,但其透明度可以由一个阈值来切断。

Background:用于背景图像,一般是不透明的。

Overlay:用于叠加材质,通常用于添加光晕或其它效果。

TreeOpaque:用于树木对象的不透明部分。

TreeTransparentCutout:用于树木对象的透明部分。

TreeBillboard:用于树木对象的Billboard部分。

Queue是来指定绘制顺序和优先级的,有如下几种:

BackGround:背景,通常是天空和或者其他背景元素

Geomety:一般的几何物体,包含场景中的大部分物体

AlphaTest、Transparent、Overley(覆盖物)

Lighting表示渲染顺序,有前置渲染和延迟渲染

Forward渲染是每个物体都分别渲染,并且每个像素被光照多次,适用于移动端和低端PC

Deferred Rendering是所有集合体都会被先渲染一次,然后光照信息被存储在G-buffer中,再对G-buffer进行光照计算,这种方法需要更高的GPU

RenderState表示渲染的状态,有如下几种

Cull:剔除状态,可选Back,Front,Off三种剔除状态

ZTest:深度测试状态,可选Less,Greater,LEqual

ZWrite:深度写入状态,可选On/Off

Blend:混合状态可选Zero,One,SrcColor,EstColor等

Offset:偏移状态,可选Factor,Units

然后在子着色器中我们定义了一个Pass,可以说Pass是我们与GPU进行沟通的桥梁,它包含一组着色器程序、渲染状态和其他参数,表示一次渲染的操作

首先:#pragma vertex vert
           #pragma fragment frag

这两个指令用于指定哪些函数应该作为顶点着色器和比像素着色器

顶点着色器计算每个顶点的最终位置和纹理信息,颜色

像素着色器则为每个像素计算最终的颜色值

除了顶点着色器和像素着色器外,还有几何着色器(Geometey shader)、外壳着色器(hull shader)、细分着色器(Domain shader)、计算着色器(Compute shader)

声明的方法就是#pragma + 着色器名称 +自己的命名

其次:float4 _OutlineColor;
           float _OutlineWidth;

也就是在属性中声明的属性值,这里要在着色器中,使用所以需要在此处声明一下

接着:

struct appdata {
                float4 vertex : POSITION;
            };

声明一个叫appdate的结构体,里面有一个名为vertex的float4的变量,Position表示这个变量是存储该顶点的信息位置,在实际的顶点数据中,可能不仅仅有position,还可能会有法线,颜色等等,都可以在该结构体中声明,比如:

struct appdata {
    float4 vertex : POSITION;
    float3 normal : NORMAL;
    float4 color : COLOR;
};

接着:

struct v2f {
                float4 vertex : SV_POSITION;
            };

这个结构体和上一个类似,不过这里的SV_POSITION是表示经过顶点着色器计算后的顶点屏幕空间位置

继续:

v2f vert (appdata v) {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.vertex.xy += _OutlineWidth * o.vertex.w * UnityObjectToClipPos(float4(1, 1, 0, 0));

                return o;
            }

首先这是一个名为vert,返回值为v2f的顶点着色器函数,接受的参数是appdate类型,我们之前定义了一个appdate的结构体,所以说参数是存储的顶点数据

首先实例化了经过顶点着色器计算后的v2f类型的结构体o,UnityObjectToClipPos()是一个Unity函数,它将对象空间中的点转换为剪裁空间中的点,在这里我们使用该函数将传入的顶点位置从对象空间转换为剪裁空间,并将其保存到 o.vertex 中,作为 vert() 函数的返回值。

#这里我们要声明一点,为什么要进行空间裁剪呢?

3D图形学中,我们需要把三维坐标系中的顶点投影到二维平面,这个过程叫投影,在图形学中,通常采用裁剪空间( Clip Space)来进行投影,举个例子,裁剪空间其实就是一个类似摄像机镜头的视锥体,这个视锥体的顶点就是摄像机的位置,当场景中的物体进入这个视锥体时,就会被渲染到屏幕上。

最后一步:

fixed4 frag (v2f i) : SV_Target {
                // If the pixel is not on the edge, make it transparent
                if (ddx(i.vertex.xy) != 0 || ddy(i.vertex.xy) != 0) {
                    discard;
                }

                // Otherwise, set the pixel color to the outline color
                return _OutlineColor;
            }
            ENDCG

这是一个名为frag的函数,参数就是经过处理顶点着色器的v2f结构体,也就是经过了空间裁剪的顶点屏幕空间位置,返回值是一个fixed4 数据类型,包含四个浮点数的向量,使用定点数而不是浮点数来表示颜色和坐标等数据可以提高性能和精度,因为定点数在硬件中的处理速度比浮点数快,并且可以避免一些精度误差。

:SV_Target表示,将该颜色输出到当前渲染目标。

if (ddx(i.vertex.xy) != 0 || ddy(i.vertex.xy) != 0) {
                    discard;
                }

这段代码是用来判断像素是否处于边缘,如果不是的话,就通过discard让这个像素不被绘制,也就是只绘制边缘,ddx和ddy是HLSL内置的函数,用于计算像素位置的梯度