這一塊我之前一直看得不是很懂,前幾個月有一次做粒子裁剪的時候想模仿做,沒有做出來,只能靠着很挫的if來判斷。這幾天又有一個需求想實現圖片的裁剪,不可能一個圖片一個panel吧,所以就想到了自己實現Clip Sprite。
在這裏先分析一下Ngui Panel的軟裁剪是怎麼實現的。我之前沒有看懂的原因是,NGUI對這塊做了大量的優化,單獨看一行代碼,或者一個公式是看不出什麼的,單獨看shader也會雲裏霧裏,所以這裏我們要結合shader和代碼一起看,才能真正瞭解ngui的設計精巧之處。
首先,我們知道,所有的NGUI渲染都是通過UIDrawCall這個類實現的,所以我們主要分析這個類。
NGUI考慮到了嵌套Soft Clip的情況,設計了一套支持多重嵌套的Soft Clip架構,目前最多支持到三重嵌套。不同嵌套對應的Shader是不一樣的,具體設置函數在這(分析也貼在上面了)
void CreateMaterial ()
{
mTextureClip = false;
mLegacyShader = false;
mClipCount = panel.clipCount;
string shaderName = (mShader != null) ? mShader.name :
((mMaterial != null) ? mMaterial.shader.name : "Unlit/Transparent Colored");
// Figure out the normal shader's name
shaderName = shaderName.Replace("GUI/Text Shader", "Unlit/Text");
if (shaderName.Length > 2)
{
if (shaderName[shaderName.Length - 2] == ' ')
{
int index = shaderName[shaderName.Length - 1];
if (index > '0' && index <= '9') shaderName = shaderName.Substring(0, shaderName.Length - 2);
}
}
shaderName = shaderName.Replace(TEXTURE_CLIP, "");
var newShader = shader;
//如果是Mask裁剪,就直接尋找 Unlit/Transparent Colored(TextureClip)這樣 或者 Unlit/Text(TextureClip)這樣的shader
if (panel.clipping == Clipping.TextureMask)
{
mTextureClip = true;
newShader = Shader.Find(shaderName + TEXTURE_CLIP);
}
else if (mClipCount != 0) //如果父節點有多個裁剪panel 就設置成Soft Clip的形式
{
//尋找的Shader格式爲 Unlit/Transparent Colored 1 或者 Unlit/Transparent Colored 2 這樣的 ,後綴就是有多少層,最大ngui寫到了3,一般夠用了
var shaderPath = shaderName + " " + mClipCount;
newShader = Shader.Find(shaderPath);
}
else newShader = Shader.Find(shaderName);
// Always fallback to the default shader 如果找不到 就用默認的shader 一般設置成這個shader,就是達不到你需要的效果了
if (newShader == null) newShader = Shader.Find("Unlit/Transparent Colored");
if (mMaterial != null)
{
mDynamicMat = new Material(mMaterial);
mDynamicMat.name = "[NGUI] " + mMaterial.name;
mDynamicMat.hideFlags = HideFlags.DontSave | HideFlags.NotEditable;
mDynamicMat.CopyPropertiesFromMaterial(mMaterial); //這裏會把之前的材質的所有屬性全部複製到新的材質上(貼圖啊什麼的 很方便啊)
#if !UNITY_FLASH
string[] keywords = mMaterial.shaderKeywords;
for (int i = 0; i < keywords.Length; ++i)
mDynamicMat.EnableKeyword(keywords[i]);
#endif
// If there is a valid shader, assign it to the custom material
if (newShader != null)
{
mDynamicMat.shader = newShader;
}
else if (mClipCount != 0)
{
Debug.LogError(shaderName + " shader doesn't have a clipped shader version for " + mClipCount + " clip regions");
}
}
else
{
mDynamicMat = new Material(newShader);
mDynamicMat.name = "[NGUI] " + newShader.name;
mDynamicMat.hideFlags = HideFlags.DontSave | HideFlags.NotEditable;
}
}
所以到時候我們只需要最終去找Unlit/Transparent Colored 1這樣的shader就好了
這個類中與裁剪相關的代碼在OnWillRenderObject這個函數中,這個函數是用於設置渲染之前的一些設置的,一般用於設置材質的參數。我將函數貼在下面
void OnWillRenderObject ()
{
UpdateMaterials();
if (onRender != null) onRender(mDynamicMat ?? mMaterial);
if (mDynamicMat == null || mClipCount == 0) return;
if (mTextureClip)
{
Vector4 cr = panel.drawCallClipRange;
Vector2 soft = panel.clipSoftness;
Vector2 sharpness = new Vector2(1000.0f, 1000.0f);
if (soft.x > 0f) sharpness.x = cr.z / soft.x;
if (soft.y > 0f) sharpness.y = cr.w / soft.y;
mDynamicMat.SetVector(ClipRange[0], new Vector4(-cr.x / cr.z, -cr.y / cr.w, 1f / cr.z, 1f / cr.w));
mDynamicMat.SetTexture("_ClipTex", clipTexture);
}
else if (!mLegacyShader)
{
UIPanel currentPanel = panel;
for (int i = 0; currentPanel != null; )
{
if (currentPanel.hasClipping)
{
float angle = 0f;
Vector4 cr = currentPanel.drawCallClipRange;
// Clipping regions past the first one need additional math
if (currentPanel != panel)
{
Vector3 pos = currentPanel.cachedTransform.InverseTransformPoint(panel.cachedTransform.position);
cr.x -= pos.x;
cr.y -= pos.y;
Vector3 v0 = panel.cachedTransform.rotation.eulerAngles;
Vector3 v1 = currentPanel.cachedTransform.rotation.eulerAngles;
Vector3 diff = v1 - v0;
diff.x = NGUIMath.WrapAngle(diff.x);
diff.y = NGUIMath.WrapAngle(diff.y);
diff.z = NGUIMath.WrapAngle(diff.z);
if (Mathf.Abs(diff.x) > 0.001f || Mathf.Abs(diff.y) > 0.001f)
Debug.LogWarning("Panel can only be clipped properly if X and Y rotation is left at 0", panel);
angle = diff.z;
}
// Pass the clipping parameters to the shader
SetClipping(i++, cr, currentPanel.clipSoftness, angle);
}
currentPanel = currentPanel.parentPanel;
}
}
else // Legacy functionality
{
Vector2 soft = panel.clipSoftness;
Vector4 cr = panel.drawCallClipRange;
Vector2 v0 = new Vector2(-cr.x / cr.z, -cr.y / cr.w);
Vector2 v1 = new Vector2(1f / cr.z, 1f / cr.w);
Vector2 sharpness = new Vector2(1000.0f, 1000.0f);
if (soft.x > 0f) sharpness.x = cr.z / soft.x;
if (soft.y > 0f) sharpness.y = cr.w / soft.y;
mDynamicMat.mainTextureOffset = v0;
mDynamicMat.mainTextureScale = v1;
mDynamicMat.SetVector("_ClipSharpness", sharpness);
}
}
可以看到,這裏有3個分支,
if (mTextureClip)
{
...
}
對應的是圖片裁剪,作用是設置一張Mask圖片,然後Panel會根據圖片的Alpha值來設置顯示區域。
第二個分支就是比較重要了(一般不會走到第三個分支,因爲mLegacyShader似乎被寫死成爲false了)
else if (!mLegacyShader)
{
...
}
這一個分支有什麼用呢?就是用於給軟裁剪設置裁剪範圍的。下面的代碼段是我對下面這個分支的理解 else if (!mLegacyShader)
{
UIPanel currentPanel = panel;
//遍歷所有的父Panel(所以支持嵌套的Soft Clip)
for (int i = 0; currentPanel != null; )
{
//如果父Panel節點有裁剪
if (currentPanel.hasClipping)
{
//這個裏面計算裁剪範圍和父Panel和當前DrawCall所屬的Panel的角度
float angle = 0f;
Vector4 cr = currentPanel.drawCallClipRange;
//如果有多重Soft Clip的話 就會走到這裏
// Clipping regions past the first one need additional math
if (currentPanel != panel)
{
Vector3 pos = currentPanel.cachedTransform.InverseTransformPoint(panel.cachedTransform.position);
cr.x -= pos.x;
cr.y -= pos.y;
Vector3 v0 = panel.cachedTransform.rotation.eulerAngles;
Vector3 v1 = currentPanel.cachedTransform.rotation.eulerAngles;
Vector3 diff = v1 - v0;
//其實這個函數就是 把角度限制在-180和180之間
diff.x = NGUIMath.WrapAngle(diff.x);
diff.y = NGUIMath.WrapAngle(diff.y);
diff.z = NGUIMath.WrapAngle(diff.z);
if (Mathf.Abs(diff.x) > 0.001f || Mathf.Abs(diff.y) > 0.001f)
Debug.LogWarning("Panel can only be clipped properly if X and Y rotation is left at 0", panel);
//因爲是界面 所以角度是平面的 只有Z是有效角度
angle = diff.z;
}
//這裏就是真正設置裁剪的地方了,我們看看這裏的各個參數的意義吧。
//如果只是單層Soft Clip的話
// i = 0,
// cr就是this.panel的 drawCallClipRange,而這個drawCallClipRange的各個參數意義是這樣的: x:中心點X座標 y:中心點Y座標 z:panel width的一半, w:panel height的一半
// currentPanel.clipSoftness 軟裁剪設置的漸變邊緣
// angle 與父Panel的角度 如果 是單層shader的話,這個角度是0(而且shader裏面也不會用到)
// Pass the clipping parameters to the shader
SetClipping(i++, cr, currentPanel.clipSoftness, angle);
}
currentPanel = currentPanel.parentPanel;
}
}
可以看到最重要的函數就是SetClipping了 我們走進去瞧瞧。
void SetClipping (int index, Vector4 cr, Vector2 soft, float angle)
{
angle *= -Mathf.Deg2Rad;
Vector2 sharpness = new Vector2(1000.0f, 1000.0f);
if (soft.x > 0f) sharpness.x = cr.z / soft.x;
if (soft.y > 0f) sharpness.y = cr.w / soft.y;
//這個ClipRange在awake被寫死成數量爲4的數組,命名規則爲:_ClipRange0 _ClipRange1 _ClipRange2 _ClipRange3這樣
//ClipArgs同理 規則爲 _ClipArgs0 _ClipArgs1 _ClipArgs2 _ClipArgs3
//所以我們自己擴展的話,也要跟着他的命名規則走。
//如果是單層soft clip的話,index = 0,所以設置的參數名字爲_ClipRange0和_ClipArgs0
if (index < ClipRange.Length)
{
mDynamicMat.SetVector(ClipRange[index], new Vector4(-cr.x / cr.z, -cr.y / cr.w, 1f / cr.z, 1f / cr.w));
mDynamicMat.SetVector(ClipArgs[index], new Vector4(sharpness.x, sharpness.y, Mathf.Sin(angle), Mathf.Cos(angle)));
}
}
在這裏,有一堆看不懂的公式,我們先不要管,我們先走進shader裏面看看,這些參數是怎麼用的,因爲單獨看這些公式是沒有用的。我們就只分析單層的soft clip,所以去Unlit - Transparent Colored 1.shader這個shader裏面看看,我們先分析頂點着色器
v2f vert (appdata_t v)
{
o.vertex = mul(UNITY_MATRIX_MVP, v.vertex);
o.color = v.color;
o.texcoord = v.texcoord;
//在頂點着色器裏面計算了一個所謂的world pos,這個world pos是一個什麼東西呢?結合之前在SetCliping函數中的參數,可以得知這個完整的公式如下
// o.worldPos = float2(v.vertex.x / cr.z - cr.x / cr.z, v.vertex.y / cr.w - cr.y / cr.w);
//最終實際的得到的是一個標準化值,-1~1範圍內的值表示爲在裁剪區域範圍內,超過這個值的像素點就被裁剪掉了
o.worldPos = v.vertex.xy * _ClipRange0.zw + _ClipRange0.xy;
return o;
}
我們通過一個圖來分析這個公式的意義:
圖片隨便畫的 比較low,最終實際的得到的是一個標準化值,-1~1範圍內的值表示爲在裁剪區域範圍內,超過這個值的像素點就被裁剪掉了
接下來,我們到像素着色器裏面看看究竟是怎麼裁剪的:
half4 frag (v2f IN) : COLOR
{
//這個公式可以看成兩個部分 第一個部分是(float2(1.0, 1.0) - abs(IN.worldPos))
//意義是 把 worldPos轉換成另外一種表現形式,就是大於0的就是在像素範圍內,小於0的就是在像素範圍外
//同時 * _ClipArgs0實際上是在做漸變邊緣計算,這裏的_ClipArgs0實際上是cr.z / soft.x和 cr.z / soft.x;
//所以這個 * _ClipArgs0的意義把factor最終設置成顯示範圍以內並且在不在漸變邊緣的 設置成大於1的,在漸變邊緣的,設置成0-1的 裁剪的 設置成小於0的
// Softness factor
float2 factor = (float2(1.0, 1.0) - abs(IN.worldPos)) * _ClipArgs0;
// Sample the texture
half4 col = tex2D(_MainTex, IN.texcoord) * IN.color;
//這裏的alpha就非常簡單了,先取得xy中最小的那個,然後大於1的設置爲1 小於0的設置爲0,完事
col.a *= clamp( min(factor.x, factor.y), 0.0, 1.0);
return col;
}
我們通過一張圖片看看factor的計算結果
總結一下,NGUI在通過這樣比較隱晦的方式去實現這個shader,避免了在shader中出現if和除法,使得這種裁剪方式非常高效而且精巧,值得學習