UIDrawCall
文章说明
在阅读 NGUI 源码的时候,希望大家已经非常熟悉这套 UI 了,知道基本组件怎么使用。对于一个陌生的东西,我建议大家不需要先 “求解”,我们可以先学会 “使用”,只有熟练使用了,我们就可以带着问题去看源码,那样,更方便我们理解。
所以如果你阅读过程中有些看不懂,建议看看我只之前关于 untiy 渲染,坐标系 ,相关的文章先了解一下 mesh 。
如果文章中有写的不对的地方,欢迎大家指出,谢谢。
源码分析
代码名:“UIDrawCall”,文章中不会将所有源代码粘贴出来,也不会每一句去讲解,只会讲一些重点的内容,其余内容读者可以自己去阅读。
Drawcall 创建
在 “UIDrawCall” 中存储着两个静态变量:
mActiveList
:表示正在使用中的列表,全局所有的正在使用中的 UIDrawCall 都被保存在这个列表中;mInactiveList
:表示未使用的回收的列表,全局所有回收到的 UIDrawCall 都被保存到这个列表中;
//这里缓存的是全局中被使用的 drawcall
static BetterList<UIDrawCall> mActiveList = new BetterList<UIDrawCall>();
//存储未使用的 drawcall 列表
static BetterList<UIDrawCall> mInactiveList = new BetterList<UIDrawCall>();
创建一个 UIDrawCall :
- 先去
mInactiveList
中寻找是否有未使用的,如果有就获取它,将其从 mInactiveList 列表中移除,并放入到 mActiveList 列表中。 - 如果
mInactiveList
没有多余的可使用的,就去创建一个新的 UIDrawCall ,放入到 mActiveList 列表中。
源码:
static UIDrawCall Create (string name)
{
#if SHOW_HIDDEN_OBJECTS && UNITY_EDITOR
name = (name != null) ? "_UIDrawCall [" + name + "]" : "DrawCall";
#endif
//检查未使用列表中是否有多余的可使用的 dc
while (mInactiveList.size > 0)
{
UIDrawCall dc = mInactiveList.Pop();
if (dc != null)
{
mActiveList.Add(dc);
if (name != null) dc.name = name;
NGUITools.SetActive(dc.gameObject, true);
return dc;
}
}
//没有就先创建一个对象,然后给这个对象添加上 UIDrawCall 组件,并将这个组件添加到使用列表中
#if UNITY_EDITOR
// If we're in the editor, create the game object with hide flags set right away
GameObject go = UnityEditor.EditorUtility.CreateGameObjectWithHideFlags(name,
#if SHOW_HIDDEN_OBJECTS
HideFlags.DontSave | HideFlags.NotEditable, typeof(UIDrawCall));
#else
HideFlags.HideAndDontSave, typeof(UIDrawCall));
#endif
UIDrawCall newDC = go.GetComponent<UIDrawCall>();
#else
GameObject go = new GameObject(name);
DontDestroyOnLoad(go);
UIDrawCall newDC = go.AddComponent<UIDrawCall>();
#endif
// Create the draw call
mActiveList.Add(newDC);
return newDC;
}
Drawcall 更新调用
1.首先介绍一些缓存信息的变量:
- public List
verts
= new List(); 顶点 - public List
norms
= new List(); 法线 - public List
tans
= new List(); 切线 - public List
uvs
= new List(); uv - public List
uv2
= new List(); uv2 - public List
cols
= new List(); 颜色 mMaterial
:被本次 drawcall 调用的 材质,就是我们外部设置的材质,在 baseMaterial 中设置,当创建一个 UIDrawCall 的时候外部会传入一个材质信息,通过 baseMaterial 将这个材质信息保存到这个变量中。mDynamicMat
:动态材质,说明这个材质一直在变化mMesh
:meshmFilter
: Mesh Filter 组件mRenderer
: Mesh Renderer 组件
//设置材质
public Material baseMaterial
{
get
{
return mMaterial;
}
set
{
if (mMaterial != value)
{
mMaterial = value;
mRebuildMat = true;
}
}
}
2.主要函数:
UpdateGeometry()
:UIDrawCall 的核心方法,通过顶点,uv,颜色,贴图等,设置需要绘制的几何信息,主要通过需要当前更新的 UIDrawCall 对象调用这个函数进行数据更新,将一些变化的几何信息重新设置到对应的 MeshFilter, MeshRenderer 和 材质(Material)中保存起来。这里的设置是全部重新设置,也就是说,只要在我们的使用中有涉及到几何信息变化的操作,都会导致这个几何信息被重新计算设置。
源码:
public void UpdateGeometry (int widgetCount)
{
//表示需要调用的 widget 的数量
this.widgetCount = widgetCount;
int vertexCount = verts.Count; //顶点数量
// Safety check to ensure we get valid values
// vertexCount % 4 == 0 说明顶点数量一定是4的倍数
if (vertexCount > 0 && (vertexCount == uvs.Count && vertexCount == cols.Count) && (vertexCount % 4) == 0)
{
if (mColorSpace == ColorSpace.Uninitialized)
mColorSpace = QualitySettings.activeColorSpace;
if (mColorSpace == ColorSpace.Linear)
{
for (int i = 0; i < vertexCount; ++i)
{
var c = cols[i];
c.r = Mathf.GammaToLinearSpace(c.r);
c.g = Mathf.GammaToLinearSpace(c.g);
c.b = Mathf.GammaToLinearSpace(c.b);
c.a = Mathf.GammaToLinearSpace(c.a);
cols[i] = c;
}
}
// Cache all components
//没有就创建一个 meshFilter
if (mFilter == null) mFilter = gameObject.GetComponent<MeshFilter>();
if (mFilter == null) mFilter = gameObject.AddComponent<MeshFilter>();
//顶点个数上限 65000
if (vertexCount < 65000)
{
// Populate the index buffer
//三角形数组的长度 = (顶点个数 / 2) * 3 ======》 (顶点个数/2) 计算三角形个数,
int indexCount = (vertexCount >> 1) * 3;
bool setIndices = (mIndices == null || mIndices.Length != indexCount);
// Create the mesh
//如果没有 mesh 创建一个
if (mMesh == null)
{
mMesh = new Mesh();
mMesh.hideFlags = HideFlags.DontSave;
mMesh.name = (mMaterial != null) ? "[NGUI] " + mMaterial.name : "[NGUI] Mesh";
if (dx9BugWorkaround == 0) mMesh.MarkDynamic();
setIndices = true;
}
#if !UNITY_FLASH
// If the buffer length doesn't match, we need to trim all buffers
//如果缓冲区长度匹配不正确,我们需要修剪所有缓冲区
bool trim = uvs.Count != vertexCount || cols.Count != vertexCount || uv2.Count != vertexCount || norms.Count != vertexCount || tans.Count != vertexCount;
// Non-automatic render queues rely on Z position, so it's a good idea to trim everything
if (!trim && panel != null && panel.renderQueue != UIPanel.RenderQueue.Automatic)
trim = (mMesh == null || mMesh.vertexCount != verts.Count);
// NOTE: Apparently there is a bug with Adreno devices:
// http://www.tasharen.com/forum/index.php?topic=8415.0
#if !UNITY_ANDROID
// If the number of vertices in the buffer is less than half of the full buffer, trim it
if (!trim && (vertexCount << 1) < verts.Count) trim = true;
#endif
#endif
//计算三角形数量
mTriangles = (vertexCount >> 1);
if (mMesh.vertexCount != vertexCount)
{
mMesh.Clear();
setIndices = true;
}
#if UNITY_4_7
var hasUV2 = (uv2 != null && uv2.Count == vertexCount);
var hasNormals = (norms != null && norms.Count == vertexCount);
var hasTans = (tans != null && tans.Count == vertexCount);
if (mTempVerts == null || mTempVerts.Length < vertexCount) mTempVerts = new Vector3[vertexCount];
if (mTempUV0 == null || mTempUV0.Length < vertexCount) mTempUV0 = new Vector2[vertexCount];
if (mTempCols == null || mTempCols.Length < vertexCount) mTempCols = new Color[vertexCount];
if (hasUV2 && (mTempUV2 == null || mTempUV2.Length < vertexCount)) mTempUV2 = new Vector2[vertexCount];
if (hasNormals && (mTempNormals == null || mTempNormals.Length < vertexCount)) mTempNormals = new Vector3[vertexCount];
if (hasTans && (mTempTans == null || mTempTans.Length < vertexCount)) mTempTans = new Vector4[vertexCount];
verts.CopyTo(mTempVerts);
uvs.CopyTo(mTempUV0);
cols.CopyTo(mTempCols);
if (hasNormals) norms.CopyTo(mTempNormals);
if (hasTans) tans.CopyTo(mTempTans);
if (hasUV2) for (int i = 0, imax = verts.Count; i < imax; ++i) mTempUV2[i] = uv2[i];
mMesh.vertices = mTempVerts;
mMesh.uv = mTempUV0;
mMesh.colors = mTempCols;
mMesh.uv2 = hasUV2 ? mTempUV2 : null;
mMesh.normals = hasNormals ? mTempNormals : null;
mMesh.tangents = hasTans ? mTempTans : null;
#else
//将顶点信息, UV信息,颜色信息赋值给网络对象
mMesh.SetVertices(verts);
mMesh.SetUVs(0, uvs);
mMesh.SetColors(cols);
#if UNITY_5_4 || UNITY_5_5_OR_NEWER
mMesh.SetUVs(1, (uv2.Count == vertexCount) ? uv2 : null);
//设置法线信息 给网络对象
mMesh.SetNormals((norms.Count == vertexCount) ? norms : null);
//设置切线信息 给网络对象
mMesh.SetTangents((tans.Count == vertexCount) ? tans : null);
#else
if (uv2.Count != vertexCount) uv2.Clear();
if (norms.Count != vertexCount) norms.Clear();
if (tans.Count != vertexCount) tans.Clear();
mMesh.SetUVs(1, uv2);
mMesh.SetNormals(norms);
mMesh.SetTangents(tans);
#endif
#endif
if (setIndices)
{
mIndices = GenerateCachedIndexBuffer(vertexCount, indexCount);
mMesh.triangles = mIndices;
}
#if !UNITY_FLASH
if (trim || !alwaysOnScreen)
#endif
mMesh.RecalculateBounds();
//将网络对象 mMesh 赋值给 mfilter
mFilter.mesh = mMesh;
}
else
{
mTriangles = 0;
if (mMesh != null) mMesh.Clear();
Debug.LogError("Too many vertices on one panel: " + vertexCount);
}
//获取 MeshRenderer 组件,如果没有就添加
if (mRenderer == null) mRenderer = gameObject.GetComponent<MeshRenderer>();
if (mRenderer == null)
{
mRenderer = gameObject.AddComponent<MeshRenderer>();
#if UNITY_EDITOR
mRenderer.enabled = isActive;
#endif
#if !UNITY_4_7
if (mShadowMode == ShadowMode.None)
{
mRenderer.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off;
mRenderer.receiveShadows = false;
}
else if (mShadowMode == ShadowMode.Receive)
{
mRenderer.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off;
mRenderer.receiveShadows = true;
}
else
{
mRenderer.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.On;
mRenderer.receiveShadows = true;
}
#endif
}
if (mIsNew)
{
mIsNew = false;
if (onCreateDrawCall != null) onCreateDrawCall(this, mFilter, mRenderer);
}
//更新材质 完成一次drawcall
UpdateMaterials();
}
else
{
if (mFilter.mesh != null) mFilter.mesh.Clear();
Debug.LogError("UIWidgets must fill the buffer with 4 vertices per quad. Found " + vertexCount);
}
//清除缓存信息,以下这些只是用于临时缓存
verts.Clear();
uvs.Clear();
uv2.Clear();
cols.Clear();
norms.Clear();
tans.Clear();
}
CreateMaterial()
:创建一个新材质,主要设置新材质中一些重要的信息,比如 Shader。
这里需要补充一个新知识:
3.12.0 NGUI 默认的以 “Unlit/Transparent Colored” 开头的 shader 有如下六种:
- 1.
Unlit/Transparent Colored
- 2.
Unlit/Transparent Colored (Packed) (TextureClip)
- 3.
Hidden/Unlit/Transparent Colored (TextureClip)
- 4.
Hidden/Unlit/Transparent Colored 1
- 5.
Hidden/Unlit/Transparent Colored 2
- 6.
Hidden/Unlit/Transparent Colored 3
UIPanel 的裁剪主要是通过名字为 “Hidden/Unlit/Transparent Colored N” 的 shader 实现的,N 小于等于3,如果项目中这几种不能满足需求,可以按照这种方式自定义第 4 种或者更多的 shader。
源码:
//创建一个新的材质,设置好我们需要调用的 shader 和材质信息
void CreateMaterial ()
{
mTextureClip = false;
mLegacyShader = false;
mClipCount = panel.clipCount; //设置 mClipCount 个数,裁剪的 panel 个数
//给 shader 赋值一个默认名字
/* 默认的以 “Unlit/Transparent Colored”开头的shader 有如下几种:
* 1.Unlit/Transparent Colored
* 2.Unlit/Transparent Colored (Packed) (TextureClip)
* 3.Hidden/Unlit/Transparent Colored (TextureClip)
* 4.Hidden/Unlit/Transparent Colored 1
* 5.Hidden/Unlit/Transparent Colored 2
* 6.Hidden/Unlit/Transparent Colored 3
*/
string shaderName = (mShader != null) ? mShader.name :
((mMaterial != null) ? mMaterial.shader.name : "Unlit/Transparent Colored");
// Figure out the normal shader's name
//找出正常着色器的名称, GUI/Text Shader 命名的需要文本替换
shaderName = shaderName.Replace("GUI/Text Shader", "Unlit/Text");
//删除 以“ N” 结尾的字符串,N表示数字(0-9)
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);
}
}
//删除 以“Hidden/”开头的字符串
if (shaderName.StartsWith("Hidden/"))
shaderName = shaderName.Substring(7);
// Legacy functionality
const string soft = " (SoftClip)";
shaderName = shaderName.Replace(soft, "");
//删除 “ (TextureClip)”
const string textureClip = " (TextureClip)";
shaderName = shaderName.Replace(textureClip, "");
//如果是Mask裁剪,就直接寻找 Hidden/Unlit/Transparent Colored (TextureClip) 这样 或者 Hidden/Unlit/Text (TextureClip)这样的shader
if (panel != null && panel.clipping == Clipping.TextureMask)
{
mTextureClip = true;
shader = Shader.Find("Hidden/" + shaderName + textureClip);
}
//如果有一个或者多个裁剪panel 就设置成Soft Clip的形式
else if (mClipCount != 0)
{
//寻找的Shader格式为 Hidden/Unlit/Transparent Colored 1 或者 Hidden/Unlit/Transparent Colored 2 这样的 ,上述看是3个
//也就是说目前 NGUI 接受的 mClipCount 最大是3层嵌套
shader = Shader.Find("Hidden/" + shaderName + " " + mClipCount);
if (shader == null) shader = Shader.Find(shaderName + " " + mClipCount);
// Legacy functionality 看备注这个应该是遗留功能
if (shader == null && mClipCount == 1)
{
//一般情况下不会走到这里
mLegacyShader = true;
shader = Shader.Find(shaderName + soft);
}
}
else shader = Shader.Find(shaderName);
// Always fallback to the default shader
// 如果找不到 就用默认的shader, 一般设置成这个shader,就是达不到你需要的效果了
if (shader == null) shader = 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 (shader != null)
{
mDynamicMat.shader = shader;
}
else if (mClipCount != 0)
{
Debug.LogError(shaderName + " shader doesn't have a clipped shader version for " + mClipCount + " clip regions");
}
}
else
{
mDynamicMat = new Material(shader);
mDynamicMat.name = "[NGUI] " + shader.name;
mDynamicMat.hideFlags = HideFlags.DontSave | HideFlags.NotEditable;
}
}
//重建材质
Material RebuildMaterial ()
{
// Destroy the old material
//删除旧的材质
NGUITools.DestroyImmediate(mDynamicMat);
// Create a new material
//创建一个新的材质
CreateMaterial();
mDynamicMat.renderQueue = mRenderQueue;
// Update the renderer
if (mRenderer != null)
{
//需要将材质信息存储在 meshrender 组件 sharedMaterials 中
mRenderer.sharedMaterials = new Material[] { mDynamicMat };
mRenderer.sortingLayerName = mSortingLayerName;
mRenderer.sortingOrder = mSortingOrder;
}
return mDynamicMat;
}
//更新材质
void UpdateMaterials ()
{
if (panel == null) return;
// If clipping should be used, we need to find a replacement shader
//如果没有材质或者使用了裁剪,就需要重建材质
//判断是否需要重建材质信息
if (mRebuildMat || mDynamicMat == null || mClipCount != panel.clipCount || mTextureClip != (panel.clipping == Clipping.TextureMask))
{
RebuildMaterial();
mRebuildMat = false;
}
}
Drawcall 渲染调用
OnWillRenderObject()
:这个函数是用于做一些渲染之前的一些设置的,一般用于设置材质的参数 还有 裁剪信息。
关于这一块我决定重新开一篇细讲,因为涉及到 Shader 中的信息,放在一篇文章中太多啦!
Drawcall 销毁
Destroy()
:移除一个 UIDrawCall
源码:
static public void Destroy (UIDrawCall dc)
{
if (dc)
{
if (dc.onCreateDrawCall != null)
{
NGUITools.Destroy(dc.gameObject);
return;
}
dc.onRender = null;
if (Application.isPlaying)
{
if (mActiveList.Remove(dc))
{
NGUITools.SetActive(dc.gameObject, false);
mInactiveList.Add(dc);
dc.mIsNew = true;
}
}
else
{
mActiveList.Remove(dc);
#if SHOW_HIDDEN_OBJECTS && UNITY_EDITOR
if (UnityEditor.Selection.activeGameObject == dc.gameObject)
UnityEditor.Selection.activeGameObject = null;
#endif
NGUITools.DestroyImmediate(dc.gameObject);
}
}
}
思考
谁创建的,什么时候创建?
谁调用的,什么时候调用?
谁移除的,什么时候移除?
任何时候我们都要带着这种不断思考的态度去学习,不着急,我们接下来的文章中,慢慢揭晓答案。
Drawcall 的意义
妈耶,到这里我感觉又要来一篇文章讲一下渲染流程了,好累,学的越多感觉越无知。
这里我简单介绍一下什么是 “drawcall”。CPU 通过调用 drawcall 来告诉 GPU 开始进行一个渲染过程。对于 CPU 来讲,drawcall 就是一个命令,通过这个命令让 GPU 进行渲染工作。那么 NGUI 的 UIDrawcall 是什么,可以理解为,UIDrawcall 是 NGUI 的一种上层封装,处理一些 drawcall 中需要的数据,在这里面 NGUI 可以做一些拓展功能。