【Unity】基於矩陣的UGUI引導蒙版方案
阿新 • • 發佈:2019-02-16
UGUI實現引導蒙版有多種方案,可以基於shader,或純粹靠程式實現,這裡分享一種最近在專案中使用的基於shader的蒙版方案。
許多基於shader的引導蒙版方案都是將需要遮罩的區域以vector4的形式傳入shader,這種方式實現簡單且易於理解,而且效果不錯,但使用這種方式容易造成一種無法避免的問題:蒙版區域無法旋轉,當然除非額外傳入遮罩區域的旋轉資訊。
今天要介紹的方案是直接計算出蒙版區域的矩陣(一個規範化空間,可以將座標規範化到0-1內),將矩陣傳入shader,在蒙版shader中處理頂點時將頂點轉入矩陣,以矩陣中的座標是否在0-1範圍內判斷(有點類似投影空間裁剪)。以這種方式實現的蒙版優點是,由於矩陣本身記錄了座標、旋轉、縮放等資訊,不需要在再shader中額外計算,只需要做一次矩陣左乘操作即可擁有同時具有旋轉和遮罩功能的引導蒙版。另外c#指令碼可以快取一個當前矩陣,用來做點選穿透判斷。另一個優點是,由於矩陣資訊本身來自UI的RectTransform,因此可以避開對UI錨點等螢幕自適應規則的計算,總的來說如果熟悉矩陣運算的話,這種方式可以避開很多頭疼的計算。另外由於最終是將蒙版中的畫素座標轉到實際遮罩區域,並且遮罩區域規範化到0-1內,可以做出邊緣過渡柔和的效果。
效果如下:
旋轉支援:
穿透判定:
邊緣柔和:
圖片蒙版:
自動適應UI的錨點等:
首先是Shader程式碼:
然後是C#端,只擷取關鍵部分程式碼, 以下是矩陣計算:
其中ltw矩陣表示被遮罩物件的空間矩陣(其實嚴格意義上不算,因為將這個空間的x和y軸的z值都設定為0,然後z軸強制設定為和世界空間z軸平行,之所以不直接使用被遮罩物件的worldToLocal,是因為避免被遮罩物體如果發生x和y軸旋轉時蒙版會變形,但是如果案例中確定被遮罩物體肯定不會發生x和y軸旋轉,則可以直接去掉ltw矩陣,把公式中的ltw.inverse替換成targetTransform.worldToLocal,效果是一樣的)。m_MaskAreaMatrix根據被遮罩物件的rect資訊計算出一個矩陣,用來把座標規範化到0-1內。 另外可以注意到m_MaskAreaMatrix不是臨時變數,這裡將它快取下來,用來在重寫Image類的IsRaycastLocationValid方法時判斷是否點選到蒙版區域:
更多文章:http://www.lsngo.net
穿透判定:
邊緣柔和:
圖片蒙版:
自動適應UI的錨點等:
首先是Shader程式碼:
Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" #include "UnityUI.cginc" #pragma multi_compile __ UNITY_UI_ALPHACLIP struct appdata_t { float4 vertex : POSITION; float4 color : COLOR; }; struct v2f { float4 vertex : SV_POSITION; fixed4 color : COLOR; float4 worldPosition : TEXCOORD0; float2 clipPosition : TEXCOORD1; }; fixed4 _Color; fixed4 _TextureSampleAdd; float4 _ClipRect; float4x4 internalWorldToMaskMatrix; half2 internalClipAtten; sampler2D _MaskTex; float4 _Offset; v2f vert(appdata_t IN) { v2f OUT; OUT.worldPosition = IN.vertex; OUT.vertex = UnityObjectToClipPos(OUT.worldPosition); #ifdef UNITY_HALF_TEXEL_OFFSET OUT.vertex.xy += (_ScreenParams.zw-1.0)*float2(-1,1); #endif OUT.color = IN.color * _Color; float4 clipPos = mul(internalWorldToMaskMatrix, IN.vertex);//頂點轉入蒙版區域空間(該空間會將座標規範化,如果xy座標在蒙版空間內則必然為0-1內) OUT.clipPosition = clipPos.xy / clipPos.w; return OUT; } fixed4 frag(v2f IN) : SV_Target { half4 color = IN.color; half2 atten = 1-saturate((abs(IN.clipPosition.xy - half2(0.5, 0.5)) - internalClipAtten.x) / (0.5 - internalClipAtten.x));//根據蒙版區域座標計算邊緣柔和 color.a *= 1-saturate(atten.x*atten.y* tex2D(_MaskTex, IN.clipPosition.xy*_Offset.xy+_Offset.zw).a)*internalClipAtten.y; color.a *= UnityGet2DClipping(IN.worldPosition.xy, _ClipRect); #ifdef UNITY_UI_ALPHACLIP clip (color.a - 0.001); #endif return color; } ENDCG
然後是C#端,只擷取關鍵部分程式碼, 以下是矩陣計算:
private void ResetMaterial(RectTransform targetTransform) { if (targetTransform == null) return; m_MaskAreaMatrix = default(Matrix4x4); m_MaskAreaMatrix.m00 = 1 / targetTransform.rect.width; m_MaskAreaMatrix.m03 = -targetTransform.rect.x / targetTransform.rect.width; m_MaskAreaMatrix.m11 = 1 / targetTransform.rect.height; m_MaskAreaMatrix.m13 = -targetTransform.rect.y / targetTransform.rect.height; m_MaskAreaMatrix.m33 = 1; Matrix4x4 ltw = default(Matrix4x4); ltw.m00 = targetTransform.right.x; ltw.m01 = targetTransform.up.x; ltw.m03 = targetTransform.position.x; ltw.m10 = targetTransform.right.y; ltw.m11 = targetTransform.up.y; ltw.m13 = targetTransform.position.y; ltw.m22 = 1; ltw.m23 = targetTransform.position.z; ltw.m33 = 1; m_MaskAreaMatrix = m_MaskAreaMatrix * ltw.inverse * canvas.transform.localToWorldMatrix; if (m_OriginMaskAreaMatrix != m_MaskAreaMatrix) { material.SetMatrix("internalWorldToMaskMatrix", m_MaskAreaMatrix); m_OriginMaskAreaMatrix = m_MaskAreaMatrix; } }
其中ltw矩陣表示被遮罩物件的空間矩陣(其實嚴格意義上不算,因為將這個空間的x和y軸的z值都設定為0,然後z軸強制設定為和世界空間z軸平行,之所以不直接使用被遮罩物件的worldToLocal,是因為避免被遮罩物體如果發生x和y軸旋轉時蒙版會變形,但是如果案例中確定被遮罩物體肯定不會發生x和y軸旋轉,則可以直接去掉ltw矩陣,把公式中的ltw.inverse替換成targetTransform.worldToLocal,效果是一樣的)。m_MaskAreaMatrix根據被遮罩物件的rect資訊計算出一個矩陣,用來把座標規範化到0-1內。 另外可以注意到m_MaskAreaMatrix不是臨時變數,這裡將它快取下來,用來在重寫Image類的IsRaycastLocationValid方法時判斷是否點選到蒙版區域:
Demo下載地址:http://www.lsngo.net/2017/10/25/unity_uimask/public override bool IsRaycastLocationValid(Vector2 screenPoint, Camera eventCamera) { if (base.IsRaycastLocationValid(screenPoint, eventCamera)) { if (!useRaycastMask) { return true; } else { if (m_IsClear) return true; Vector3 worldPos = eventCamera == null ? (Vector3)screenPoint : eventCamera.ScreenToWorldPoint(screenPoint); worldPos = canvas.transform.worldToLocalMatrix.MultiplyPoint(worldPos); worldPos = m_MaskAreaMatrix.MultiplyPoint(worldPos); if (worldPos.x >= 0 && worldPos.x <= 1 && worldPos.y >= 0 && worldPos.y <= 1) { return false; } return true; } } return false; }
更多文章:http://www.lsngo.net