游戏中的界面往往会添加一些动效来丰富玩家体验,前段时间我使用后期做了一个界面过渡的小功能,原理并不复杂,不过在刚接收需求的时候思路上也走了一些歪路,先看最终效果:
这个效果其实挺常见的,就是一个遮罩动画,相信大家分分钟就能把shader写出来,然而相我写完之后准备用到项目的时候,我发现了一个问题,我们的项目是使用NGUI做的(UGUI也有这个问题),游戏中的界面是由很多图片拼起来的,并不是一张图,而这个效果只能用于一张图片,此时我第一个想到的就是NGUI中UIPanel的SoftClip功能,众所周知,SoftClip可以在UIPanel中显示一个指定矩形范围的内容,那是否可以把这个矩形换成上面的效果呢?于是我把NGUI的SoftClip的相关的源码看了一遍,结果人家是用坐标来进行判断是否在区域内,如果在就显示,不在就把透明度制成零,难怪只支持矩形,因为好算,可是需求的效果却很复杂,因为过渡的边缘并非是线性的,当然也能用sin函数去模拟,不过这样做调节成本太大,而且生硬,所以我把这个方案PASS了。
于是我换了一个思路去思考这个问题。
1.不规则的边缘最好使用控制图去实现,因为让美术做图比写算法容易多了,而且美术可以通过修改控制图对最终效果进行微调
2.效果只能针对一张图,可是UI界面却是由多个UISprite组成的,那就把有界面时的画面和无界面的画面取出来做过渡效果,这样一来问题就被简化成在两张图中做过渡,而过渡依据就是1中所说的控制图。
下面是美术按我的需求提供的控制图
其实就是一个256X256的小图,黑色部分用于显示有界面图的颜色,白色部分用于显示没有界面图的颜色,而控制图自身只要动他的UV坐标,整个图就会从全白变到全黑,中间的过渡区由美术控制,灰度用于控制颜色权重比,所以就会有一些透明效果,shader比较简单如下:
Shader "Custom/MaskAnim" { Properties { _MainTex ("MainTex", 2D) = "white" {} //当前画面(没有播放该动画效果的画面) _FullTex ("FullTex", 2D) = "white" {} //完整画面 _MaskTex ("MaskTex", 2D) = "white" {} } SubShader { CGINCLUDE #include "UnityCG.cginc" sampler2D _MaskTex; float4 _MaskTex_ST; sampler2D _MainTex; sampler2D _FullTex; struct a2v { float4 vertex : POSITION; float2 uv : TEXCOORD0; }; struct v2f { float2 uv : TEXCOORD0; float2 uv2 : TEXCOORD1; float4 vertex : SV_POSITION; }; //----------------------------第一次UV动画----------------------------------- v2f vert1(a2v v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); o.uv = TRANSFORM_TEX(v.uv, _MaskTex); o.uv2 = v.uv; return o; } fixed4 frag1(v2f i) : SV_Target { fixed4 col = tex2D(_MaskTex, i.uv); return col; } //---------------------------第二次颜色融合----------------------------------- v2f vert2(a2v v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); o.uv = TRANSFORM_TEX(v.uv, _MaskTex); o.uv2 = v.uv; return o; } fixed4 frag2(v2f i) : SV_Target { fixed4 mask_col = tex2D(_MaskTex, i.uv); fixed4 main_col = tex2D(_MainTex, i.uv2); fixed4 full_col = tex2D(_FullTex, i.uv2); fixed weight = dot(mask_col.rgb, fixed3(1, 1, 1)) * 0.33333; //fixed4 col = lerp(clip_col,main_col,weight); fixed4 col = lerp(full_col, main_col, weight); return col; } ENDCG Lighting Off ZTest Always ZWrite Off LOD 100 Pass { CGPROGRAM #pragma vertex vert1 #pragma fragment frag1 ENDCG } Pass { CGPROGRAM #pragma vertex vert2 #pragma fragment frag2 ENDCG } } }
C#的代码比较简单,就是offset从(0.5,-0.5)到(-0.5,0.5)的变化
不过这个有个小问题,就是如果从(0.5,-0.5)到(-0.5,0.5),那是整个画面的过渡,根据策划的需求这是一个通过的界面过渡效果,而界面是有大有小的,假设我们配置的动画时长都一样,那么越小的界面变化的会越快,所以正确的处理方式应该计算出界面的左上角的UV坐标与左下角的UV坐标,然后再换算到(0.5,-0.5)到(-0.5,0.5)之间的变化值,为方便UI策划使用,我把整个功能开发成一个叫组件。以下是组件效果
黄色的线是界面的包围盒子,在添加组件的时候会自动生成,也可以通过修改组件上的“包围盒”属性进行调整,最后我们只要把组件提供的播放动画接口填到项目的UI框架中即可,这个UI策划只要把该组件挂到指定界面上,该界面在打开/关闭的时候就会播放相应的动画
最后给出C#核心代码给大家一个参考
组件
using System; using UnityEngine; using System.Collections.Generic; using UnityEngine.Rendering; using UnityEngine.Rendering.Universal; using GameCore; namespace PF.URP.PostProcessing { [ExecuteInEditMode] [DisallowMultipleComponent] [AddComponentMenu("PFUIAnim/UIMaskCommonAnim")] public class UIMaskCommonAnim : MonoBehaviour { public class MaskMaterial { private int mResId; private Material mMat; private string mAssetPath; public MaskMaterial(int resId,string assetPath) { mResId = resId; mAssetPath = assetPath; } public Material material { get { if (mMat == null) { #if UNITY_EDITOR mMat = (Material)UnityEditor.AssetDatabase.LoadAssetAtPath(mAssetPath, typeof(Material)); #else mMat = (Material)ResMgr.Instance.LoadObject(mResId); #endif } return mMat; } } } [System.Serializable] public class Border { public Vector2 Center; public Vector2 Size; //左上角顶点 public Vector2 LT; //左下角顶点 public Vector2 RB; //UI矩阵 public Matrix4x4 Matrix; public bool Init(UIMaskCommonAnim ui) { if (UIRoot.list.Count > 0) { UIRoot root = UIRoot.list[0]; if (root != null) { Matrix = ui.transform.localToWorldMatrix; UIWidget[] widgets = ui.GetComponentsInChildren<UIWidget>(); foreach (UIWidget w in widgets) { Vector3 localPos = root.transform.worldToLocalMatrix.MultiplyPoint(w.transform.position); //左上角 float LT_X = localPos.x + (0 - w.pivotOffset.x) * w.width; float LT_Y = localPos.y + (1 - w.pivotOffset.y) * w.height; //右下角 float RB_X = localPos.x + (1 - w.pivotOffset.x) * w.width; float RB_Y = localPos.y + (0 - w.pivotOffset.y) * w.height; //更新包围盒 LT.x = LT_X < LT.x ? LT_X : LT.x; LT.y = LT_Y > LT.y ? LT_Y : LT.y; RB.x = RB_X > RB.x ? RB_X : RB.x; RB.y = RB_Y < RB.y ? RB_Y : RB.y; } Size.x = RB.x - LT.x; Size.y = LT.y - RB.y; //Debug.LogErrorFormat("计算一次! {0} {1}", RB, LT); Center = (RB + LT) / 2; return true; } } return false; } /// <summary> /// 更新顶点信息 /// </summary> /// <returns></returns> public void UpdateVertsByCenter() { LT = LT + Center; RB = RB + Center; } public void UpdateVertsBySize() { LT.x = Center.x - Size.x / 2f; LT.y = Center.y + Size.y / 2f; RB.x = Center.x + Size.x / 2f; RB.y = Center.y - Size.y / 2f; } } [SerializeField] public Border border; public enum State { None, Playing, PlayEnd, } public enum PlayType { Forward, Reverse, } public float mProcess = 0; public float mDuration = 1f; public AnimationCurve mCurve = new AnimationCurve(new Keyframe(), new Keyframe(1f, 1f, 2f, 2f, 0, 0)); public RenderTexture mTempRT; public Vector4 mOffset; public MaskMaterial mMaskMat = new MaskMaterial(603000009, "Assets/Res/Misc/UIMaskCommonAnim.mat"); private State mState = State.None; private float mOffsetX_from = 0.5f; private float mOffsetX_to = -0.5f; private float mOffsetY_from = -0.5f; private float mOffsetY_to = 0.5f; private float mStartTime; private int mOldUILayer; private System.Action mOnFinish; void Update() { if (mState == State.None) return; if (mState == State.Playing) { float passTime = Time.realtimeSinceStartup - mStartTime; if (passTime > mDuration) { OnPlaying(mDuration); mState = State.PlayEnd; } else { OnPlaying(passTime); } } else if (mState == State.PlayEnd) { OnPlayEnd(); mState = State.None; } } private bool OnCheck() { if (UIRoot.list.Count < 1) { Debug.LogError("UIMaskCommonAnim play error because can not found UIRoot!"); return false; } if (PFPostProcessingMgr.Instance.GetPostProcessing<UIMaskCommonAnim>() != null) { Debug.LogError("UIMaskCommonAnim is playing can not play angin!"); return false; } return true; } private void OnPlayBgin(PlayType playType) { UIRoot root = UIRoot.list[0]; float screenHalfWidth = root.manualWidth / 2f; float screenHalfHeight = root.manualHeight / 2f; //左上角uv最大值为 0.5,-0.5 float uvLT_X = 0.5f * (Math.Abs(border.LT.x) / screenHalfWidth); float uvLT_Y = -0.5f * (Math.Abs(border.LT.y) / screenHalfHeight); //左下角uv最大值为 -0.5,0.5 float uvRB_X = -0.5f * (Math.Abs(border.RB.x) / screenHalfWidth); float uvRB_Y = 0.5f * (Math.Abs(border.RB.y) / screenHalfHeight); if (playType == PlayType.Forward) { mOffset = new Vector4(1f, 1f, uvLT_X, uvLT_Y); mOffsetX_from = uvLT_X; mOffsetY_from = uvLT_Y; mOffsetX_to = uvRB_X; mOffsetY_to = uvRB_Y; } else if (playType == PlayType.Reverse) { mOffset = new Vector4(1f, 1f, uvRB_X, uvRB_Y); mOffsetX_from = uvRB_X; mOffsetY_from = uvRB_Y; mOffsetX_to = uvLT_X; mOffsetY_to = uvLT_Y; } SetCamera(); mOldUILayer = gameObject.layer; SetLayer(LayerMask.NameToLayer("Temp3")); PFPostProcessingMgr.Instance.AddPostProcessing(this); mStartTime = Time.realtimeSinceStartup; mState = State.Playing; } private void OnPlaying(float passTime) { mProcess = passTime / mDuration; float process = mCurve.Evaluate(mProcess); process = Mathf.Clamp(process, 0, 1f); mOffset.z = Mathf.Lerp(mOffsetX_from, mOffsetX_to, process); mOffset.w = Mathf.Lerp(mOffsetY_from, mOffsetY_to, process); } private void OnPlayEnd() { PFPostProcessingMgr.Instance.RemovePostProcessing(this); SlaveCamera.Get().ClearRenderTarget(); SetLayer(mOldUILayer); SlaveCamera.Release(); if (mTempRT != null) { RenderTexture.ReleaseTemporary(mTempRT); mTempRT = null; } if (mOnFinish != null) mOnFinish(); } private void SetLayer(int layer) { transform.gameObject.layer = layer; transform.SetChildLayer(layer); } private void SetCamera() { SlaveCamera.Get().ToUICamera(); SlaveCamera.Get().RenderType = CameraRenderType.Base; //从相机取的没有该界面的整个画面 SlaveCamera.Get().SetCullingMask("UI", "Temp3"); mTempRT = RenderTexture.GetTemporary(Screen.width, Screen.height, 0, RenderTextureFormat.ARGB32); SlaveCamera.Get().SetRenderTarget(mTempRT); } private void OnDestroy() { OnPlayEnd(); } #if UNITY_EDITOR void OnDrawGizmos() { Gizmos.matrix = border.Matrix; Gizmos.color = Color.yellow; Gizmos.DrawWireCube(border.Center, border.Size); Gizmos.color = Color.clear; } #endif //------------------------------------------对外接口--------------------------------------------------- public Material Material { get { return mMaskMat.material; } } private bool Play(PlayType playType) { var ret = OnCheck(); if (ret) OnPlayBgin(playType); return ret; } public bool PlayForward(Action callBack = null) { mOnFinish = callBack; return Play(PlayType.Forward); } public bool PlayReverse(Action callBack = null) { mOnFinish = callBack; return Play(PlayType.Reverse); } } }
检视面板
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEditor; using PF.URP.PostProcessing; namespace GameCore { [CustomEditor(typeof(UIMaskCommonAnim), true)] public class UIMaskCommonAnim_Inspector : Editor { private UIMaskCommonAnim mTarget; protected SerializedObject mObject; private bool mInit = false; private bool mEditor = false; private string KEY_INIT = "KEY_INIT_{0}"; private string KEY_EDIT_BORDER = "KEY_EDIT_BORDER_{0}"; public void OnEnable() { mTarget = target as UIMaskCommonAnim; if (mObject == null) mObject = new SerializedObject(target); KEY_INIT = string.Format(KEY_INIT, mTarget.GetHashCode()); KEY_EDIT_BORDER = string.Format(KEY_EDIT_BORDER, mTarget.GetHashCode()); mInit = EditorPrefs.GetBool(KEY_INIT); mEditor = EditorPrefs.GetBool(KEY_EDIT_BORDER); if (!mInit) { mInit = mTarget.border.Init(mTarget); EditorPrefs.SetBool(KEY_INIT, mInit); } } public override void OnInspectorGUI() { if (!mInit) { EditorGUILayout.BeginVertical(EditorStyles.helpBox); EditorGUILayout.LabelField("未检测到UIRoot,请将UIMaskCommonAnim组件所属UI,放置到NGUI UIRoot下面!"); EditorGUILayout.EndVertical(); return; } mObject.Update(); EditorGUI.BeginDisabledGroup(true); EditorGUILayout.ObjectField("Script", mTarget, typeof(UIMaskCommonAnim), false); EditorGUI.EndDisabledGroup(); EditorGUILayout.BeginVertical(GUI.skin.box); SerializedProperty duration = mObject.FindProperty("mDuration"); EditorGUILayout.PropertyField(duration, new GUIContent("动画持续时间")); SerializedProperty curve = mObject.FindProperty("mCurve"); EditorGUILayout.PropertyField(curve, new GUIContent("速度变化曲线")); EditorGUI.BeginDisabledGroup(true); EditorGUILayout.ObjectField("材质球", mTarget.Material, typeof(Material), false); EditorGUI.EndDisabledGroup(); EditorGUILayout.EndVertical(); EditorGUILayout.BeginHorizontal(); EditorGUILayout.LabelField("包围盒"); if (mEditor) { if (GUILayout.Button("保存", GUILayout.Width(50))) { mEditor = false; EditorPrefs.SetBool(KEY_EDIT_BORDER, mEditor); } } else { if (GUILayout.Button("编辑", GUILayout.Width(50))) { mEditor = true; EditorPrefs.SetBool(KEY_EDIT_BORDER, mEditor); } } EditorGUILayout.EndHorizontal(); mObject.ApplyModifiedProperties(); //包围盒属性 EditorGUILayout.BeginVertical(GUI.skin.box); EditorGUI.BeginDisabledGroup(!mEditor); mTarget.border.Center = EditorGUILayout.Vector2Field(" 中心", mTarget.border.Center); mTarget.border.UpdateVertsByCenter(); mTarget.border.Size = EditorGUILayout.Vector2Field(" 范围", mTarget.border.Size); mTarget.border.UpdateVertsBySize(); EditorGUI.EndDisabledGroup(); EditorGUILayout.EndVertical(); if (GUI.changed) { EditorUtility.SetDirty(mTarget); } } } }
我们项目使用URP渲染框架,以下为URP相关代码(Feature与Pass)
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.Rendering; using UnityEngine.Rendering.Universal; using GameCore; namespace PF.URP.PostProcessing { public class UIMaskCommonAnimFeature : ScriptableRendererFeature { public RenderPassEvent mEvent = RenderPassEvent.AfterRenderingPostProcessing; private UIMaskCommonAnimPass mPass; private UIMaskCommonAnim mMaskAnim; private RenderTargetIdentifier mBaseCameraColorTarget; public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData) { mMaskAnim = PFPostProcessingMgr.Instance.GetPostProcessing<UIMaskCommonAnim>(); if (mMaskAnim == null)//控制开关 return; if (renderingData.cameraData.camera != UICamera.mainCamera) return; var cameraColorTarget = renderer.cameraColorTarget; //设置当前需要后期的画面 mPass.Setup(cameraColorTarget, mMaskAnim); //添加到渲染列表 renderer.EnqueuePass(mPass); } public override void Create() { mPass = new UIMaskCommonAnimPass(mEvent); } } }
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.Rendering; using UnityEngine.Rendering.Universal; using GameCore; namespace PF.URP.PostProcessing { public class UIMaskCommonAnimPass : ScriptableRenderPass { private const string mCommandBufferName = "CommandBuffer_UIMaskCommonAnim"; private const string mTempTexName = "UIMaskCommonAnim Temp Texture"; private RenderTargetHandle mTempTex_Handle; private UIMaskCommonAnim mMaskAnim; private FilterMode mFilterMode = FilterMode.Bilinear; private RenderTargetIdentifier mSourceRT_Id; private int mShaderId_FullTex = Shader.PropertyToID("_FullTex"); private int mShaderId_MaskTex_ST = Shader.PropertyToID("_MaskTex_ST"); public UIMaskCommonAnimPass(RenderPassEvent @event) { this.renderPassEvent = @event; mTempTex_Handle.Init(mTempTexName); } public void Setup(RenderTargetIdentifier sourceRT, UIMaskCommonAnim maskAnim) { mSourceRT_Id = sourceRT; mMaskAnim = maskAnim; } private int maskTexId = Shader.PropertyToID("_MaskTex"); public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData) { var cmd = CommandBufferPool.Get(mCommandBufferName); RenderImage(cmd, ref renderingData); context.ExecuteCommandBuffer(cmd); CommandBufferPool.Release(cmd); } private void RenderImage(CommandBuffer cmd, ref RenderingData renderingData) { mMaskAnim.Material.SetTexture(mShaderId_FullTex, mMaskAnim.mTempRT); mMaskAnim.Material.SetVector(mShaderId_MaskTex_ST, mMaskAnim.mOffset); RenderTextureDescriptor opaqueDesc = renderingData.cameraData.cameraTargetDescriptor; //获取临时RT cmd.GetTemporaryRT(mTempTex_Handle.id, opaqueDesc, mFilterMode); //将当前相机的RT经过处理后,存入临时RT Blit(cmd, mSourceRT_Id, mTempTex_Handle.Identifier(), mMaskAnim.Material, -1); //将处理后的RT赋值给相机RT Blit(cmd, mTempTex_Handle.Identifier(), mSourceRT_Id); } public override void FrameCleanup(CommandBuffer cmd) { //释放临时RT cmd.ReleaseTemporaryRT(mTempTex_Handle.id); } } }
文章评论