Unity Shader学习记录(八)

Unity Shader学习记录(八)

  大部分情况下,Shader的运行过程是与时间无关的静态过程,换句话说游戏进行过程中渲染的结果不会有什么变化;动态光影或许是比较典型的例外,但它们和游戏运行时间依然没有什么直接的联系,仅仅与场景的变化有关。
  但有些情况下,能将Shader的渲染流程与游戏运行的时间建立联系是有益处的,比如希望渲染出随着时间变化而变化的效果,水波或者移动等。因此Unity的Shader系统中提供了一个参数用于描述当前游戏的运行时间,如果在Shader的渲染过程中将运算与该参数联系到一起,就能让Shader拥有随着时间变化而变化的能力。


Shader序列帧动画

  序列帧动画是一种很常见的动画形式,在Unity中可以通过Animator组件来管理以及播放序列帧动画,如果使用精灵的方式导入序列帧动画的资源,那么在2D或者3D场景中都可以使用,只需要按照规律分割序列帧资源,制作完整动画并加入Animator即可。
  而如果序列帧动画的资源是以2D纹理的方法导入的,那么它就不能用在Sprite组件上,在这种情况下要正确地播放序列帧动画就必须使用合适的Shader来进行采样的渲染了。
  播放序列帧动画的Shader原理很简单,因为序列帧动画的资源往往都是一张按照N*M规律平铺好的图像,每个格子里都是一帧动画,整个图片合起来就是整个动画;因此播放的原理就是按照时间规律,依次渲染每个格子里的动画图像,连起来之后就形成了动画。
  为了达到这个效果,必须知道当前Unity引擎的执行时间,也就是在Shader中使用_Time变量。
  具体代码如下

Shader "Custom/ImageSequenceAnimShader" {
    Properties {
        _Color ("Color", Color) = (1,1,1,1)
        _MainTex ("Albedo (RGB)", 2D) = "white" {}
        _HorizontalAmount ("Horizontal Amount", Float) = 4
        _VerticalAmount ("Vertical Amount", Float) = 4
        _Speed ("Speed", Range(1, 100)) = 30
    }
    SubShader {
        Tags { "Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" }
        Pass {
            Tags { "LightMode"="ForwardBase"}
            ZWrite Off
            Blend SrcAlpha OneMinusSrcAlpha

            CGPROGRAM

            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"

            fixed4 _Color;
            sampler2D _MainTex;
            float4 _MainTex_ST;
            float _HorizontalAmount; // 素材有多少行
            float _VerticalAmount; // 素材有多少列
            fixed _Speed;

            struct a2v {
                float3 vertex : POSITION;
                float4 texcoord : TEXCOORD;
            };

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

            v2f vert(a2v v) {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
                return o;
            }

            fixed4 frag(v2f i) : SV_TARGET {
                float time = floor(_Time.y * _Speed); // _Time.y表示自该场景加载后经过的时间,做乘法得到与速度相关的虚拟时间
                float row = floor(time / _HorizontalAmount); // 通过将游戏运行的虚拟时间与总行数做除法可以得到当前时刻所指向的行
                float column = time - row * _HorizontalAmount; // 余数就是列
                half2 uv = i.uv + half2(column, -row); // 注意row的符号,这是因为Unity对纹理的采样坐标是按照左下角为原点的基准进行的,而往往序列帧动画的顺序是从左上角开始
                // 下面裁剪采样坐标
                uv.x /= _HorizontalAmount;
                uv.y /= _VerticalAmount;
                fixed4 c = tex2D(_MainTex, uv);
                c.rgb *= _Color;
                return c;
            }

            ENDCG
        }
    }
    FallBack "Transparent/VertexLit"
}

  通过这样一个Shader,当它对一个Quad模型起作用时,只要给MainTex挂上一个序列帧动画的纹理资源,然后设置好行数和列数,给定一个播放速度,启动游戏后便能看到动画效果了。

滚动的背景图

  无限滚动的背景是在卷轴游戏中常见的一种背景渲染方式,通过将背景无限地重复移动,营造出速度感和战斗氛围。在实践中如果用Sprite的方式来制作这种背景,那么需要使用脚本比较精确地控制Sprite的位移和重置,这样才能做到无限重复。
  而Shader通过对Time参数的使用也能比较好地做到这种无限滚动的动画效果,基本思想自然就是让采样坐标随着时间变化而变化,同时对变后的坐标进行裁剪。
  具体代码如下

