小言_互联网的博客

《Unity Shader入门精要》学习笔记第11章 让画面动起来

242人阅读  评论(0)

本文章用于帮助自己学习,因此只记录一些个人认为比较重要或者还不够熟悉的内容。
原作者:http://blog.csdn.net/candycat1992/article/

第十一章 让画面动起来

11.1 Unity Shader中的内置变量(时间篇)

动画效果往往都是把时间添加到一些变量的计算中,以便在时间变化时画面也可以随之变化。
Unity Shader提供了一系列关于时间的内置变量来允许我们方便地在Shader中访问运行时间,实现各种动画效果。下表给出了这些内置的时间变量。

11.2 纹理动画

在各种资源都比较局限的移动平台上,我们往往会使用纹理动画来代替复杂的粒子系统等模拟各种动画效果。

11.2.1序列帧动画

最常见的纹理动画之一就是序列帧动画。序列帧动画的原理非常简单,就是依次播放一系列关键帧图像,当播放速度达到一定数值时,看起来就是一个连续的动画。它的优点在于灵活性很强,我们不需要进行任何物理计算就可以得到非常细腻的动画效果

要想实现序列帧动画,我们先要提供一张包含了关键帧图像的图像。如图所示。

代码:

Shader "Unity Shaders Book/Chapter 11/ImageSequenceAnimation"
{
    Properties
    {
        _Color("Color Tint",Color) = (1,1,1,1)
        //包含所有关键帧图像的纹理
        _MainTex ("Image Sequence", 2D) = "white" {}
        //水平方向包含的关键帧图像个数
        _HorizontalAmount("Horizontal Amount",Float) = 4
        //竖直方向包含的关键帧图像个数
        _VerticalAmount("Vertical Amount",Float) = 4
        //控制序列帧动画播放速度
        _Speed("Speed",Range(1,100)) = 30
    }
    SubShader
    {
        //由于序列帧图像通常是透明纹理,
        //我们需要设置Pass的相关状态以渲染透明效果
        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;
            float _Speed;


            struct a2v
            {
                float4 vertex : POSITION;
                float2 texcoord : TEXCOORD0;
            };

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



            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
            {
                //_Time.y是自该场景加载后经过的时间,floor函数取整
                float time = floor(_Time.y * _Speed);
                //求出当前对应的行索引
                float row = floor(time / _HorizontalAmount);
                //余数就是列索引
                float column = time - row * _HorizontalAmount;

                //unity纹理坐标从下到上递增,因此row符号为-。
                half2 uv = i.uv + half2(column, -row);
                uv.x /= _HorizontalAmount;
                uv.y /= _HorizontalAmount;

                fixed4 c = tex2D(_MainTex, uv);
                c.rgb *= _Color;

                return c;

            }
            ENDCG
        }
    }
            Fallback "Transparent/VertexLit"
}

效果如下:

11.2.2滚动的背景

