0. 概述
本文将从整体类图出发,先对NGUI渲染涉及到几个重点的类的关系有一个整体的了解,接着再讲下各个类的作用,然后通过源码将下整个渲染的流程,最后尝试解答几个问题。本文使用的NGUI版本是3.8.2。
1. 整体类图
NGUI_Class.png我们从图中可以看到涉及到NGUI渲染流程的类主要有UIRect、UIWidget、UIPanel、UIDrawcall和UIGeometry。
2. 各个类的作用
2.1 UIRect
UIRect作为UIWidget和UIPanel的抽象基类,主要作用是维护4个锚点(左/上/右/下),并根据锚定更新类型在适当的时候更新4个锚点,并提供OnAnchor抽象方法让子类根据锚点更新自己的尺寸。
2.2 UIGeometry
- UIGeometry由UIWidget生成,UIWidget都有一个UIGeometry
- UIGeometry包含Widget的顶点verts,几何顶点的纹理坐标uvs,几何顶点的颜色cols,并提供接口将顶点数据转换为所属Panel局部坐标的顶点mRtpVerts,法线mRtpNormal和切线数据mRtpTan。
- UIGeometry将几何数据生成分开为3个步骤,保证widget只有在自己发生变化的时候重build
- 1.清空缓存-对应方法Clear
- 根据世界坐标到panel本地坐标的矩阵将转换Widget的顶点数据- 对应方法ApplyTransform
- 向特定传入的buffer填入转换后的顶点等数据-对应方法WriteToBuffers
2.3 UIWidget
- UIWidget是每个UI组件如我们常见的UITexture、UISprite、UILabel的基类,每个UI元素对应一个UIWidget。
- UIWidget包含UI元素信息如width、height、depth、pivot、alpha、color,并提供几个抽象属性material、mainTexture和shader由子类
去实现。 - UIWidget都有一个UIGeometry,并提供OnFill抽象方法,由子类如UILabel和UISprite将顶点、UV、Color、法线和切线数据填入UIGeometry缓存中。
- UIWidget都有一个UIDrawcall,但一个UIDrawcall对应1个或者多个的UIWidget,UI元素的渲染最终是通过UIDrawcall实现的。
2.4 UIDrawCall
- UIDrawcall是渲染UI元素的载体,由UIPanel生成,其中包含渲染使用的MeshRender、Mesh、MeshFilter和Material组件。
- 其核心函数是UpdateGeometry,其分为以下几个步骤进行几何信息更新:1. 生成Mesh,2.赋值缓存的顶点、UV、颜色、法线和切线等数据给Mesh,3.更新MeshRender使用的材质
2.5 UIPanel
- UIPanel作为UIWidget列表和UIDrawcall列表的管理者,监测UIPanel下的Widget的变化,一旦Widget发生变化就需要更新widget对应的drawcall。在必要的时候需要重新生成全部的drawcall。
- UIPanel的UIWidget列表是按照widget的深度排序的
- UIPanel维护一个静态的panel列表,是按照panel的深度排序的
3. 渲染流程
[图片上传中...(NGUI_UIPanel.png-f91a26-1571404641111-0)]
3.1 UIPanel的LateUpdate
先遍历根据深度进行排序的静态的Panel列表,并调用UIPanel的UpdateSelf函数更新每个Panel.
再遍历Panel列表根据Panel的RenderQueue类型不同(Automatic/StartAt/Explicit),设置相应的RenderQueue数据并调用Panel的UpdateDrawCalls更新Drawcall。
void LateUpdate ()
{
#if UNITY_EDITOR
if (mUpdateFrame != Time.frameCount || !Application.isPlaying)
#else
if (mUpdateFrame != Time.frameCount)
#endif
{
mUpdateFrame = Time.frameCount;
// Update each panel in order
for (int i = 0, imax = list.Count; i < imax; ++i)
list[i].UpdateSelf();
int rq = 3000;
// Update all draw calls, making them draw in the right order
for (int i = 0, imax = list.Count; i < imax; ++i)
{
UIPanel p = list[i];
if (p.renderQueue == RenderQueue.Automatic)
{
p.startingRenderQueue = rq;
p.UpdateDrawCalls();
rq += p.drawCalls.Count;
}
else if (p.renderQueue == RenderQueue.StartAt)
{
p.UpdateDrawCalls();
if (p.drawCalls.Count != 0)
rq = Mathf.Max(rq, p.startingRenderQueue + p.drawCalls.Count);
}
else // Explicit
{
p.UpdateDrawCalls();
if (p.drawCalls.Count != 0)
rq = Mathf.Max(rq, p.startingRenderQueue + 1);
}
}
}
}
3.2 UIPanel的UpdateSelf
3.2.1 流程
- UpdateTransformMatrix
更新世界坐标到Panel本地坐标的矩阵和裁剪区域 - UpdateLayers
更新widget layer,保持widget的layer与panel一致 - UpdateWidgets
更新属于Panel下的所有widget- 判断是否重Build
- 是 调用UIPanel的FillAllDrawCalls重新生成Drawcall
- 否 遍历Drawcall列表如果Drawcall.isDirty就调用FillDrawCall更新对应Drawcall
- 判断是否重Build
3.2.2 代码
void UpdateSelf ()
{
mUpdateTime = RealTime.time;
UpdateTransformMatrix();
UpdateLayers();
UpdateWidgets();
if (mRebuild)
{
mRebuild = false;
FillAllDrawCalls();
}
else
{
for (int i = 0; i < drawCalls.Count; )
{
UIDrawCall dc = drawCalls[i];
if (dc.isDirty && !FillDrawCall(dc))
{
UIDrawCall.Destroy(dc);
drawCalls.RemoveAt(i);
continue;
}
++i;
}
}
if (mUpdateScroll)
{
mUpdateScroll = false;
UIScrollView sv = GetComponent<UIScrollView>();
if (sv != null) sv.UpdateScrollbars();
}
}
3.3 UIPanel的UpdateWidgets
3.3.1 流程
- 遍历调用Widget列表调用UIWidget的UpdateTransform判断widget的位置是否发生变化
- 调用widget的UpdateVisibility函数更新其可见性
- 调用widget的UpdateGeometry函数更新其几何数据,返回widget的几何数据是否发生改变
如果几何信息发生改变- drawcall存在,设置drawcall dirty
- drawcall没有则调用FindDrawCall寻找widget对应的drawcall
3.3.2 代码
void UpdateWidgets()
{
#if UNITY_EDITOR
bool forceVisible = cullWhileDragging ? false : (Application.isPlaying && mCullTime > mUpdateTime);
#else
bool forceVisible = cullWhileDragging ? false : (mCullTime > mUpdateTime);
#endif
bool changed = false;
if (mForced != forceVisible)
{
mForced = forceVisible;
mResized = true;
}
bool clipped = hasCumulativeClipping;
// Update all widgets
for (int i = 0, imax = widgets.Count; i < imax; ++i)
{
UIWidget w = widgets[i];
// If the widget is visible, update it
if (w.panel == this && w.enabled)
{
#if UNITY_EDITOR
// When an object is dragged from Project view to Scene view, its Z is...
// odd, to say the least. Force it if possible.
if (!Application.isPlaying)
{
Transform t = w.cachedTransform;
if (t.hideFlags != HideFlags.HideInHierarchy)
{
t = (t.parent != null && t.parent.hideFlags == HideFlags.HideInHierarchy) ?
t.parent : null;
}
if (t != null)
{
for (; ; )
{
if (t.parent == null) break;
if (t.parent.hideFlags == HideFlags.HideInHierarchy) t = t.parent;
else break;
}
if (t != null)
{
Vector3 pos = t.localPosition;
pos.x = Mathf.Round(pos.x);
pos.y = Mathf.Round(pos.y);
pos.z = 0f;
if (Vector3.SqrMagnitude(t.localPosition - pos) > 0.0001f)
t.localPosition = pos;
}
}
}
#endif
int frame = Time.frameCount;
// First update the widget's transform
if (w.UpdateTransform(frame) || mResized)
{
// Only proceed to checking the widget's visibility if it actually moved
bool vis = forceVisible || (w.CalculateCumulativeAlpha(frame) > 0.001f);
w.UpdateVisibility(vis, forceVisible || ((clipped || w.hideIfOffScreen) ? IsVisible(w) : true));
}
// Update the widget's geometry if necessary
if (w.UpdateGeometry(frame))
{
changed = true;
if (!mRebuild)
{
if (w.drawCall != null)
{
w.drawCall.isDirty = true;
}
else
{
// Find an existing draw call, if possible
FindDrawCall(w);
}
}
}
}
}
// Inform the changed event listeners
if (changed && onGeometryUpdated != null) onGeometryUpdated();
mResized = false;
}
3.4 UIWidget的UpdateGeometry
3.4.1 流程
- 当widget信息改变的时候,调用UIGeometry.Clear清除缓存,并传入UIGeometry的缓存数据到OnFill函数填入顶点,UV和颜色数据,UIGeometry.ApplyTransform传入widget本地坐标到panel本地坐标的矩阵进行数据转化
- 如果只是widget位置变化,只需要调用UIGeometry.ApplyTransform更新其转化数据
3.4.2 代码
public bool UpdateGeometry (int frame)
{
// Has the alpha changed?
float finalAlpha = CalculateFinalAlpha(frame);
if (mIsVisibleByAlpha && mLastAlpha != finalAlpha) mChanged = true;
mLastAlpha = finalAlpha;
if (mChanged)
{
mChanged = false;
if (mIsVisibleByAlpha && finalAlpha > 0.001f && shader != null)
{
bool hadVertices = geometry.hasVertices;
if (fillGeometry)
{
geometry.Clear();
OnFill(geometry.verts, geometry.uvs, geometry.cols);
}
if (geometry.hasVertices)
{
// Want to see what's being filled? Uncomment this line.
//Debug.Log("Fill " + name + " (" + Time.frameCount + ")");
if (mMatrixFrame != frame)
{
mLocalToPanel = panel.worldToLocal * cachedTransform.localToWorldMatrix;
mMatrixFrame = frame;
}
geometry.ApplyTransform(mLocalToPanel);
mMoved = false;
return true;
}
return hadVertices;
}
else if (geometry.hasVertices)
{
if (fillGeometry) geometry.Clear();
mMoved = false;
return true;
}
}
else if (mMoved && geometry.hasVertices)
{
if (mMatrixFrame != frame)
{
mLocalToPanel = panel.worldToLocal * cachedTransform.localToWorldMatrix;
mMatrixFrame = frame;
}
geometry.ApplyTransform(mLocalToPanel);
mMoved = false;
return true;
}
mMoved = false;
return false;
}
3.5 UIPanel的FillAllDrawCalls
3.5.1 流程
- 清空Drawcall列表
- 遍历已经深度排序好的Widget列表,先创建一个Drawcall,将widget的顶点、UV、Color数据都调用widget的WriteToBuffers函数写入Drawcall的缓存,不断循环,将material,texture,shader都相同的widget数据写入同一个Drawcall里面,并更新Drawcall的初始深度、终点深度,直到遇到不满足条件的widget,则创建新的Drawcall。
- 最终调用Drawcall的UpdateGeometry函数更新其几何信息
3.5.2 代码
void FillAllDrawCalls ()
{
for (int i = 0; i < drawCalls.Count; ++i)
UIDrawCall.Destroy(drawCalls[i]);
drawCalls.Clear();
Material mat = null;
Texture tex = null;
Shader sdr = null;
UIDrawCall dc = null;
int count = 0;
if (mSortWidgets) SortWidgets();
for (int i = 0; i < widgets.Count; ++i)
{
UIWidget w = widgets[i];
if (w.isVisible && w.hasVertices)
{
Material mt = w.material;
Texture tx = w.mainTexture;
Shader sd = w.shader;
if (mat != mt || tex != tx || sdr != sd)
{
if (dc != null && dc.verts.size != 0)
{
drawCalls.Add(dc);
dc.UpdateGeometry(count);
dc.onRender = mOnRender;
mOnRender = null;
count = 0;
dc = null;
}
mat = mt;
tex = tx;
sdr = sd;
}
if (mat != null || sdr != null || tex != null)
{
if (dc == null)
{
dc = UIDrawCall.Create(this, mat, tex, sdr);
dc.depthStart = w.depth;
dc.depthEnd = dc.depthStart;
dc.panel = this;
}
else
{
int rd = w.depth;
if (rd < dc.depthStart) dc.depthStart = rd;
if (rd > dc.depthEnd) dc.depthEnd = rd;
}
w.drawCall = dc;
++count;
if (generateNormals) w.WriteToBuffers(dc.verts, dc.uvs, dc.cols, dc.norms, dc.tans);
else w.WriteToBuffers(dc.verts, dc.uvs, dc.cols, null, null);
if (w.mOnRender != null)
{
if (mOnRender == null) mOnRender = w.mOnRender;
else mOnRender += w.mOnRender;
}
}
}
else w.drawCall = null;
}
if (dc != null && dc.verts.size != 0)
{
drawCalls.Add(dc);
dc.UpdateGeometry(count);
dc.onRender = mOnRender;
mOnRender = null;
}
}
3.6 UIPanel的FillDrawCall
3.6.1 流程
- 遍历widget,寻找widget的drawcall为目标drawcall
- 调用该widget的WriteToBuffers函数将几何信息写入Drawcall的缓存
- 最终调用Drawcall的UpdateGeometry函数更新其几何信息
3.7.2 代码
bool FillDrawCall (UIDrawCall dc)
{
if (dc != null)
{
dc.isDirty = false;
int count = 0;
for (int i = 0; i < widgets.Count; )
{
UIWidget w = widgets[i];
if (w == null)
{
#if UNITY_EDITOR
Debug.LogError("This should never happen");
#endif
widgets.RemoveAt(i);
continue;
}
if (w.drawCall == dc)
{
if (w.isVisible && w.hasVertices)
{
++count;
if (generateNormals) w.WriteToBuffers(dc.verts, dc.uvs, dc.cols, dc.norms, dc.tans);
else w.WriteToBuffers(dc.verts, dc.uvs, dc.cols, null, null);
if (w.mOnRender != null)
{
if (mOnRender == null) mOnRender = w.mOnRender;
else mOnRender += w.mOnRender;
}
}
else w.drawCall = null;
}
++i;
}
if (dc.verts.size != 0)
{
dc.UpdateGeometry(count);
dc.onRender = mOnRender;
mOnRender = null;
return true;
}
}
return false;
}
3.7 UIDrawcall的UpdateGeometry
3.7.1 流程
- 创建Mesh
- 将drawcall缓存的顶点、UV、Color等数据赋值给Mesh
- 更新MeshRenderer使用的材质
- 清空drawcall缓存的顶点、UV、Color等数据
3.7.2 代码
public void UpdateGeometry (int widgetCount)
{
this.widgetCount = widgetCount;
int count = verts.size;
// Safety check to ensure we get valid values
if (count > 0 && (count == uvs.size && count == cols.size) && (count % 4) == 0)
{
// Cache all components
if (mFilter == null) mFilter = gameObject.GetComponent<MeshFilter>();
if (mFilter == null) mFilter = gameObject.AddComponent<MeshFilter>();
if (verts.size < 65000)
{
// Populate the index buffer
int indexCount = (count >> 1) * 3;
bool setIndices = (mIndices == null || mIndices.Length != indexCount);
// Create the mesh
if (mMesh == null)
{
mMesh = new Mesh();
mMesh.hideFlags = HideFlags.DontSave;
mMesh.name = (mMaterial != null) ? "[NGUI] " + mMaterial.name : "[NGUI] Mesh";
mMesh.MarkDynamic();
setIndices = true;
}
#if !UNITY_FLASH
// If the buffer length doesn't match, we need to trim all buffers
bool trim = (uvs.buffer.Length != verts.buffer.Length) ||
(cols.buffer.Length != verts.buffer.Length) ||
(norms.buffer != null && norms.buffer.Length != verts.buffer.Length) ||
(tans.buffer != null && tans.buffer.Length != verts.buffer.Length);
// Non-automatic render queues rely on Z position, so it's a good idea to trim everything
if (!trim && panel.renderQueue != UIPanel.RenderQueue.Automatic)
trim = (mMesh == null || mMesh.vertexCount != verts.buffer.Length);
// 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 && (verts.size << 1) < verts.buffer.Length) trim = true;
#endif
mTriangles = (verts.size >> 1);
if (trim || verts.buffer.Length > 65000)
{
if (trim || mMesh.vertexCount != verts.size)
{
mMesh.Clear();
setIndices = true;
}
mMesh.vertices = verts.ToArray();
mMesh.uv = uvs.ToArray();
mMesh.colors32 = cols.ToArray();
if (norms != null) mMesh.normals = norms.ToArray();
if (tans != null) mMesh.tangents = tans.ToArray();
}
else
{
if (mMesh.vertexCount != verts.buffer.Length)
{
mMesh.Clear();
setIndices = true;
}
mMesh.vertices = verts.buffer;
mMesh.uv = uvs.buffer;
mMesh.colors32 = cols.buffer;
if (norms != null) mMesh.normals = norms.buffer;
if (tans != null) mMesh.tangents = tans.buffer;
}
#else
mTriangles = (verts.size >> 1);
if (mMesh.vertexCount != verts.size)
{
mMesh.Clear();
setIndices = true;
}
mMesh.vertices = verts.ToArray();
mMesh.uv = uvs.ToArray();
mMesh.colors32 = cols.ToArray();
if (norms != null) mMesh.normals = norms.ToArray();
if (tans != null) mMesh.tangents = tans.ToArray();
#endif
if (setIndices)
{
mIndices = GenerateCachedIndexBuffer(count, indexCount);
mMesh.triangles = mIndices;
}
#if !UNITY_FLASH
if (trim || !alwaysOnScreen)
#endif
mMesh.RecalculateBounds();
mFilter.mesh = mMesh;
}
else
{
mTriangles = 0;
if (mFilter.mesh != null) mFilter.mesh.Clear();
Debug.LogError("Too many vertices on one panel: " + verts.size);
}
if (mRenderer == null) mRenderer = gameObject.GetComponent<MeshRenderer>();
if (mRenderer == null)
{
mRenderer = gameObject.AddComponent<MeshRenderer>();
#if UNITY_EDITOR
mRenderer.enabled = isActive;
#endif
}
UpdateMaterials();
}
else
{
if (mFilter.mesh != null) mFilter.mesh.Clear();
Debug.LogError("UIWidgets must fill the buffer with 4 vertices per quad. Found " + count);
}
verts.Clear();
uvs.Clear();
cols.Clear();
norms.Clear();
tans.Clear();
}
3.8 UIPanel的UpdateDrawCalls
3.8.1 作用
更新Panel的裁剪区域和Drawcall的位置,旋转角度,sortingorder,renderQueue等信息。
3.8.2 代码
void UpdateDrawCalls ()
{
Transform trans = cachedTransform;
bool isUI = usedForUI;
if (clipping != UIDrawCall.Clipping.None)
{
drawCallClipRange = finalClipRegion;
drawCallClipRange.z *= 0.5f;
drawCallClipRange.w *= 0.5f;
}
else drawCallClipRange = Vector4.zero;
int w = Screen.width;
int h = Screen.height;
// Legacy functionality
if (drawCallClipRange.z == 0f) drawCallClipRange.z = w * 0.5f;
if (drawCallClipRange.w == 0f) drawCallClipRange.w = h * 0.5f;
// DirectX 9 half-pixel offset
if (halfPixelOffset)
{
drawCallClipRange.x -= 0.5f;
drawCallClipRange.y += 0.5f;
}
Vector3 pos;
if (isUI)
{
Transform parent = cachedTransform.parent;
pos = cachedTransform.localPosition;
if (clipping != UIDrawCall.Clipping.None)
{
pos.x = Mathf.RoundToInt(pos.x);
pos.y = Mathf.RoundToInt(pos.y);
}
if (parent != null) pos = parent.TransformPoint(pos);
pos += drawCallOffset;
}
else pos = trans.position;
Quaternion rot = trans.rotation;
Vector3 scale = trans.lossyScale;
for (int i = 0; i < drawCalls.Count; ++i)
{
UIDrawCall dc = drawCalls[i];
Transform t = dc.cachedTransform;
t.position = pos;
t.rotation = rot;
t.localScale = scale;
dc.renderQueue = (renderQueue == RenderQueue.Explicit) ? startingRenderQueue : startingRenderQueue + i;
dc.alwaysOnScreen = alwaysOnScreen &&
(mClipping == UIDrawCall.Clipping.None || mClipping == UIDrawCall.Clipping.ConstrainButDontClip);
dc.sortingOrder = mSortingOrder;
dc.clipTexture = mClipTexture;
}
}
4. 解答问题
4.1 UIWidget的depth如何生效
- 我们知道在同一个Panel不同的UIWidget可以通过设置深度depth值,depth越大越显示在前面
- UIPanel下widget组件列表按照在widget的depth进行排序,在FillAllDrawCalls遍历widget组件列表将material,texture,shader相同的widget的顶点,UV,Color等几何数据填入同一个Drawcall缓存中。
- 上面讲到UIDrawcall是渲染UI元素的载体,UIPanel生成UIDrawcall,UIDrawcall是一个组件,挂载在一个GameObject,这个GameObject上再挂载MeshRender、Mesh、MeshFilter、材质等Unity组件,通过这些组件将UI元素渲染出来。
- 最终UI元素渲染顺序是通过MeshRender的sortingOrder和其材质的renderQueue共同决定的。
- 下方为RenderQueue,Sortingorder,Sorting Layer如何共同决定物体的渲染顺序
1.RenderQueue 2500以下
1. Sorting Layer/Order in Layer
1. 按照Sorting Layer/Order in Layer 设置的值,越小越优先
2. 无此属性,等同于 Sorting Layer=default ,Order in Layer=0 参与排序
2.RenderQueue 越小越优先
3.RenderQueue 相等,由近到远排序优先
2.RenderQueue 2500以上
1. Sorting Layer/Order in Layer
1. 按照Sorting Layer/Order in Layer 设置的值,越小越优先
2. 无此属性,等同于 Sorting Layer=default ,Order in Layer=0 参与排序
2.RenderQueue 越小越优先
3.RenderQueue 相等,由远到近排序优先
参考链接https://www.jianshu.com/p/0341f0ab9020
- UIPanel有sortingorder和RenderQueue类型,sortingorder用来设置对应UIPanel下每个UIDrawcall上MeshRender的sortingorder。所以同一个Panel下的UIdrawcall的MeshRender的sortingorder是一样的。而RenderQueue有3个类型,分别是
RenderQueue.Automatic,自动。起始值3000,Panel下第一个UIDrawcall起始为之前处理Panel中最大的的rendqueue ,每一个drawcall的RenderQueue 不断+1。
RenderQueue.StartAt 指定Panel下UIDrawcall的RenderQueue 初始值,每一个drawcall的RenderQueue 不断+1
RenderQueue.Explicit,指定特定值,Panel下的每个drawcall都是这个值 - 代码
4.2 UIPanel什么时候重新绘制
- 第一次LateUpdate
- 移除Widget的时候Widget的depth等于其Drawcall的起始或者结束深度
- 添加Widget的时候,Panel无法为其从现有的Drawcall列表找到复合条件的Drawcall(Widget的material、shader、texture与Drawcall相等且Widget的depth在Drawcall的深度范围内)。
4.3 UIWidget什么时候重新绘制
- Widget的最终alpha发生变化
- Widget的锚点发生变化
- 当调用Widget的MarkAsChanged标记其发生变化的时候
- Widget位置或者大小发生变化
4.4 项目如何减少Drawcall
- 由UIPanel生成Drawcall的函数FillAllDrawCalls可以知道,按照遍历已经按照深度排序的Widget列表,当碰到Widget的Shader、Texture或者Material不想等的时候,需要重新生成一个Drawcall。
- 了解到这机制,减少drawcall的思路就是尽可能将Shader、Texture、Material相同的Widget安排在同一个深度范围内,并且中间尽可能少的穿插与其不同的Widget。
- 公司项目减少Drawcall的方法
3.1 公司项目的界面一般对应一个UIPanel(没有ScrollView或者ListView),静态UI界面主要由图片或者文字,我们定义图片或者文字的层级区间类型,其中文字最高为1000,并且我们界面都是同一个字体,一个界面的所UILabel的深度值都为1000,这样一个界面的所有文字只会产生一个Drawcall。
3.2 另外使用图集的Widget(UISprite)和使用单一图片的Widget(UITexture)的深度值范围是区分开的,因为UISprite和UITexture一般来说使用的Shader、Texture、Material无法完全一样,通过划分深度范围能有效减少因两者穿插产生多余Drawcall。
3.3 图片或者文字的层级区间- PageMask, //页面遮罩 depth:0
- BgIcon, //背景Icon depth:100-200
- AtlasBg, //图集-背景 depth:200-300
- AtlasBasicComponent, //图集-基础控件 depth:300-400
- AtalsMainScene, //图集-主界面 depth:200-500
- DynIcon, //动态图标 depth:500-700
- UpperAtlas, //图标或者图集上的图集 depth:700-800
- UpperIcon, //图标之上的图标 depth:800-900
- Label //字体为1000
网友评论