Shader "Custom/ScrollingBGShader" {
    Properties {
        _MainTex ("Base Layer", 2D) = "white" {}
        _DetailTex ("2nd Layer", 2D) = "white" {}
        _ScrollX ("Base Layer Scroll Speed", Float) = 1.0
        _Scroll2X ("2nd Layer Scroll Speed", Float) = 1.0
        _Multiplier ("Layer Multiplier", Float) = 1.0
    }
    SubShader {
        Tags { "Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" }
        Pass {
            Tags { "LightMode"="ForwardBase"}
            ZWrite Off
            Blend SrcAlpha OneMinusSrcAlpha

            CGPROGRAM

            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"

            sampler2D _MainTex; // 背景纹理采样器
            sampler2D _DetailTex; // 前景纹理采样器
            float4 _MainTex_ST;
            float4 _DetailTex_ST;
            float _ScrollX;
            float _Scroll2X;
            fixed _Multiplier;

            struct a2v {
                float3 vertex : POSITION;
                float4 texcoord : TEXCOORD;
            };

            struct v2f {
                float4 pos : SV_POSITION;
                float4 uv : TEXCOORD0;
            };

            v2f vert(a2v v) {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                // 分别将前景和背景的采样坐标保存在uv的不同分量里,如果有需要还可以使用更多的层数,也需要更多的坐标
                o.uv.xy = TRANSFORM_TEX(v.texcoord, _MainTex) + frac(float2(_ScrollX, 0.0) * _Time.y);
                o.uv.zw = TRANSFORM_TEX(v.texcoord, _DetailTex) + frac(float2(_Scroll2X, 0.0) * _Time.y);
                return o;
            }

            fixed4 frag(v2f i) : SV_TARGET {
                fixed4 firstLayer = tex2D(_MainTex, i.uv.xy);
                fixed4 secondLayer = tex2D(_DetailTex, i.uv.zw);
                fixed4 c = lerp(firstLayer, secondLayer, secondLayer.a); // 使用lerp函数来得到过渡色效果
                c.rgb *= _Multiplier;
                return c;
            }

            ENDCG
        }
    }
    FallBack "VertexLit"
}

  可以看到在顶点运算函数中对纹理进行了偏移采样,使用frac函数来对采样坐标进行裁剪,超过最大值的坐标会被裁剪到开始的时候。
  将该Shader用于一片比较大的Quad对象,然后给材质赋值背景纹理和前景纹理,启动游戏后就能看到一个无限重复的滚动背景图了。

顶点动画

  所谓顶点动画是指的一种直接改变顶点来形成动画效果的技术,在实际的使用中,顶点动画常见于一些局部的,只需要自身扰动便可以达到效果的动画,比如旗帜飘扬或者水面波纹。
  如果要在Shader中实现一个顶点动画效果,必须准确地考虑到动画的实际需求,因地制宜,比如2D卡通风格的水流,它就可以使用简单的正弦波计算来实现。
  代码如下

Shader "Custom/2DWater" {
    Properties {
        _MainTex ("Albedo (RGB)", 2D) = "white" {}
        _Color ("Color", Color) = (1,1,1,1)
        _Magnitude ("Distortion Magnitude", Float) = 1
        _Frequency ("Distortion Frequency", Float) = 1
        _InvWaveLength ("Distortion Inverse Wave Length", Float) = 10
        _Speed ("Speed", Float) = 0.5
    }
    SubShader {
        Tags { "Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" "DisableBatching"="True" }
        Pass {
            Tags { "LightMode"="ForwardBase"}
            ZWrite Off
            Blend SrcAlpha OneMinusSrcAlpha
            Cull Off

            CGPROGRAM

            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"

            sampler2D _MainTex;
            float4 _MainTex_ST;
            fixed4 _Color;
            float _Magnitude; // 波浪的变形幅度,幅度越大,波峰波谷的差距越大
            float _Frequency; // 波浪的频率,数值越大,变化的频率越快
            float _InvWaveLength; // 波长的倒数,数值越大单个波所占长度越小
            float _Speed; // 变化速度参数

            struct a2v {
                float3 vertex : POSITION;
                float4 texcoord : TEXCOORD;
            };

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

            v2f vert(a2v v) {
                v2f o;
                float4 offset;
                offset.yzw = float3(0.0, 0.0, 0.0);
                // 偏移量的计算,可以看出这里就是简单地使用了正弦函数,频率和时间是主要的变量参数,让顶点坐标参与计算则是为了让不同坐标上的偏移结果不同,也就是让水流看起来真的在动
                offset.x = sin(_Frequency * _Time.y + v.vertex.x * _InvWaveLength + v.vertex.y * _InvWaveLength + v.vertex.z * _InvWaveLength) * _Magnitude;
                o.pos = UnityObjectToClipPos(v.vertex + offset); // 顶点坐标加上偏移量,这样一来片元运算中拿到的位置就是变形后的位置了
                o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
                o.uv += float2(0.0, _Time.y * _Speed); // 这里是很简单的纹理位移,也是水流动画的一个组成部分
                return o;
            }

            fixed4 frag(v2f i) : SV_TARGET {
                fixed4 c = tex2D(_MainTex, i.uv);
                c.rgb *= _Color.rgb;
                return c;
            }

            ENDCG
        }
    }
    FallBack "Transparent/VertexLit"
}

  使用这样的Shader,再将其赋予一个平面模型对象,使用卡通表现的水流贴图,调整好参数后它就能在游戏运行时流动起来了。
  需要注意的是,在使用了这样的Shader后,虽然渲染时是按照改变后的模型进行的渲染,但是由于Shader里没有阴影相关的ShadowCaster,所以Unity会去Callback中寻找,那么投射的阴影就会出错,因为Callback中的ShadowCaster并没有对模型顶点做出任何改变。
  解决方法也很简单,自定义一个ShadowCaster就行了,在其中按照原本的ShadowCaster代码,加上顶点变化的计算过程,得到的阴影就会跟着顶点动画而变化了。

