ngui UIPanel绘制原理
在初学ngui时,我对UIPanel的绘制原理不太了解,于是查找了相关文章,在此将学习成果分享给大家。
UIPanel基本原理
UIPanel本质上是一个面片。UIDrawCall会动态实例化出材质球,然后通过UV偏移和缩放来实现滑动显示的效果。由于材质球是动态实例化的,并且UIPanel属于Editor界面,所以我们无法直接看到它。此外,网格的Gizmos显示似乎经过了改动,比较隐蔽。
查找过程与分析
1. 定位UIPanel与UIDrawCall的关联
首先,在UIPanel中找到mClipRange,接着在Fill方法里发现了与UIDrawCall相关的代码。以下是Fill方法的详细内容:
void Fill (Material mat)
{
// 清理已删除的widget
for (int i = mWidgets.size; i > 0; ) if (mWidgets[--i] == null) mWidgets.RemoveAt(i);
// 为指定材质填充缓冲区
for (int i = 0, imax = mWidgets.size; i < imax; ++i)
{
UIWidget w = mWidgets.buffer[i];
if (w.visibleFlag == 1 && w.material == mat)
{
UINode node = GetNode(w.cachedTransform);
if (node != null)
{
if (generateNormals) w.WriteToBuffers(mVerts, mUvs, mCols, mNorms, mTans);
else w.WriteToBuffers(mVerts, mUvs, mCols, null, null);
}
else
{
Debug.LogError("No transform found for " + NGUITools.GetHierarchy(w.gameObject), this);
}
}
}
if (mVerts.size > 0)
{
// 重建绘制调用的网格
UIDrawCall dc = GetDrawCall(mat, true);
dc.depthPass = depthPass;
dc.Set(mVerts, generateNormals ? mNorms : null, generateNormals ? mTans : null, mUvs, mCols);
}
else
{
// 此材质没有要绘制的内容,移除绘制调用
UIDrawCall dc = GetDrawCall(mat, false);
if (dc != null)
{
mDrawCalls.Remove(dc);
NGUITools.DestroyImmediate(dc.gameObject);
}
}
// 清理缓冲区
mVerts.Clear();
mNorms.Clear();
mTans.Clear();
mUvs.Clear();
mCols.Clear();
}
在这个方法中,会先清理已删除的widget,然后遍历所有可见且使用指定材质的widget,将其顶点、UV、颜色等信息写入缓冲区。如果缓冲区中有数据,就会创建或更新对应的UIDrawCall;如果没有数据,则移除相应的UIDrawCall。最后,清理所有缓冲区。
2. UIDrawCall对材质球的创建控制
经过分析发现,UIDrawCall是对材质球创建控制的底层实现。不过,它没有采用单例模式,而是以组合的方式实现,这导致UI组件的粒度比较大。
3. Panel软硬边裁剪的实现
UpdateMaterials方法实现了Panel的软硬边裁剪功能,以下是该方法的代码:
void UpdateMaterials()
{
bool useClipping = (mClipping != Clipping.None);
// 如果需要裁剪,创建裁剪材质
if (useClipping)
{
Shader shader = null;
if (mClipping != Clipping.None)
{
const string alpha = " (AlphaClip)";
const string soft = " (SoftClip)";
// 确定普通着色器的名称
string shaderName = mSharedMat.shader.name;
shaderName = shaderName.Replace(alpha, "");
shaderName = shaderName.Replace(soft, "");
// 尝试查找新的着色器
if (mClipping == Clipping.HardClip || mClipping == Clipping.AlphaClip) shader = Shader.Find(shaderName + alpha);
else if (mClipping == Clipping.SoftClip) shader = Shader.Find(shaderName + soft);
// 如果找到有效的着色器,将其分配给自定义材质
if (shader == null) mClipping = Clipping.None;
}
// 如果找到着色器,创建新的材质
if (shader != null)
{
if (mClippedMat == null)
{
mClippedMat = mSharedMat;
mClippedMat.hideFlags = HideFlags.DontSave;
}
mClippedMat.shader = shader;
mClippedMat.mainTexture = mSharedMat.mainTexture;
}
else if (mClippedMat != null)
{
NGUITools.Destroy(mClippedMat);
mClippedMat = null;
}
}
else if (mClippedMat != null)
{
NGUITools.Destroy(mClippedMat);
mClippedMat = null;
}
// 如果需要深度通道,创建深度材质
if (mDepthPass)
{
if (mDepthMat == null)
{
Shader shader = Shader.Find("Unlit/Depth Cutout");
mDepthMat = new Material(shader);
mDepthMat.hideFlags = HideFlags.DontSave;
}
mDepthMat.mainTexture = mSharedMat.mainTexture;
}
else if (mDepthMat != null)
{
NGUITools.Destroy(mDepthMat);
mDepthMat = null;
}
// 确定要使用的材质
Material mat = (mClippedMat != null) ? mClippedMat : mSharedMat;
if (mDepthMat != null)
{
// 如果已经在使用此材质,不做任何操作
if (mRen.sharedMaterials != null && mRen.sharedMaterials.Length == 2 && mRen.sharedMaterials[1] == mat) return;
// 设置双材质
mRen.sharedMaterials = new Material[] { mDepthMat, mat };
}
else if (mRen.sharedMaterial != mat)
{
mRen.sharedMaterials = new Material[] { mat };
}
}
该方法会根据mClipping的值判断是否需要裁剪,如果需要,则根据裁剪类型查找相应的着色器,并创建裁剪材质。同时,还会处理深度通道材质的创建和使用,最后确定要使用的材质并设置给渲染器。
4. 对材质球调用的可疑点
在OnWillRenderObject()方法中,对材质球的调用十分可疑:
mClippedMat.mainTextureOffset = new Vector2(-mClipRange.x / mClipRange.z, -mClipRange.y / mClipRange.w);
mClippedMat.mainTextureScale = new Vector2(1f / mClipRange.z, 1f / mClipRange.w);
这里通过mClipRange的值来设置mClippedMat的纹理偏移和缩放,可能与滑动显示效果的实现有关。
5. 验证想法
为了验证前面的想法,我们对动态实例化的材质球进行了修改,手动调节UV。在UpdateMaterials()方法中,将代码:
//mClippedMat = new Material(mSharedMat);
mClippedMat = mSharedMat;
6. 测试结论
经过上述测试,可以确定UIPanel确实是一个面片。
通过以上的分析,我们对ngui中UIPanel的绘制原理有了更深入的了解,包括材质球的创建、裁剪的实现以及UV的调节等方面。