Unity 补完计划(四):UGUI-10:Image & RawImage
前言
将 MaskableGraphic
分析完之后,就可以来看 UGUI 中真正使用到的图像组件了。Image
和 RawImage
是 UGUI 中最常用的两个图像组件,它们分别用于显示 Sprite
和 Texture
。本文将分析这两个组件的实现。
Sprite 与 Texture
在 UGUI 中,Sprite
和 Texture
是两个最常用的图像资源。Sprite
是 Unity 引擎中专门用于 2D 图像的资源,它包含了图像的像素数据、UV 坐标、边界信息等。Texture
是 Unity 引擎中用于存储图像像素数据的资源,它可以被 Sprite
使用,也可以被 RawImage
使用。
Unity 的官方手册中,这样描述 Sprite
:
-
Sprite
是一种 2D 图形对象,用于 2D 游戏中的角色、道具、飞弹和其他游戏元素。图形是从位图图像Texture2D
获取的。Sprite
类主要标识应该用于特定精灵的图像部分。然后,GameObject
上的SpriteRenderer
组件可以使用该信息来实际显示图形。 -
Sprite
是 2D 图形对象。如果习惯于在 3D 空间中工作,Sprite
本质上只是标准纹理,但可通过一些特殊技巧在开发过程中组合和管理精灵纹理以提高效率和方便性。
由 Sprite
的描述可以看出,Sprite
依然是一个 2D 图形对象,它只是对 Texture2D
的一个封装。Sprite
除了包含 Texture2D
的像素数据外,还包含了一些额外的信息,比如 UV 坐标、边界信息等。
1 | // Sprite.cs |
从 Sprite
的定义中也可以看出这一点,Sprite
中不仅包含了 Texture2D
的像素数据,还包含了 bounds
、rect
、border
等信息。
RawImage
RawImage
的实现十分简单,仅仅是重写了 Graphic
的 mainTexture
属性以及 OnPopulateMesh()
方法,用于设置显示的 Texture
。此外,还提供了一个 UV Rect
属性,用于设置显示的 Texture
的 UV 坐标。
通过前面对
Graphic
的分析我们知道,mainTexture
是真正提交给CanvasRenderer
的纹理,而OnPopulateMesh()
方法则会在UpdateGeometry()
方法中被调用,用于生成顶点信息。
1 | public class RawImage : MaskableGraphic |
在 OnPopulateMesh()
方法中,会将 Texture
的大小信息以及 UV Rect
信息传递给 VertexHelper
,用于生成顶点数据。
此外,在 RawImage
中实现了一个 Graphic
中定义的虚函数 SetNativeSize()
,用于将 RawImage
的大小设置为 Texture
的原始大小。
1 | /// <summary> |
Image
相比于 RawImage
,Image
就要复杂许多。二者的区别主要在于:
-
Image
使用Sprite
作为显示的图像资源,而RawImage
使用Texture
作为显示的图像资源。 -
Image
提供了四种不同的显示模式:Simple
、Sliced
、Tiled
和Filled
。相关的顶点生成方法非常复杂,使得
Image.cs
足足达到了 1800 行。
Image 的显示模式
Image
提供了四种不同的显示模式,分别是 Simple
、Sliced
、Tiled
和 Filled
。这四种显示模式是通过 Type
枚举来定义的。
1 | public enum Type |