广告牌效果

  广告牌效果也是一类很常见的渲染效果,它的外在表现是“根据视角方向来旋转被渲染的模型,使得模型的‘正面’始终对准视线方向”,在有些游戏中,这项技术被用于制作2D表现的植被和远景。
  这种技术的本质就是旋转,放到计算机图形图像中就是指构造旋转矩阵,通常来说一个旋转矩阵需要至少三个基向量,包括法线,“向上”的方向,“向右”的方向;针对广告牌技术而言,还有一个额外的参数需求,锚点坐标。
  由此可见,广告牌技术的重点就在于锚点的确定和基向量的构造。
  代码如下

Shader "Custom/BillboardShader" {
    Properties {
        _Color ("Color", Color) = (1,1,1,1)
        _MainTex ("Albedo (RGB)", 2D) = "white" {}
        _VerticalBillboarding ("Vertical Restraints", Range(0,1)) = 1
    }
    SubShader {
        Tags { "Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" "DisableBatching"="True" }
        Pass {
            Tags { "LightMode"="ForwardBase"}
            ZWrite Off
            Blend SrcAlpha OneMinusSrcAlpha
            Cull Off

            CGPROGRAM

            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"
            #include "Lighting.cginc"

            sampler2D _MainTex;
            float4 _MainTex_ST;
            fixed4 _Color;
            fixed _VerticalBillboarding; // 方向绑定情况,如果为1则绑定法向量始终指向视角方向,如果为0则固定垂直方向朝上(0,1,0)

            struct a2v {
                float3 vertex : POSITION;
                float4 texcoord : TEXCOORD;
            };

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

            v2f vert(a2v v) {
                v2f o;
                float3 center = float3(0,0,0); // 确定锚点,此处为模型中心点
                float3 viewer = mul(unity_WorldToObject, float4(_WorldSpaceCameraPos, 1)); // 计算视角方向,通过摄像机坐标获得
                float3 normalDir = viewer - center; // 法向量从锚点指向视角方向
                // 这里就是根据参数决定固定什么方向的时候了,如果参数为1则法向量固定,否则法向量的Y分量变化,后续的计算会有所不同
                normalDir.y = normalDir.y * _VerticalBillboarding;
                normalDir = normalize(normalDir); // 归一化法向量
                float3 upDir = abs(normalDir.y > 0.999 ? float3(0,0,1) : float3(0,1,0)); // 根据法向量的Y分量决定“向上”方向
                float3 rightDir = normalize(cross(upDir, normalDir)); // 叉乘得出“向右”的方向
                upDir = normalize(cross(normalDir, rightDir)); // 由于之前的“向上”方向是手动确定的,因此在这里通过叉乘得到更精确的结果
                float3 centerOffs = v.vertex.xyz - center;
                // 这里就是最终的顶点旋转过程,通过计算得到的一组正交基和锚点偏移量一起参与运算
                float3 localPos = center + rightDir * centerOffs.x + upDir * centerOffs.y + normalDir * centerOffs.z;
                o.pos = UnityObjectToClipPos(float4(localPos, 1));
                o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
                return o;
            }

            fixed4 frag(v2f i) : SV_TARGET {
                fixed4 c = tex2D(_MainTex, i.uv);
                c.rgb *= _Color.rgb;
                return c;
            }

            ENDCG
        }
    }
    FallBack "Transparent/VertexLit"
}

  将以上的Shader应用到Quad模型对象,设置纹理后即可看到广告牌效果,通过调节_VerticalBillboarding参数可以让渲染结果发生变化,这种变化的根源就在于对法方向和“向上”方向的不同计算。
  值得注意的是,这个Shader仅能应用于Quad模型而无法用于Plane模型,这是因为在基向量的计算过程中默认了模型的顶点是处于“竖直排列”的状态,Plane是“水平排列”的状态,因此要在Plane上应用广告牌效果需要不同的计算方法。
  以上就是关于Shader能实现的一些特别效果的解析,其中有与游戏执行时间相关联的动画效果,也有和视角方向关联的广告牌效果。后续会解析关于后处理效果的Shader

猜你喜欢

转载自blog.csdn.net/soul900524/article/details/79930786