模仿NGUI實現SoftClip(一)
用過NGUI的童鞋都知道UIPanel可以設定一個矩形的Clip區域,它下轄的UIWidget都只能在Clip區域內顯示。今天我就模仿UIPanel實現類似的Clip功能,讓一個3D面片只能在我所指定的矩形區域內顯示。相信看完這篇文章,UIPanel的Clip原理也就不再神祕了。
為了讓難度循序漸進,我們先實現HardClip(邊緣沒有Alpha過渡,直接硬切)。
實現原理:用指令碼算出顯示區域ClipArea的大小和位置,把計算結果通過Material傳給Shader,讓Shader在顯示時做判斷,如果該畫素在ClipArea外就讓其Alpha為0,達到裁切的目的。
準備1:開啟一個Unity專案,自己找一張圖片放到專案裡用於顯示。
準備2:建立一個Unlit Shader取名為Clip,再建立一個Material也取名Clip並選擇“Unlit/Clip”這個Shader,然後把圖片拖到Texture屬性裡。
準備3:在場景中新增一個叫做ClipPanel的空物體(用於配置Clip),再新增一個叫做ClipDrawer的Quad物體並把Clip.Material拖上去(用於顯示)。
準備工作做好後,先建立一個指令碼ClipPanel.cs並拖到ClipPanel上,有了該指令碼的4個變數,我們就能確定顯示區域的大小位置了。指令碼內容如下
using UnityEngine; using System.Collections; using System.Collections.Generic; public class ClipPanel : MonoBehaviour { public float clipWidth = 4f; // 顯示區域寬度 public float clipHeight = 3f; // 顯示區域高度 public float offsetHor = 0; // 顯示區域水平偏移 public float offsetVer = 0; // 顯示區域垂直偏移 // 在Scene視窗中繪製一個白框用來定位顯示區域 private void OnDrawGizmos() { Vector3 panelPointLB = new Vector3(offsetHor - clipWidth * 0.5f, offsetVer - clipHeight * 0.5f); Vector3 panelPointRT = new Vector3(offsetHor + clipWidth * 0.5f, offsetVer + clipHeight * 0.5f); Vector3 worldPointLB = transform.TransformPoint(panelPointLB); Vector3 worldPointRT = transform.TransformPoint(panelPointRT); Vector3 worldPointLT = new Vector3(worldPointLB.x, worldPointRT.y, worldPointRT.z); Vector3 worldPointRB = new Vector3(worldPointRT.x, worldPointLB.y, worldPointLB.z); Gizmos.DrawLine(worldPointLB, worldPointLT); Gizmos.DrawLine(worldPointRB, worldPointRT); Gizmos.DrawLine(worldPointLB, worldPointRB); Gizmos.DrawLine(worldPointLT, worldPointRT); } }
再建立一個指令碼ClipDrawer.cs並拖到ClipDrawer上,然後把ClipPanel物體拖到其panel屬性裡。這個指令碼負責收集ClipPanel的資料然後傳遞給Shader。指令碼內容如下
using UnityEngine; using System.Collections; using System.Collections.Generic; public class ClipDrawer : MonoBehaviour { public ClipPanel panel; private void OnWillRenderObject() { if (panel != null) { // 從panel裡取得裁切視窗資料,轉化為視窗的左下角、右上角兩個座標點(基於panel的本地座標) Vector3 panelPointLB = new Vector3(panel.offsetHor - panel.clipWidth * 0.5f, panel.offsetVer - panel.clipHeight * 0.5f); Vector3 panelPointRT = new Vector3(panel.offsetHor + panel.clipWidth * 0.5f, panel.offsetVer + panel.clipHeight * 0.5f); // 把這兩個點轉化為世界座標點 Vector3 worldPointLB = panel.transform.TransformPoint(panelPointLB); Vector3 worldPointRT = panel.transform.TransformPoint(panelPointRT); // 把這兩個點轉化為ClipDrawer的本地座標點 Vector3 localPointLB = transform.InverseTransformPoint(worldPointLB); Vector3 localPointRT = transform.InverseTransformPoint(worldPointRT); // 恢復為視窗尺寸和偏移資料 Vector2 localSize = new Vector2(localPointRT.x - localPointLB.x, localPointRT.y - localPointLB.y); Vector2 localOffset = (localPointLB + localPointRT) * 0.5f; // 合併資料到一個Vector4中並等待發送 Vector4 clipRange = new Vector4(localSize.x, localSize.y, localOffset.x, localOffset.y); Renderer r = GetComponent<Renderer>(); Material mat = r.materials[0]; // 把資料傳送給Shader,一共4條資料:視窗寬度、視窗高度、視窗水平偏移、視窗垂直偏移(注意這些資料都基於本地座標) mat.SetVector("_ClipRange", clipRange); } } }
這個指令碼每幀都會把最新的裁切視窗資料傳送給自己的Material,供Shader使用。下面到了最關鍵的Shader部分,如果你對Shader一無所知,我推薦你買本《Unity Shader入門精要》學習一下。這裡假定你已經對Shader有一定基礎。下面開啟Clip.shader並修改為如下內容
Shader "Unlit/Clip"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_ClipRange("ClipRange", Vector) = (0, 0, 0, 0)
}
SubShader
{
// 因為需要操控alpha,所以需要設定渲染型別為透明
Tags { "RenderType"="Transparent" "Queue"="Transparent" }
Pass
{
// 因為是透明Shader,需要關閉深度寫入並開啟alpha混合
ZWrite Off
Blend SrcAlpha OneMinusSrcAlpha
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
// 該二維陣列記錄該點的xy座標與顯示區域的關係
float2 relation : TEXCOORD1;
};
sampler2D _MainTex;
float4 _MainTex_ST;
float4 _ClipRange = float4(1.0, 1.0, 0.0, 0.0);
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
// 將頂點的本地座標值和視窗尺寸相除,如果點在視窗內,結果會落在(-1,1)區間內。這行程式碼一定要理解
o.relation = (v.vertex.xy - _ClipRange.zw) / (_ClipRange.xy * 0.5);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
// sample the texture
fixed4 col = tex2D(_MainTex, i.uv);
// 如果畫素點的relation值不在[-1, 1]區間內,就把輸出顏色的alpha改成0
if (i.relation.x < -1 || i.relation.x > 1 || i.relation.y < -1 || i.relation.y > 1)
{
col.a = 0;
}
return col;
}
ENDCG
}
}
}
Shader裡的程式碼很短,理解了座標轉區間那行程式碼就可以了。
然後執行場景,調整ClipDrawer和ClipPanel的位置,就能看到圖片被裁剪的效果,效果如下圖所示
上面的Shader雖然有效但很low,在Shader裡分支語句會影響效能,所以if..else..能避免就避免。下面則提供一個高階點的不帶if的frag函式
fixed4 frag (v2f i) : SV_Target
{
// 處理relation,處理後的結果中,大於0表示點在視窗內,否則點在視窗外
float2 relation1 = float2(1, 1) - abs(i.relation.xy);
// 不需要分別檢查x和y兩個座標,選擇其中較小的值做檢查即可
float relation2 = min(relation1.x, relation1.y);
// 向上取整,大於0的整數表示視窗內的點,小於1的整數表示點在視窗外
float relation3 = ceil(relation2);
// 把資料約束到[0, 1]範圍內,此時的結果只可能是0或者1
float relation4 = clamp(relation3, 0, 1);
// sample the texture
fixed4 col = tex2D(_MainTex, i.uv);
// 把輸出顏色乘以relation4,得到想要的結果
col.a *= relation4;
return col;
}
我們用cg語言自帶的各種數學函式來操作資料,最後達到了和if語句相同的結果。
下文我會新增一個帶alpha過渡的Soft Clip版本shader,敬請期待。