之前的Unity项目中,UI部分需要的流光比较多,特别是一些使用图集的UISprite,为节省图片资源,必然是使用shader来实现。
搜了很多前人的分享,发现一些有意思的解决思路,但并没有很完善的分享,而且我们的需求还包括自定义流光图、流光间隔、流光时间等。所以自己做了一套,这里正好整理分享一下。
本方案最终实现的流光效果如下:
此流光效果,有以下优点:
- 支持UITexture
- 支持使用图集的UISprite,且每个UISprite可以独立效果
- 可以自定义流光的图、流光速度、流光时间间隔、流光强度等
不过,也有以下缺点或者说是待完善的地方:
- 应用于UISprite时,图片的清晰度会受到影响,变得模糊一点
- 原理是通过独立的材质球来做,所以会增加NGUI的drawcall数量
Shader我们是在默认的Transparent Colored的代码基础上进行修改,新增的变量定义部分:
Properties
{
...
//流光纹理
_FlowLightTex("FlowLight Texture",2D) = "white"{}
//流光强度
_FlowLightPower("FlowLightPower",float) = 1
//流光开关,0关闭,1开启
_IsOpenFlowLight ("IsOpenFlowLight", float) = 0
//流光的偏移,通过设置此值来达到uv动画效果
_FlowLightOffset("FlowLight Offset", float) = 0
}
主要逻辑是在像素处理函数中来做,原理就是通过变化uv取流光纹理的颜色,叠加到主纹理上即可,直接上代码:
fixed4 frag(v2f IN)
{
fixed4 colorMain = tex2D(_MainTex, IN.texcoord);
//如果开启流光
if(_IsOpenFlowLight > 0.5) {
float2 uvFlowLight = IN.texcoord;
//uv减半处理
uvFlowLight.x /= 2;
//根据速度变化
uvFlowLight.x -= _FlowLightOffset;
//用计算后的uv取流光那张图
fixed4 colorFlowLight = tex2D(_FlowLightTex, uvFlowLight) * _FlowLightPower;
//颜色叠加计算
colorFlowLight.rgb *= colorMain.rgb;
colorMain.rgb += colorFlowLight.rgb;
colorMain.rgb *= colorMain.a;
}
return colorMain;
}
对于颜色叠加计算这块,以上面的2张图为例,多解释一下:
colorFlowLight.rgb *= colorMain.rgb;
colorMain.rgb += colorFlowLight.rgb;
这句话把上面这张图再叠加到原图上,就有了流光的高亮效果,如图:
colorMain.rgb *= colorMain.a;
最后这句是保持和原图同样的透明度而已。
好了,现在我们的shader已经准备好了,就差使用到UITexture上了。
首先我们创建一个对应上面shader的材质球,接下来写一个C#脚本,挂在UITexture上,这样我们就可以自由调整参数,来实现我们想要的效果了。
using UnityEngine;
using System.Collections;
public class EffectFlowLightForTex : MonoBehaviour{
//起始的uv坐标
public float mUvStart = 0f;
//uv移动的速度
public float mUvSpeed = 0.02f;
//一次动画uv移动的最大长度
public float mUvXMax = 0.9f;
//流光的间隔时间
public float mTimeInteval = 3f;
public Material mCurMaterial = null;
private float mUvAdd;
private bool mIsPlaying;
void Awake () {
UITexture tex = gameObject.GetComponent ();
//onRender是什么鬼?这里稍后解释
tex.onRender += UpdateMaterial;
mUvAdd = 0;
mIsPlaying = true;
tex.material = mCurMaterial;
}
//NGUI更新Material的回调
private void UpdateMaterial(Material mat) {
if (mIsPlaying) {
//逐帧移动uv
mUvAdd += mUvSpeed;
mat.SetFloat ("_FlowLightOffset", mUvStart + mUvAdd);
mat.SetFloat ("_IsOpenFlowLight", 1f);
//如果移动的uv已经超过最大值,重置,准备下一次流光
if (mUvAdd >= mUvXMax) {
mIsPlaying = false;
mat.SetFloat ("_IsOpenFlowLight", 0f);
Invoke ("PlayOnceAgain", mTimeInteval);
}
} else {
mat.SetFloat ("_IsOpenFlowLight", 0f);
}
}
//再次触发流光
private void PlayOnceAgain() {
mUvAdd = 0;
mIsPlaying = true;
}
}
大家可能会奇怪,上面的代码中的onRender什么鬼,我直接在脚本内部,起个定时器什么的来修改shader的参数不就行了吗。有兴趣的同学可以尝试一下,不管用的。
这个问题一开始我也遇到了,google一下,原来这和NGUI的渲染机制有关。大概解释一下:
NGUI在渲染的时候,大家都知道,会合并DrawCall,合并的必然是使用同一材质球的元素,NGUI内部会新建一个Material,然后UIDrawCall会进行一次渲染,渲染的时候就会调用onRender这个回调,并且把这个新建的Material传过来,方便我们做一些自定义的操作
看看UIWidget中的onRender定义大家也就都明白了:
///
/// Set the callback that will be triggered when the widget is being rendered (OnWillRenderObject).
/// This is where you would set material properties and shader values.
///
public UIDrawCall.OnRenderCallback onRender
{
...
}
好了,基于UITexture的流光解决方案就是以上这些了,那么接下来使用图集的UISprite怎么办呢?鉴于篇幅已经很长了,我们下一篇再说。