嗨,大家好!我是你们的技术美术(或者爱捣鼓Shader的程序员)朋友。今天想和大家分享一次特别有趣的“造轮子”经历——为Unity的URP管线编写一个用于UI的UV动画Shader。这不仅仅是一段代码,更像是一次为静态界面注入灵魂的冒险。
缘起:当UI不再满足于“静态美”
不知道你有没有见过那些让人眼前一亮的游戏UI?比如:
登录界面背景中,流光缓缓掠过的云层。
技能图标上,能量如呼吸般明灭涌动。
经验值进度条里,光芒向着终点缓缓流淌。
这些效果,如果只用序列帧或者Animator
去控制图片,不仅费时费力,更会徒增Draw Call。作为一名“懒惰”的开发者,我决定寻求更优雅的解决方案:用Shader直接让纹理动起来! 这样,一个材质球就能搞定所有动画,性能高效,艺术同学调节起来也无比方便。
于是,UIUVAnimation
Shader的创作之旅开始了。
第一步:站稳脚跟——继承UI的“优良传统”
在Shader的世界里,想搞创新,第一步往往是“抄作业”。不对,是“站在巨人的肩膀上”。我首先深入研究了URP和UGUI的标准Shader。它们已经完美处理了UI渲染的方方面面:
透明度混合:让UI可以层层叠叠而不互相遮挡。
模板测试:这是
Mask
遮罩功能的核心,必须保留!颜色叠加:那个
_Color
属性,让我们能轻松地给UI换色。
我的目标是“增强”,而非“重造”。所以,我毫不犹豫地把这些关键的Tags
、Stencil
、Blend
状态原封不动地“搬”了过来。这确保了我们的动画Shader首先是一个合格的UI Shader,能和整个UI系统和谐共处。
第二步:注入灵魂——操控纹理坐标的魔法
现在,轮到重头戏了。如何让一张静态图片动起来?答案就在于操控它的纹理坐标,也就是我们常说的UV。
你可以把UV想象成一张世界地图的经纬线,而Shader就是根据这些经纬线去地图(纹理)上查找颜色的。原本,每个UI顶点的UV是固定的,所以采样到的颜色也是固定的。
那么,如果我让这些UV坐标随时间变化呢?比如,让代表“经度”的U坐标不停地增加,那么采样点就会不停地向右移动,纹理看起来就像在向左流动了!这就是最基础的UV动画。
基于这个简单的想法,我定义了两个最核心的参数:
_UVSpeedX
:控制U方向(水平)的移动速度。_UVSpeedY
:控制V方向(垂直)的移动速度。
在顶点着色器中,我写下了最激动人心的一行代码:
float time = _TimeParameters.x; // 获取游戏的运行时间
animatedUV.x += _UVSpeedX * time; // 让U坐标随时间流逝
animatedUV.y += _UVSpeedY * time; // 让V坐标随时间流逝
就这? 对,就这!一个最简单的滚动动画就完成了。当我在材质球上把_UVSpeedX
设为0.5,立刻就看到纹理开始缓缓流动,那种成就感,简直像魔术师完成了第一个魔术!
第三步:精益求精——让魔法更强大
只有平移未免太单调了。为了让Shader更强大、更实用,我决定加入更多控制维度。
缩放:加入了
_UVScaleX
和_UVScaleY
。比如,把缩放设为2,纹理就会被放大,可以模拟能量聚集或镜头拉近的效果。注意: 缩放的原点在UV的(0,0),也就是左下角。偏移:加入了
_UVOffsetX
和_UVOffsetY
。这就像在动画开始前,先把纹理的某个特定区域“摆放”到我们的视野中。比如一张很大的雪景图,我可以先偏移到有雪花的部分,再让雪花动起来。
在代码中,我精心安排了它们的执行顺序:先缩放,再偏移,最后加上时间动画。这个顺序非常重要,不同的顺序会产生完全不同的视觉效果。
第四步:融入体系——URP的优雅之道
代码写好了,但要让它成为一个“专业”的URP Shader,还得遵循规范。我做了几件事:
使用HLSL和
Core.hlsl
库:告别旧的CG和UnityCG.cginc
,拥抱URP的新语法。使用
TEXTURE2D
和SAMPLER
宏:这是URP中采样纹理的正确姿势。使用
CBUFFER
:将属性打包,利于SRP Batcher合批,提升性能。简化片元着色器:我移除了手动的
_ClipRect
裁剪判断。因为在URP的UI系统中,RectMask2D
组件通过Stencil
(模板测试)来实现裁剪是更高效、更标准的方式。我们应该相信并利用好引擎提供的这套机制,不画蛇添足。
最终成果与感想
就这样,经过构思、编码、测试和打磨,UIUVAnimation
Shader诞生了!
它的美妙之处在于:
对艺术家友好:美术或UI同学只需要在材质面板上拖拖滑块,就能实时看到各种动画效果,无需任何代码。
性能优异:动画计算在顶点阶段完成,开销极小。
功能专注而灵活:虽然原理简单,但通过速度、缩放、偏移的组合,可以创造出滚动、扫描、呼吸、漩涡等丰富效果。
现在,你可以把它赋予一张背景图,让它成为流动的星河;也可以赋予一个按钮边框,让它充满科技的动感。这就是Shader的魅力——用简洁的数学,赋予数字世界以动态的生命。
希望这篇“心路历程”能让你对Shader开发有更感性的认识。如果你也心动了,不妨拿代码去试试,创造出属于你的动态UI吧!
附:shader代码
Shader "URP/UI/UIUVAnimation"
{
Properties
{
[PerRendererData] _MainTex ("Texture", 2D) = "white" {}
_Color ("Tint", Color) = (1,1,1,1)
// UV动画控制参数
_UVSpeedX ("UV Speed X", Float) = 0.0
_UVSpeedY ("UV Speed Y", Float) = 0.0
// 可选:UV缩放控制
_UVScaleX ("UV Scale X", Float) = 1.0
_UVScaleY ("UV Scale Y", Float) = 1.0
// 可选:UV偏移控制
_UVOffsetX ("UV Offset X", Float) = 0.0
_UVOffsetY ("UV Offset Y", Float) = 0.0
// UI必要属性
_StencilComp ("Stencil Comparison", Float) = 8
_Stencil ("Stencil ID", Float) = 0
_StencilOp ("Stencil Operation", Float) = 0
_StencilWriteMask ("Stencil Write Mask", Float) = 255
_StencilReadMask ("Stencil Read Mask", Float) = 255
_ColorMask ("Color Mask", Float) = 15
}
SubShader
{
Tags
{
"Queue"="Transparent"
"IgnoreProjector"="True"
"RenderType"="Transparent"
"PreviewType"="Plane"
"CanUseSpriteAtlas"="True"
"RenderPipeline"="UniversalPipeline"
}
Stencil
{
Ref [_Stencil]
Comp [_StencilComp]
Pass [_StencilOp]
ReadMask [_StencilReadMask]
WriteMask [_StencilWriteMask]
}
Cull Off
Lighting Off
ZWrite Off
ZTest [unity_GUIZTestMode]
Blend SrcAlpha OneMinusSrcAlpha
ColorMask [_ColorMask]
Pass
{
Name "ForwardLit"
Tags { "LightMode"="UniversalForward" }
HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma target 3.0
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/DeclareOpaqueTexture.hlsl"
struct Attributes
{
float4 positionOS : POSITION;
float2 uv : TEXCOORD0;
float4 color : COLOR;
};
struct Varyings
{
float4 positionHCS : SV_POSITION;
float2 uv : TEXCOORD0;
float4 color : COLOR;
float4 worldPosition : TEXCOORD1;
};
TEXTURE2D(_MainTex);
SAMPLER(sampler_MainTex);
CBUFFER_START(UnityPerMaterial)
float4 _MainTex_ST;
float4 _Color;
float4 _ClipRect;
float _UVSpeedX;
float _UVSpeedY;
float _UVScaleX;
float _UVScaleY;
float _UVOffsetX;
float _UVOffsetY;
CBUFFER_END
Varyings vert(Attributes input)
{
Varyings output;
VertexPositionInputs vertexInput = GetVertexPositionInputs(input.positionOS.xyz);
output.positionHCS = vertexInput.positionCS;
output.worldPosition = input.positionOS;
// 计算动画UV
float2 animatedUV = input.uv;
// 应用UV缩放
animatedUV.x *= _UVScaleX;
animatedUV.y *= _UVScaleY;
// 应用UV偏移
animatedUV.x += _UVOffsetX;
animatedUV.y += _UVOffsetY;
// 应用时间动画 - 使用正确的时间变量
float time = _TimeParameters.x; // 使用URP的时间变量
animatedUV.x += _UVSpeedX * time;
animatedUV.y += _UVSpeedY * time;
output.uv = TRANSFORM_TEX(animatedUV, _MainTex);
output.color = input.color * _Color;
return output;
}
half4 frag(Varyings input) : SV_Target
{
half4 col = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, input.uv);
col *= input.color;
// 简化的裁剪支持 - 移除UnityGet2DClipping调用
// 如果需要UI裁剪,可以使用RectTransform的Mask组件
return col;
}
ENDHLSL
}
}
Fallback "Hidden/Universal Render Pipeline/FallbackError"
}
评论区