很多2D游戏都使用了不断滚动的背景来模拟游戏角色在场景中的穿梭,这些背景往往包含 了多个层(layers)来模拟一种视差效果。而这些背景的实现往往就是利用了纹理动画。在本节中, 我们将实现一个包含了两层的无限滚动的2D游戏背景。本节使用的纹理资源均来自OpenGameArt (http://opengameart.org)网站。

由于本例模拟的是2D游戏中的滚动背景,因此我们需要把摄像 机的投影模式设置为正交投影。
代码:



Shader "Unity Shaders Book/Chapter 11/Scrolling Background" 
{
	Properties 
	{
		//MainTex和DetailTex分别是第一层(较远)和第二层(较近)的背景纹理
		_MainTex ("Base Layer (RGB)", 2D) = "white" {}
		_DetailTex ("2nd Layer (RGB)", 2D) = "white" {}
		//ScrollX 和_Scroll2X对应了各自的水平滚动速度
		_ScrollX ("Base layer Scroll Speed", Float) = 1.0
		_Scroll2X ("2nd layer Scroll Speed", Float) = 1.0
		//Multiplier参数用于控制纹理的整体亮度
		_Multiplier ("Layer Multiplier", Float) = 1
	}
	SubShader 
	{
		Tags { "RenderType"="Opaque" "Queue"="Geometry"}
		
		Pass { 
			Tags { "LightMode"="ForwardBase" }
			
			CGPROGRAM
			
			#pragma vertex vert
			#pragma fragment frag
			
			#include "UnityCG.cginc"
			
			sampler2D _MainTex;
			sampler2D _DetailTex;
			float4 _MainTex_ST;
			float4 _DetailTex_ST;
			float _ScrollX;
			float _Scroll2X;
			float _Multiplier;
			
			struct a2v 
			{
				float4 vertex : POSITION;
				float4 texcoord : TEXCOORD0;
			};
			
			struct v2f 
			{
				float4 pos : SV_POSITION;
				float4 uv : TEXCOORD0;
			};
			
			v2f vert (a2v v) 
			{
				v2f o;
				o.pos = UnityObjectToClipPos(v.vertex);
				
				//初始纹理坐标加上偏移量
				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);
				
			![在这里插入图片描述](https://img-blog.csdnimg.cn/20210404165817703.gif)
	return o;
			}
			
			fixed4 frag (v2f i) : SV_Target 
			{
				fixed4 firstLayer = tex2D(_MainTex, i.uv.xy);
				fixed4 secondLayer = tex2D(_DetailTex, i.uv.zw);
				
				//使用第二层纹理的透明通道调用lerp函数插值混合纹理
				fixed4 c = lerp(firstLayer, secondLayer, secondLayer.a);
				c.rgb *= _Multiplier;
				
				return c;
			}
			
			ENDCG
		}
	}
	FallBack "VertexLit"
}

效果如下:

11.3 顶点动画

11.3.1 流动的河流

河流的模拟是顶点动画最常见的应用之一。它的原理通常就是使用正弦函数等来模拟水流的波动效果。
代码:

// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'

Shader "Unity Shaders Book/Chapter 11/Water" 
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _Color("Color Tint",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 {
				float4 vertex : POSITION;
				float4 texcoord : TEXCOORD0;
			};
			
			struct v2f {
				float4 pos : SV_POSITION;
				float2 uv : TEXCOORD0;
			};

            v2f vert (a2v v)
            {
                v2f o;
                
                float4 offset;
                //只对顶点x方向进行位移,
                //因此yzw的位移量置为0
                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"
}

效果如下:

11.3.2 广告牌

广告牌技术会根据视角方向来旋转。一个被纹理着色的多边形(通常就是简单的四边形,这个多边形就是广告牌),使得多边形看起来好像总是面对着摄像机。广告牌技术被用于很多应用,比如渲染烟雾、云朵、闪光效果等

广告牌技术的本质就是构建旋转矩阵,而我们知道一个变换矩阵需要3个基向量。广告牌技术使用的基向量通常就是表面法线(normal)、指向上的方向(up)以及指向右的方向(right)。除此之外,我们还需要指定一个锚点(anchorlocation),这个锚点在旋转过程中是固定不变的, 以此来确定多边形在空间中的位置。

广告牌技术的难点在于,如何根据需求来构建3个相互正交的基向量。计算过程通常是,我们首先会通过初始计算得到目标的表面法线(例如就是视角方向)和指向上的方向,而两者往往是不垂直的。
但是,两者其中之一是固定的,例如当模拟草丛时,我们希望广告牌的指向上的方向永远是(0,1,0),而法线方向应该随视角变化;而当模拟粒子效果时,我们希望广告牌的法线方向是固定的,即总是指向视角方向,指向上的方向则可以发生变化。

我们假设法线方向是固定的,首先,我们根据初始的表面法线和指向上的方向来计算出目标方向的指向右的方向(通过叉积操作):
right = up × normal
对其归一化后,再由法线方向和指向右的方向计算出正交的指向上的方向即可:
up’ = normal × right
至此,我们就可以得到用于旋转的3个正交基了。下图给出了上述计算过程的图示。如果指向上的方向是固定的,计算过程也是类似的。

代码:

// Upgrade NOTE: replaced '_World2Object' with 'unity_WorldToObject'
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'

Shader "Unlit/Chapter11-Billboard"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _Color("Color Tint",Color) = (1,1,1,1)
        //调整是固定法线还是固定指向上的方向
        _VerticalBillboarding("Vertical Restraints", Range(0, 1)) = 1
    }
        SubShader
    {
        Tags { "Queue" = "Transparent" "Ignoreprojector" = "True" "RenderType" = "Transparent"
        "Disable Batching" = "True"}


        Pass
        {
           ZWrite Off
			Blend SrcAlpha OneMinusSrcAlpha
			Cull Off
		
			CGPROGRAM
			
			#pragma vertex vert
			#pragma fragment frag
			
			#include "Lighting.cginc"

            sampler2D _MainTex;
            float4 _MainTex_ST;
            fixed4 _Color;
            //让广告牌的每个面都能显示
            float _VerticalBillboarding;

            struct a2v {
				float4 vertex : POSITION;
				float4 texcoord : TEXCOORD0;
			};
			
			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;
				//_VerticalBillboarding为1时,法线方向固定为视角方向
				//为0时,向上方向固定为(0,1,0)
				//因为法线y方向分量为0时,与之垂直的向上方向必为(0,1,0)
				normalDir.y =normalDir.y * _VerticalBillboarding;
				normalDir = normalize(normalDir);

				//得到粗略的向上方向(abs函数返回绝对值)
				//由法线方向和粗略的向上方向得到向右方向
				//,并归一化(因为是粗略的,所以向右方向结果不是归一化的)得到准确向右方向
				//,再由法线方向和向右方向得到准确的向上方向
				float3 upDir = abs(normalDir.y) > 0.999 ? float3(0, 0, 1) : float3(0, 1, 0);
				float3 rightDir = normalize(cross(upDir, normalDir));
				upDir = normalize(cross(normalDir, rightDir));
				
				//根据顶点原始位置相对于中心的偏移量以及3个正交基矢量
				//,得到新的顶点位置。
				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"
}

效果如下:
Vertical Restraints为1:

Vertical Restraints为0:

11.3.3 注意事项

在模型空间下进行顶点动画时,需要强制取消对该Unity Shader的批处理。但是取消批处理会带来一定的性能下降,增加了 Draw Call,因此我们应该尽量避免使用模型空间下的一些绝对位置和方向来进行计算。在广告牌的例子中,为了避免显式使用模型空间的中心来作为锚点,我们可以利用顶点颜色来存储每个顶点到锚点的距离值, 这种做法在商业游戏中很常见。

其次,如果我们想要对包含了顶点动画的物体添加阴影,那么如果仍然像9.4节中那样使用内置的Diffuse等包含的阴影Pass来渲染,就得不到正确的阴影效果。
这是因为,我们讲过Unity的阴影绘制需要调用一个ShadowCasterPass,而如果直接使用这些内置的ShadowCaster Pass,这个Pass中并没有进行相关的顶点动画,因此Unity会仍然按照原来的顶点位置来计算阴影,这并不是我们希望看到的。
这时,我们就需要提供一个 自定义的ShadowCaster Pass,在这个Pass中,我们将进行同样的顶点变换过程。需要注意的是, 在前面的实现中,如果涉及半透明物体我们都把Fallback设置成了Transparent/VertexLit,而 Transparent/VertexLit没有定义ShadowCaster Pass,因此也就不会产生阴影

在11.3.1节的场景中,开启了场景中平行光的阴影效果,并添加了一个平面来接收来自“水流”的阴影。还把这个Unity Shader的Fallback设置为了内置的VertexLit,这样Unity将根据Fallback最终找到VertexLit中ShadowCasterPass来渲染阴影,效果如下:

可以看出,此时虽然Water模型发生了形变,但它的阴影并没有产生相应的动画效果。为了 正确绘制变形对象的阴影,我们就需要提供自定义的ShadowCasterPass。
代码:

Pass 
        {
			Tags { "LightMode" = "ShadowCaster" }
			
			CGPROGRAM
			
			#pragma vertex vert
			#pragma fragment frag
			
			#pragma multi_compile_shadowcaster
			
			#include "UnityCG.cginc"
			
			float _Magnitude;
			float _Frequency;
			float _InvWaveLength;
			float _Speed;
			
			struct v2f 
            { 
			    V2F_SHADOW_CASTER;
			};
			
			v2f vert(appdata_base 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;
				v.vertex = v.vertex + offset;

				TRANSFER_SHADOW_CASTER_NORMALOFFSET(o)
				
				return o;
			}
			
			fixed4 frag(v2f i) : SV_Target 
            {
			    SHADOW_CASTER_FRAGMENT(i)
			}
			ENDCG
		}

效果如下:


转载:https://blog.csdn.net/weixin_46124783/article/details/115418003
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场