UV动画的原理(The “Conveyor Belt” Theory)
想象一下跑步机
- 模型(Mesh):是跑步机的框架,它是不动的。
- UV坐标:是传送带上的刻度
- 贴图(Texture):是印在传送带上的图案
1、静态的时候(Static)
通常情况下,UV是锁死的。比如你的模型上有一个点P,它的UV是(0.5,0.5)(正中间)。无论过了多久,显卡去(0.5,0.5)这个位置找贴图,永远找到的是同一个像素。
2、动起来的时候(Scrolling)
在代码中会写这句:uv += _Time.y * Speed;
这就相当于把传送带开动了。
- 第0秒:显卡去(0.5,0.5)找颜色
- 第1秒:显卡被我们骗了,它加上了偏移量,跑去(0.6,0.5)找颜色
- 第10秒:它跑去(1.5,0.5)找颜色
关键点:因为我们在贴图设置里开启了Repeat(重复模式)。当UV变成1.5时,显卡会把它当成0.5(因为它只看小数部分)
结果:看起来就像贴图在源源不断地向一个地方流动,像流水线一样。
这就是UV偏移(UV Offset)动画的本质:模型不动,我们偷换了采样的坐标。
以下是UV效果的基础代码(先使用了Plane模型)
Shader "Unlit/Simple"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
//控制流动的速度
_ScrollXSpeed ("X Scroll Speed", Range(-5, 5)) = 1.0//X轴流动速度
_ScrollYSpeed ("Y Scroll Speed", Range(-5, 5)) = 0.0//Y轴流动速度
}
SubShader
{
//渲染队列设置
//Opaque = 不透明物体(最先渲染)
//如果要做半透明流光,这里要改成Transparent
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
CGPROGRAM
//定义顶点和片元函数的名字
#pragma vertex vert
#pragma fragment frag
// 开启雾效支持(Unity自带)
#pragma multi_compile_fog
#include "UnityCG.cginc"
struct appdata //appdata:从模型网络读取的数据(输入)
{
float4 vertex : POSITION; //顶点位置
float2 uv : TEXCOORD0; //原始UV坐标
};
struct v2f //v2f:顶点着色器传给片元着色器的数据(输出)
{
float2 uv : TEXCOORD0; //处理后的UV
UNITY_FOG_COORDS(1) //雾效坐标
float4 vertex : SV_POSITION; //裁剪空间坐标
};
sampler2D _MainTex;
float4 _MainTex_ST; //ST = Scale & Translate(用来支持贴图的Tiling和Offset)
float _ScrollXSpeed;
float _ScrollYSpeed;
float _EmissionStrength;
v2f vert (appdata v) //顶点着色器;处理形状,这里只负责把UV传下去
{
v2f o;
//把3D模型坐标转成2D屏幕裁剪坐标(MVP变换)
o.vertex = UnityObjectToClipPos(v.vertex);
//应用材质面板里的Tiling和Offset设置。否则在面板改Tiling没反应
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
UNITY_TRANSFER_FOG(o,o.vertex);
return o;
}
fixed4 frag (v2f i) : SV_Target//片元着色器;让UV动起来
{
//拿到当前的UV
float2 uv = i.uv;
//计算偏移量:让UV随时间变化;_Time.y是游戏运行的时间(秒)
//偏移量 = 时间 * 速度
float xOffset = _Time.y * _ScrollXSpeed;
float yOffset = _Time.y * _ScrollYSpeed;
//修改UV:把偏移量加到UV上
uv += float2(xOffset, yOffset);
//用移动后的UV去采样贴图
fixed4 col = tex2D(_MainTex, uv);
//应用雾效
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
ENDCG
}
}
}
UV效果

