我们都知道在Unity中渲染一个3D的物体, 需要提供3D物体的模型, 纹理, 材质甚至Shader. 但我们在制作UI时, 并不是必须提供这些. 那UI是另起炉灶, 有一套新机制吗? 答案是否定的. 下面我们就从UGUI源码层面来看看Image的渲染.

初遇Image组件

我们在Hierarchy窗口创建一个Image控件, 可以在Inspector窗口看到它由 RectTransform, Canvans Renderer, Image 几个Component组成. 我们可以给 Image 组件指定 Source Image, Color, Material 等属性.

我们可以看到 Image 组件的源码, 位于 Runtime/UI/Core/Image.cs, Image 渲染相关的类继承关系如下:

Mesh的生成

我们在一个测试Scene中加入一个Image, 并给 Image 类的 OnPopulateMesh 函数添加断点, 运行调试后, 可以看到如下调用堆栈:

从上图我们也可以看到, 如果 activeSprite 为空的情况下(一般可以理解为在Inspector窗口不设置Source Image), 则会直接调用 父类 GraphicOnPopulateMesh 函数, 我们来看下其实现.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
protected virtual void OnPopulateMesh(VertexHelper vh)
{
    var r = GetPixelAdjustedRect();
    var v = new Vector4(r.x, r.y, r.x + r.width, r.y + r.height);

    Color32 color32 = color;
    vh.Clear();
    vh.AddVert(new Vector3(v.x, v.y), color32, new Vector2(0f, 0f)); //左下
    vh.AddVert(new Vector3(v.x, v.w), color32, new Vector2(0f, 1f)); //左上
    vh.AddVert(new Vector3(v.z, v.w), color32, new Vector2(1f, 1f)); //右上
    vh.AddVert(new Vector3(v.z, v.y), color32, new Vector2(1f, 0f)); //右下

    vh.AddTriangle(0, 1, 2);
    vh.AddTriangle(2, 3, 0);
}

我们知道, UI元素一般是按四边形来渲染, 即需要两个三角形. 在上面代码中, 可以看到变量 v 存储了四边形的左下角和右上角的坐标. 然后以顺时针顺序把左下角, 左上角, 右上角, 右下角, 当作四个顶点坐标传给 VertexHelper, 这四点的顶点索引(Index), 依次为0, 1, 2, 3. 分别以(0, 1, 2)和(2, 3, 0) 组成两个三角形, 构成了UI元素的Mesh.

我们也看到通过给 VertexHelperAddVert 函数, 也设置了对应顶点的颜色及纹理坐标.

在Scene窗口的 Wireframe 模式下, 可以看到上述两个三角形组成的Mesh.

打开Frame Debug可以看到绘制参数, 和顶点信息, 4个顶点, 6个顶点索引.

Image有多种类型, 这些类型生成Mesh的方式不同, 但都是通过 OnPopulateMesh 函数来提供的.

UI元素一定是按四边形来渲染吗? 答案是否定的. 当 Image 组件的 Image TypeSimple 并且勾选了 Use Sprite Mesh 时, 当前使用的Sprite包含透明的区域, 此时其 Sprite Mesh 可能不是四边形, 就不以四边形来渲染.

Image 的部分参数变化, 可能也会引起Mesh的变化.

材质的选择

我们从 Inspector 窗口可以看到 Image 组件可以设置 Material 属性. 实际上该属性由 Graphic 类提供. 材质相关的方法和属性及类继承关系如下图所示:

Imagematerial 属性的 getter 方法添加断点后, 调试运行, 可以看到如下调用堆栈:

我们也从 material 的 getter 方法中看到, 在Inspector窗口设置的材质有最高优先级, 其次是如果给 sprite 设置了 associatedAlphaSplitTexture, 则会使用 defaultETC1GraphicMaterial, 最后才会使用 defaultMaterial.

追踪 defaultMaterial 的定义, 会发现它实际上是由 Canvas.GetDefaultCanvasMaterial() 提供, 但该方法实现在 Unity 的C++代码中, 并不开源. 根据网上资料所说, 该方法会优先调用 SPR 中提供的UI材质, 再调用内置的UI默认Shader生成材质.

我们也注意到, 在 Graphic.materialForRendering 属性中, 可以对材质进行修改.

这样我们就获得了渲染UI时用到的材质.

Shader在哪里?

材质使用的Shader在哪里呢?

我们可以在Unity网站下载到对应版本的内置Shader以作为参考. UI部分Shader位于 ./DefaultResourcesExtra/UI 目录.

纹理的提供

可以在 Inspector 窗口给 Image 组件设置 Source Image, 为 Sprite 类型. Sprite 类位于 UnityEngine 命名空间下, 并非 UGUI 专用.

我们给 ImagemainTexture 属性的 getter 方法增加断点, 调试后可以看到如下堆栈:

我们看到首先如果有 activeSprite , 则会使用 activeSprite.texture. 再接着会尝试使用 material 中的纹理, 都不存在的情况下, 会使用默认的白色纹理.

activeSprite 会优先返回 m_OverrideSprite, 其次返回 sprite (即 m_Sprite, Inspector中设置的 Sprite).

纹理相关的方法和属性及类继承关系如下图所示:

我们上面分析Mesh的生成时, 提到 VertexHelper 需要纹理坐标. 我们可以再回头看下, Image 不同类型的 Mesh 是如何匹配其纹理坐标的.

例如, 我们在 GenerateSimpleSprite 可以看到:

1
var uv = (activeSprite != null) ? Sprites.DataUtility.GetOuterUV(activeSprite) : Vector4.zero;

渲染一下

我们从上面调试时的堆栈可以看到, 这些调用都是由 Graphic.Rebuild 触发, 代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
public virtual void Rebuild(CanvasUpdate update)
{
    if (canvasRenderer == null || canvasRenderer.cull)
        return;

    switch (update)
    {
        case CanvasUpdate.PreRender:
            if (m_VertsDirty)
            {
                UpdateGeometry();
                m_VertsDirty = false;
            }
            if (m_MaterialDirty)
            {
                UpdateMaterial();
                m_MaterialDirty = false;
            }
            break;
    }
}

我们查看 UpdateGeometryUpdateMaterial 的实现, 就可以看到生成的Mesh以及被选择的纹理, 材质等, 分别设置给了 CanvasRenderer, 最终由 CanvasRenderer 来负责 Image (也包括所有 Graphic 的子类) 的渲染.