【猫猫的Unity Shader之旅】之可编程 Shader初步

  可编程Shader,通常也被叫做Vertex&Fragment Shader,是比Surface Shader更灵活的一种Unity Shader形态。使用可编程Shader,可以实现对最终效果的更精确的控制,然而,代价就是需要关注更多的细节。

一个最简单的可编程Shader

  作为第一个例子,我们实现了一个简单的自发光效果,它甚至没有Surface Shader的第一个例子华丽,因为没有Lambert这样的内置的光照模型可以使用了。在可编程Shader里,光照模型需要我们自己去实现,不过我们也不必沮丧,可编程Shader的潜力不是Surface Shader可以比的。

Shader "Custom/Emission" {
    Properties {
        _EmissionColor ("Emission Color", Color) = (1, 1, 1, 1)
    }
    SubShader {
        Tags { "RenderType"="Opaque" }
        LOD 200

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            uniform float4 _EmissionColor;

            float4 vert(float4 vertPos : POSITION) : SV_POSITION
            {
                return mul(UNITY_MATRIX_MVP, vertPos);
            }

            float4 frag(float4 vertPos : SV_POSITION) : COLOR
            {
                return _EmissionColor;
            }
            ENDCG
        }
    } 
    FallBack "Diffuse"
}

  最终的效果是一个纯色的Shader。

  这里写图片描述

  这个Shader和之前的Surface Shader还是有很大不同的。第一个大的不同就是Pass块,Pass块实际上表示一个渲染过程,也就是说,经过一个Pass块,我们就用给定的信息计算了一次所有片段的颜色。一个SubShader中可能包含多个Pass块,最常见的情况就是需要对模型的正反两面渲染不同的效果,这时我们就应该使用两个Pass分别对正反两面进行处理。

  第二个不同的地方就是#pragma vertex vert和#pragma fragment frag。这也是可编程Shader被称为Vertex&Fragment Shader的原因。在可编程Shader中,渲染过程被分成了两个主要的部分,也就是顶点阶段片段阶段,也可以理解为对应管线的转换光照阶段。忘记的话可以看下这里。顶点阶段通过运算得到经过变换和投影处理过的顶点信息(包括颜色、位置、贴图坐标等等),然后这些信息通过插值变为片段信息,所有的片段再经过片段阶段的处理,处理后的片段信息通过混合规则计算后写入各缓冲区。

  要使用Properties中定义的属性,需要在Cg代码中再定义一下,这时候需要使用uniform关键字。uniform关键字实际上表示需要外部传入信息的变量,我们的属性其实也相当于Unity传入Shader的信息。

  在vert中,我们使用了语义。这有什么用呢?我们来假设一下,如果没有语义,vert方法是这样的:

float4 vert(float4 vertPos)
{
    return mul(UNITY_MATRIX_MVP, vertPos);
}

  OK,这是现在的vert方法。模型的每一个顶点要经过这个方法去处理,但是我们并不知道应该把什么信息传入vert,是颜色?位置?还是其他的?同理,对于vert返回的信息,也无法知道具体是表示什么。这时候就需要用冒号+语义来指明信息的含义,POSITION就表示模型需要把顶点的位置(局部坐标)传给vert,SV_POSITION表示vert返回的信息也表示位置。在vert中,我们返回顶点位置经过模型、视图、投影变换后的结果,MVP就是Model、View、Projection的缩写。

  经过顶点阶段后顶点信息需要经过插值,比如一条直线原本是用两个顶点表示,现在经过插值计算出中间的10个点,具体的数目跟直线的位置和屏幕分辨率等都有关系。这个过程也叫光栅化。这个阶段我们是没有办法干预的,只能对光栅化产生的片段(fragment)进行处理。

  frag方法同样需要语义。我们需要把vert计算出的位置信息(准确说是又经过光栅化后产生的片段的位置信息)通过参数传递进来,所以必须保证参数的语义和vert返回的语义是相同的,也是SV_POSITION。对于frag来说,它需要返回片段的颜色,所以语义是COLOR。这里我们简单地返回自发光颜色。