-
Simple
模式:直接显示Sprite
,会随着RectTransform
的大小进行缩放。此时
Sprite
会随着RectTransform
改变长宽比例,可能会造成图像的拉伸或压缩。 -
Sliced
模式:九宫格模式,RectTransform
改变时,会根据Sprite
的border
属性进行缩放。这种模式下当
RectTransform
改变时,将只会缩放九宫格的中间部分,而四个角的部分不会缩放。非常适合用于对话框等 UI 元素。 -
Tiled
模式:平铺模式,RectTransform
改变时,会根据Sprite
的border
属性进行平铺。这种模式下,
Sprite
会根据border
属性进行平铺,四个角的部分不会被拉伸。适合用于地砖以及墙壁等需要平铺的 UI 元素。 -
Filled
模式:填充模式,根据fillMethod
以及fillAmount
属性对Sprite
进行填充。非常适用于血条、进度条等场景。对比填充度满的情况可以发现,二者的顶点数据是不同的。也就是说,填充的功能是通过设置不同的顶点三角形来实现的。
mainTexture 的重写
Image
同样重写了 Graphic
的 mainTexture
属性,用于设置显示的 Sprite
。
1 | public override Texture mainTexture |
当 activeSprite
不为空时,则由 activeSprite.texture
提供渲染的纹理。
1 | [private Sprite m_Sprite; ] |
由 m_Sprite
和 m_OverrideSprite
共同决定了 activeSprite
的值。此外,在设置 Sprite
时,也会设置脏标记以触发重新渲染。
OnPopulateMesh() 的重写
Image
重写了 OnPopulateMesh()
方法,用于支持不同显示模式下的顶点生成。
1 | protected override void OnPopulateMesh(VertexHelper toFill) |
在 OnPopulateMesh()
方法中,根据 type
的不同,调用不同的生成顶点数据的方法。这里就不再详细展开了,有兴趣的读者可以自行查看源码。
通过 useSpriteMesh
属性可以控制是否使用 Sprite
的顶点数据,二者顶点的差别在 Graphic 中已经分析过,默认的顶点就是一个简单的两个三角形组成的矩形,而 Sprite
的顶点则会根据透明度进行裁剪。
UpdateMaterial() 的重写
Image
也重写了 UpdateMaterial()
方法,不过并没有处理太多额外的逻辑。只是针对 RGBA
和 RGB
+ A
的两种情况,设置了 CanvasRenderer
的 alphaTexture
属性。
1 | protected override void UpdateMaterial() |
ILayoutElement 接口的实现
仔细观察 Image
与 RawImage
的定义我们可以发现,Image
实现了 ILayoutElement
接口,而 RawImage
没有实现。这意味着 Image
可以参与到布局系统的计算中,而 RawImage
则不行。
忘记了这部分内容的读者可以查看 LayoutRebuilder
1 | public virtual void CalculateLayoutInputHorizontal() {} |
这两个函数用于计算布局信息,实际上只有 LayoutGroup
这种会随着子物体而改变 PreferredSize
等属性的组件才需要实现这两个函数,大部分的 ILayoutElement
实现都是空函数。因此 Image
也不例外。
1 | public virtual float minWidth { get { return 0; } } |
接下来的这些属性就是我们熟悉的 minSize
、preferredSize
、flexibleSize
以及 layoutPriority
了。这些属性用于布局系统的计算,Image
中,会根据 activeSprite.pixelsPerUnit
以及 activeSprite.rect.size
来计算出 minSize
和 preferredSize
。
因此,如果 Image
和 RawImage
同时作为子物体,当父物体带有某种 LayoutGroup
组件时并且开启了 Control Child Size
选项,那么 Image
会根据 Sprite
的大小得到正确的布局信息,而 RawImage
则会直接坍缩为 0。
Image 和 RawImage 的区别
关于这两个组件之间的区别,上文已经提到了一些。这里再总结一下:
-
Image
使用Sprite
作为显示的图像资源,而RawImage
使用Texture
作为显示的图像资源。 -
Image
提供了四种不同的显示模式:Simple
、Sliced
、Tiled
和Filled
,而RawImage
只能直接显示Texture
,相当于Image
的Simple
模式。 -
渲染效率方面,使用
Image
相比RawImage
在一些情况下可以减少DrawCall
的数量。 由于RawImage
使用的是单独的Texture
,因此每一个RawImage
在显示时都会触发一次DrawCall
。而Image
使用的是Sprite
,可以使用SpriteAtlas
来进行批量渲染,如果多个Image
组件所使用的Sprite
在同一个SpriteAtlas
中,那么只会触发一次DrawCall
。但带来的问题就是整个
SpriteAtlas
都会被加载到内存中,如果SpriteAtlas
中的Sprite
过多,可能会导致内存占用过高。 -
Image
实现了ILayoutElement
接口,可以参与到布局系统的计算中,而RawImage
则不行。如果需要在布局系统中使用
RawImage
,可以通过在RawImage
上添加一个LayoutElement
组件来实现。
合批的 🌰
我们新建一个场景,运行游戏并点击 Game 窗口的 Stats,可以看到当前的 DrawCall 数量。
在没有任何 UI 元素时,基本的 DrawCall 数量为 8,可能是绘制背景、Gizmos 等。
我们导入三张独立的图片,并将 Texture Type
设置为 2D
,然后新建三个 RawImage
组件,分别显示这些图片。
运行游戏,可以看到 DrawCall 数量变为 11。这说明三张 RawImage
组件分别触发了三次 DrawCall
。
接着,我们导入另外三张独立的图片,并将 Texture Type
设置为 Sprite(2D and UI)
,然后新建三个 Image
组件,分别显示这三张图片。
运行游戏,我们发现 DrawCall 数量仍然为 11。说好的减少 DrawCall 呢?
别急,让我们继续分析。
我们新建一个 SpriteAtlas
,将这三张图片打包到 SpriteAtlas
中,然后将 Image
组件的 Sprite
设置为 SpriteAtlas
中的 Sprite
。
然后运行游戏,可以看到 DrawCall 数量变为 9。这说明三个 Image
组件只触发了一次 DrawCall
。
然而,即使将 RawImage
的 Texture
设置为 SpriteAtlas
中的 Sprite
,DrawCall 数量仍然为 11。这说明 RawImage
无法使用 SpriteAtlas
进行批量渲染。

此外,在 UGUI 的制作中,有许多 Sprite
实际上是使用 Sprite Editor
从一张大图中切割出来的。这种情况下,使用 Image
能够减少 DrawCall
吗?
我们如此设置,再运行游戏。就会发现,DrawCall
的数量确实减少到了 9。这说明使用 Sprite Editor
切割出来的 Sprite
依然会进行批量渲染,减少 DrawCall
的数量。
总结:在使用独立的图片时,Image
和 RawImage
的 DrawCall
数量是一样的。但是当使用 SpriteAtlas
或者 Sprite Editor
切割出来的 Sprite
时,Image
会进行合批,减少 DrawCall
的数量。
总结
本文我们简单分析了 Image
和 RawImage
的实现。Image
和 RawImage
是 UGUI 中最常用的两个图像组件,它们分别用于显示 Sprite
和 Texture
。Image
提供了四种不同的显示模式,可以根据需求选择。RawImage
则是一个简单的图像组件,只能直接显示 Texture
。
在使用 Image
时,可以使用 SpriteAtlas
来进行批量渲染,减少 DrawCall
的数量。而 RawImage
无法使用 SpriteAtlas
进行批量渲染,每一个 RawImage
都会触发一次 DrawCall
。
此外,Image
实现了 ILayoutElement
接口,可以参与到布局系统的计算中,而 RawImage
则不行。如果需要在布局系统中使用 RawImage
,可以通过在 RawImage
上添加一个 LayoutElement
组件来实现。