作者:王子饼干 ——镇江虹视游戏科技有限公司联合创始人、《多洛可小镇》制作人
原文链接:UnityShader入门精要16:着色器动画1
原文发布时间:2020年3月25日
文章类型: 授权转载
不相信自己的人,连努力的价值都没有。——迈特·凯《火影忍者》
笔记当前使用的Unity版本:“2019.3.3”
笔记当前Unity最新发行版:“2019.3.6”
其实高级纹理这个章节还有最后一个部分,程序纹理,但是它并不是非常重点,而且原书给出的代码并不是非常好,所以这里略过了。
我们要开始新的征程了,这次的主题是着色器动画。动画效果是很多人都喜闻乐见的,在着色器中也是一样的,带有动画的着色器将会有更棒的效果。
只要我们扯到动画,必然要扯到时间,因为“動”这个概念必须是依赖时间的,如果我们不定义时间,就没有动画的概念。所以可以先了解一下Unity3D提供的关于时间的内置变量。
以上四个float4类型的变量就是Unity提供的和时间有关的变量。
纹理动画是最常见的动画了,对于纹理动画来说,我们要做的无非就控制纹理随着时间的变化而变化。常见的纹理动画分为两类。逐帧动画,滚动动画。
对于逐帧动画,首先需要一张包含了所有动画帧的主纹理,它有两点要求,它的每行和每列都应该是可以等分的,逐帧动画唯一的难点就是我们要构建一个函数,这个函数可以通过当前的时间来计算目前应该播放哪一帧图片。当然啦,我们的逐帧动画是把所有的序列帧都保存在一张图片上,它是从左到右,从上到下依次排列的,如果不是这样排列的,那么我们的函数就需要重新定义。看下面这张图,你就懂啦~
不管怎么说,我们首先需要计算出在T时刻,行索引和列索引,并且我们认为,这张图没有无用的帧,也就是如果宽是4,高也是4,那么这张图正好一共16帧,一帧不多,一帧不少。这样我们不用刻意的略过特殊的帧。(因为这样做难度不是一般的大,即使做出来,也会非常影响效率)
首先是纵轴的索引值,我们可以通过下面的公式来计算。
floor函数是向下取整,T是目前的时刻,width是目标图片一共有几列。由于我们使用了floor函数,所以T是有余数的,这个余数就是在表示某一行的第xi张图片,可以通过下面的公式计算出来。
这样,我们就有了索引x和索引y,但是这样就可以直接用了吗?还早,我们先来注意这样一个问题。就是xi和yi的方向性。
是的,不要忘记我们的动画帧是从上到下,从左到右。然而uv坐标却不是这样的,uv坐标是从下到上,从左到右。
对于这种事情,暂时想不到什么好的解决方法。我们只能思考这样一件事,因为我们已经成功的计算出了行索引与列索引,当索引为(xi,yi)的时候,它的采样坐标应该是多少。在搞清楚这个问题之前,还有一件事要弄清楚,即采样坐标不是一个单纯的坐标,而是一个范围。
比如uv坐标,如果不做任何变动,它其实是一个正方形,x从0到1,y也从0到1。我们只能对这个uv坐标进行缩放或者偏移,让它采样中间的一小部分。
了解了这个事实之后,我们假设性的构建一个帧数为9,横纵分别为三列和三行的逐帧图纹理。如下图所示。
如果我们只关注x的采样范围的话,可以先标出x坐标的位置
可以看见,列索引为0的时候,u范围是[0,1/3],列索引为1的时候,u范围是[1/3,2/3],列索引为2的时候,u范围是[2/3,1]。
由于uv坐标最一开始是[0,1],所以最后一步应该是缩小,这样比较好控制。(当我们先缩放,后偏移时,uv坐标必然要减去一个值获得正确的输入,这样操作非常麻烦,而我们又不能单独的产生一个范围性的值,所以必然是先偏移,后缩放。)
根据这里我们先将这些坐标还原它们的缩放,我们乘以3,得到u坐标范围(依据列索引xi)
:列索引为0时,坐标为[0,1],列索引为1时,坐标为[1,2],列索引为2时,坐标为[2,3]
显然,我们可以直接给出u坐标的计算公式
我们始终要牢记u是一个范围,[0,1]的范围。接着我们来考虑纵轴的坐标。我们还是先列出纵轴坐标的范围。
当行索引为0的时候,v坐标范围是[2/3,1],行索引为1的时候,v坐标范围是[1/3,2/3],行索引为2的时候,v坐标范围是[0,1/3],同样,我们先为它乘上一个3(说是3,其实是一共有多少行),还原一下缩放。
重新来一次~
当行索引为0的时候,还原的v坐标范围是[2,3],行索引为1的时候,还原的v坐标范围是[1,2],行索引为2的时候,还原的v坐标范围是[0,1]。显然,我们得到一个行索引与坐标起点值之间的规律如下:
(此处的vstart是我擅自定义的,表示v坐标的起点值),由此,我们可以推算出vstart为
由于v坐标本身也是一个范围,所以我们知道了v坐标的起点值(所谓起点值,其实就是一个偏移),就可以获得它的范围值了。这样,连带之前的缩放一并处理。
这样,我们就可以开始编写Shader了。它的完整代码如下
Shader "UShaderMagicBook/frameAnimation1"{
Properties{
_MainTex("Animation Texture",2D) = "white"{}
_BaseColor("BaseColor",Color) = (1.0,1.0,1.0,1.0)
_XCount("XCount",Int) = 1
_YCount("YCount",Int) = 1
_Speed("Speed",Range(1,100)) = 30
}
SubShader{
Tags{"Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent"}
pass{
Tags{"LightMode"="Always"}
ZWrite Off
Blend SrcAlpha OneMinusSrcAlpha
CGPROGRAM
#include "Lighting.cginc"
#include "UnityCG.cginc"
#include "AutoLight.cginc"
#pragma vertex Vertex
#pragma fragment Pixel
struct vertexOutput{
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
};
sampler2D _MainTex;
fixed4 _BaseColor;
int _XCount;
int _YCount;
float _Speed;
vertexOutput Vertex(appdata_base v){
vertexOutput o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = v.texcoord;
return o;
}
fixed4 Pixel(vertexOutput i):SV_TARGET{
float time = floor(_Time.y * _Speed);
//将时间乘上我们指定的缩放,这样就可以控制播放速度了
float ypos = floor(time / _XCount);
float xpos = time - ypos * _XCount;
//计算行索引与列索引
i.uv.x = (i.uv.x + xpos) / _XCount;
i.uv.y = 1 - (ypos + 1 - i.uv.y)/_YCount;
//根据我们刚才计算出来的公式进行uv坐标的偏移和缩放
fixed4 c = tex2D(_MainTex,i.uv);
return c * _BaseColor;
}
ENDCG
}
}
}
我们主要关注一下片元着色器,首先我们为时间乘上了一个参数_Speed,这样就可以控制播放速度了,接着我们使用之前的公式来计算行索引与列索引。然后通过行索引与列索引来计算uv坐标,最后使用uv坐标来采样。下面是最终的效果
ok,只不过背景最好是透明的,这样,我们可以用透明度测试将透明的部分剔除掉。
细心的朋友可能发现了,我们使用的计算v坐标的方式和原书的并不一样,我们来解读一下原书的公式。书上是这样计算v坐标的
这种方式看起来确实非常简单,它是如何做到的呢?这里的秘诀其实就是纹理类型的Repeat,在初级纹理的时候我们提到过,当纹理设为Repeat的时候,当采样坐标不在[0,1]的范围内时,该如何采样。事实上,当我们的纹理设为Repeat时,它大概是下面这种感觉的。
是的,你可以无限的用主纹理来平铺超过[0,1]的空间,这样一来,当我们直接用
来计算采样坐标的时候,它采样的实际是下面的那块坐标,所以也是可以的,没有任何问题。由于和原公式在性质上差别不大,都是一种计算v坐标的方式,这里就不重新写一个Shader了。另外,为什么不直接使用原书的公式,而要费力的去自己思考出一个比原书效率还差的方法呢?
我希望大家永远不要忘记,我们现在所学习的东西还是非常非常基础的内容。说起来是在学习Shader,很大一部分程度上,我们只是在了解规则,比规则更加重要的是在规则的基础上,提升我们解决问题的能力。
其实没有纹理偏移动画这种说法,只是我的描述罢了,(声明过了哦
对纹理进行uv坐标的偏移,使其产生类似于滚动或者跑马灯一样的动画效果,这就是这节所要讲的。它的原理就是基于Repeat,不断偏移uv的采样坐标,但是还是由于Repeat的特性,它还是会回滚。
ok,我们先准备一张适合滚动的图片,比如这张(炭治郎~)
原理真的很简单,我们只要把uv坐标加上被缩放的时间,就可以了。完整的着色器代码如下
Shader "UShaderMagicBook/offsetAnimation"{
Properties{
_MainTex("Main Texture",2D) = "white"{}
_XSpeed("X Axis Speed",Float) = 1.0
_YSpeed("Y Axis Speed",Float) = 1.0
}
SubShader{
pass{
Tags{"LightMode"="Always"}
CGPROGRAM
#pragma vertex Vertex
#pragma fragment Pixel
#include "UnityCG.cginc"
struct vertexOutput{
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
};
sampler2D _MainTex;
float _XSpeed;
float _YSpeed;
vertexOutput Vertex(appdata_base v){
vertexOutput o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = v.texcoord;
return o;
}
fixed4 Pixel(vertexOutput i):SV_TARGET{
float2 offset = float2(_Time.y * _XSpeed,_Time.y * _YSpeed);
//根据水平和纵向的偏移速度计算出总得偏移
i.uv += offset;
//设置偏移
return tex2D(_MainTex,i.uv);
//采样并返回
}
ENDCG
}
}
}
代码内容非常简单,这里就不再解读了。最终的效果如下
之前的动画无非是改变uv坐标,而顶点动画无非是把时间的变化附加到顶点上去。我们做一种最简单的顶点变化,让顶点的高度随着时间的变化而变化。
没有特殊的变化,我们只是把顶点的高度增加了一个sin函数对时间的计算,先看一下效果。
这并不是我们想要的结果,因为顶点的高度在整体的变化,然而每个顶点的变化应该是不同的,每个顶点其实有一个天生不同的点,那就是它们的坐标。我们把顶点的x坐标也放入sin函数的计算中,观察结果
好像还不错,但是它只有横轴x的变化,纵轴z应该也有变化,同时呢,我们应该开放一个可以调整横轴和纵轴的缩放的参数,所以最终的变化公式如下
写成代码就是下面这样
最终效果大概是这个感觉,稍微调整一下x和z的速度。
它看起来有点像是水面,不过我们要实现较为真实的水面,顶点动画还只是冰山一角罢了。当然啦,千里之行始于足下。完整的着色器代码如下
Shader "UShaderMagicBook/vertexAnimation"{
Properties{
_MainTex("Main Texture",2D) = "white"{}
_XSpeed("X Axis Speed",Float) = 1.0
_YSpeed("Y Axis Speed",Float) = 1.0
}
SubShader{
pass{
Tags{"LightMode"="Always"}
CGPROGRAM
#pragma vertex Vertex
#pragma fragment Pixel
#include "UnityCG.cginc"
struct vertexOutput{
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
};
sampler2D _MainTex;
float _XSpeed;
float _YSpeed;
vertexOutput Vertex(appdata_base v){
vertexOutput o;
v.vertex.y += sin(_Time.y + v.vertex.x * _XSpeed + v.vertex.z * _YSpeed);
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = v.texcoord;
return o;
}
fixed4 Pixel(vertexOutput i):SV_TARGET{
return tex2D(_MainTex,i.uv);
}
ENDCG
}
}
}
ok~,本节就到此为止了,这节的内容不是很多,难度也很小。没有太多要注意的点,但是都是形成高级的着色器必不可少的部分,希望能对大家有一些帮助。
另外,本次使用到的所有代码都已经上传到了我的github,在下面的链接中可以找到。如果你喜欢这个系列的文章,欢迎给我的github贡献一颗⭐,感谢阅读。