基础效果后会有两个问题:
改造第一步:消除黑底(Additive Blending)
在特效制作中,我们通常使用“滤色/线性减淡(Additive)”模式来做火。
原理:黑色(0)+ 背景色 = 背景色(黑色等于透明)。亮色 + 背景色 = 更亮(发光效果)
所以我们现在需要在原代码基础上进行改造。修改SubShader里的配置
SubShader
{
//改标签:告诉Unity这是一个透明物体
Tags { "RenderType" = "Transparent" "Queue" = "Transparent" }
//关掉深度写入:火是虚的,不应该挡住后面的东西
ZWrite Off
//混合模式:叠加(Additive)
//SrcAlpha One(或者One One)通常用来做发光特效
//这里会让贴图里的黑色自动变透明,亮色变得更亮
Blend SrcAlpha One
LOD 100
Pass
{
//下面保持不变
解释一下为什么要改Tags中的“Queue”:因为不透明物体是先画的,透明物体(火)必须后画,否则会被后面的墙遮挡住或者透视关系出错。
改造第二步:让火焰亮起来(Emission)
现在的火焰只是图片的颜色,我们要加一个”倍增系数“,让它变亮
1、在Properties里加个变量
Properties
{
//上面保持不变
//新增:发光强度
_EmissionStrength("Glow Strength", Range(1, 10)) = 1.0
}
2、在Pass的变量声明添加一行
float _EmissionStrength;
3、在frag函数的最后修改颜色
fixed4 frag (v2f i) : SV_Target//片元着色器;让UV动起来
{
//上面算UV的代码不动
fixed col = tex2D(_MainTex, uv);
//【新增】:颜色乘以强度 = 发光!
//把RGB变亮,Alpha保持不变(如果用的是One One混合,Alpha其实不重要)
col.rgb *= _EmissionStrength;
//应用雾效
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
保存代码回到Unity,代码修改后效果

由于我使用的图是一张非无缝的火焰照片,可以看到火苗在图片的顶部被”切断“了(因为照片拍到哪里就结束了)
火焰贴图的Inspector面板中有个Wrap Mode(循环模式),把Wrap Mode改成Mirror(镜像)。记得点Apply应用
但其实Repeat(重复)是流动纹理的标配,一般必须选这个,因为我的图片不是“无缝贴图”,所以我使用了Mirror(镜像)。但是使用Mirror有个后果就是当UV跑到1.0以上时,火焰会突然倒着烧(头朝下)。所以正确的解决方案应该是要找一张上下边缘能完美拼接的无缝贴图,然后选择Repeat(重复)。

广告牌(Billboard)技术
现在的火焰是趴在地上的”地毯“。真正的火焰有个永远立着,而且永远正脸对着摄像机
我们要修改Vertex Shader(顶点着色器),强行扭转方向
前置准备:确保用的是Quad,而不是Plane(因为Plane是躺着的,Quad是站着的,上面的Plane只是一个基础练习,下面的代码是给Quad设计的)
需要把vert函数里的 o.vertex = UnityObjectToClipPos(v.vertex);这一行删改成下面的新代码
v2f vert (appdata v)
{
v2f o;
//核心:让它永远面向摄像机
//1.把“模型中心点”转换到“观察空间”(View Space)
//这一步相当于把模型瞬移到摄像机面前,同时消除了模型的旋转
float3 centerPos = UnityObjectToViewPos(float3(0, 0, 0));
//2.加上顶点的原始偏移
//v.vertex.x和v.vertex.y是平面原本的宽高
我们直接加在View Space坐标上,这就让平面永远平行于镜头
float3 viewPos = centerPos + float3(v.vertex.x, v.vertex.y, 0);
//3.最后投影到屏幕(Clip Space)
//注意:这里用的是mull(UNITY_MATRIX_P, …)
//因为 viewPos 已经在 View Space了,只需要乘投影矩阵 P
o.vertex = mul(UNITY_MATRIX_P, float4(viewPos, 1.0));
//下面保持不变
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
UNITY_TRANSFER_FOG(o, o.vertex);
return o;
}
下面是代码修改完成后的效果图


你会发现不论从哪个方向看火焰都是面向你的
如果代码修改完,你发现火焰是“横着走”而不是“竖着烧”,只需要调整一下参数就好
可以参考一下我的参数