使用结构体在Vertex和Fragment间传递参数

  当Vertex需要传递多条信息给Fragment时(大多数情况如此),我们要使用结构体,这有点类似Surface Shader里面的Input结构体,使用结构体的另一个好处是可以保证语义的对应上不会出错。当Vertex需要多个参数时,我们也可以使用结构体,或者使用内置的结构体。内置结构体类型可以在UnityCG.cginc中找到:

struct appdata_base {
    float4 vertex : POSITION;
    float3 normal : NORMAL;
    float4 texcoord : TEXCOORD0;
};

struct appdata_tan {
    float4 vertex : POSITION;
    float4 tangent : TANGENT;
    float3 normal : NORMAL;
    float4 texcoord : TEXCOORD0;
};

struct appdata_full {
    float4 vertex : POSITION;
    float4 tangent : TANGENT;
    float3 normal : NORMAL;
    float4 texcoord : TEXCOORD0;
    float4 texcoord1 : TEXCOORD1;
    fixed4 color : COLOR;
#if defined(SHADER_API_XBOX360)
    half4 texcoord2 : TEXCOORD2;
    half4 texcoord3 : TEXCOORD3;
    half4 texcoord4 : TEXCOORD4;
    half4 texcoord5 : TEXCOORD5;
#endif
};

  这次,我们重写一下上次的变色效果,当然,这版还不带光照:

Shader "Custom/ChangeColor" {
    Properties {
        _Color1 ("Color 1", Color) = (1, 1, 1, 1)
        _Color2 ("Color 2", Color) = (1, 1, 1, 1)
        _BorderColor ("边界颜色", Color) = (1, 1, 1, 1)
        _BorderThick ("边界厚度", Range(0.01, 3)) = 0.1
        _Bulge ("边界凸出程度", Range(0.01, 0.3)) = 0.08
        _CullPos ("裁剪球位置", Range(0, 3)) = 3
        _CullRadius ("裁剪球半径", Range(0.1, 5)) = 1
    }
    SubShader {
        Tags { "RenderType"="Opaque" }
        LOD 200

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"

            uniform fixed4 _Color1;
            uniform fixed4 _Color2;
            uniform fixed4 _BorderColor;
            uniform float _BorderThick;
            uniform float _Bulge;
            uniform float _CullPos;
            uniform float _CullRadius;

            struct v2f
            {
                float4 vertPos : SV_POSITION;
                float4 worldPos : TEXCOORD0;    //也可以使用其他语义,因为传递过程不会被使用
            };

            inline float GetDis(float3 pos)
            {
                return distance(pos, float3(_CullPos, _CullPos, _CullPos));
            }

            v2f vert(appdata_base v)
            {
                v2f o;
                //这里的计算顺序需要注意一下,我们需要先计算出worldPos才能做判断
                //但是vertPos的计算必须在后面否则就没有效果了
                o.worldPos = mul(_Object2World, v.vertex);
                if(abs(GetDis(o.worldPos.xyz) - _CullRadius) <= _BorderThick / 2)
                {
                    v.vertex.xyz += v.normal * _Bulge;
                }
                o.vertPos = mul(UNITY_MATRIX_MVP, v.vertex);
                return o;
            }

            fixed4 frag(v2f v) : COLOR
            {
                fixed4 tint;
                if(abs(GetDis(v.worldPos.xyz) - _CullRadius) <= _BorderThick / 2)
                {
                    tint = _BorderColor;
                }
                else if(GetDis(v.worldPos.xyz) > _CullRadius)
                {
                    tint = _Color1;
                }
                else
                {
                    tint = _Color2;
                }
                //类似自发光直接返回颜色
                return tint;
            }
            ENDCG
        }
    } 
    FallBack "Diffuse"
}

  效果如图:

  这里写图片描述

结束语

  世界上的事总是这样,有优势就会有对应的短板。就像Unity Shader,选择了Surface Shader就得面对效果和效率上的不如意,选择可编程Shader就得面对开发过程不够方便。没有什么一定是对的,就看当时需要的是什么,这也许就是选择的意义。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值