白泽图

  • 文章
    • Unity渲染
    • Unity项目开发
    • 工具
    • 数学
    • 算法
    • 网站搭建
    • 网络&操作系统
蒋程个人博客
互联网技术经验总结&分享
  1. 首页
  2. Unity渲染
  3. 正文

使用后期做界面遮罩动画

2021-02-09 1458点热度 1人点赞 0条评论

游戏中的界面往往会添加一些动效来丰富玩家体验,前段时间我使用后期做了一个界面过渡的小功能,原理并不复杂,不过在刚接收需求的时候思路上也走了一些歪路,先看最终效果:

这个效果其实挺常见的,就是一个遮罩动画,相信大家分分钟就能把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);
        }

    }
}

标签: 暂无
最后更新:2021-02-09

蒋程

这个人很懒,什么都没留下

点赞
< 上一篇
下一篇 >

文章评论

您需要 登录 之后才可以评论

COPYRIGHT © 2023 白泽图. ALL RIGHTS RESERVED.

Theme Kratos Made By Seaton Jiang

登录
注册|忘记密码?