news 2026/5/14 23:09:22

深入Unity UGUI源码:手写ExtendImage组件,彻底搞懂Image的Filled与Sliced渲染原理

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
深入Unity UGUI源码:手写ExtendImage组件,彻底搞懂Image的Filled与Sliced渲染原理

深入Unity UGUI源码:手写ExtendImage组件,彻底搞懂Image的Filled与Sliced渲染原理

在Unity的UI开发中,Image组件是最基础也是最常用的组件之一。无论是简单的图标显示,还是复杂的进度条动画,Image组件都扮演着至关重要的角色。然而,当我们需要实现一些特殊效果时,比如支持九宫格裁剪的进度条,原生的Image组件就显得力不从心了。本文将带你深入UGUI源码,通过手写ExtendImage组件,彻底理解Image组件中Filled与Sliced模式的渲染原理。

1. UGUI Image组件基础原理

UGUI的Image组件继承自MaskableGraphic类,主要负责2D精灵的渲染。其核心渲染逻辑是通过OnPopulateMesh方法实现的,这个方法负责生成网格数据(顶点、UV、颜色等)并填充到VertexHelper中。

Image组件支持四种渲染类型:

  • Simple:最简单的拉伸渲染
  • Sliced:九宫格渲染
  • Tiled:平铺渲染
  • Filled:填充渲染(常用于进度条)
// Image组件的类型定义 public enum Type { Simple, Sliced, Tiled, Filled }

在深入研究Filled和Sliced模式之前,我们需要先了解几个关键概念:

  1. Sprite的Border属性:定义了九宫格的边界值
  2. UV坐标:决定了纹理如何在网格上映射
  3. VertexHelper:用于构建网格数据的辅助类

2. Filled模式的实现原理

Filled模式是制作进度条最常用的方式,它通过fillAmount参数控制显示比例。让我们深入源码看看它是如何工作的。

2.1 Filled模式的核心算法

Filled模式的渲染逻辑主要在GenerateFilledSprite方法中实现。根据不同的填充方法(水平、垂直、径向等),计算顶点和UV的方式也有所不同。

以水平填充为例,关键计算步骤如下:

  1. 计算填充比例对应的顶点位置
  2. 根据填充原点(Left或Right)调整顶点位置
  3. 计算对应的UV坐标
  4. 生成三角形索引
// 水平填充的核心代码片段 if (fillMethod == FillMethod.Horizontal) { float fill = fillAmount; if (fillOrigin == 1) // Right { xMin = Mathf.Lerp(xMax, xMin, fill); uvMin.x = Mathf.Lerp(uvMax.x, uvMin.x, fill); } else // Left { xMax = Mathf.Lerp(xMin, xMax, fill); uvMax.x = Mathf.Lerp(uvMin.x, uvMax.x, fill); } }

2.2 Filled模式的局限性

虽然Filled模式非常适合制作进度条效果,但它有一个明显的缺点:不支持九宫格。这意味着:

  1. 进度条在不同长度时,边缘会出现拉伸变形
  2. 需要为不同长度的进度条准备不同的图片资源
  3. 无法利用九宫格的特性来优化资源

3. Sliced模式的实现原理

Sliced模式(九宫格)是UI优化的重要手段,它通过将图片分为9个区域,只拉伸中间部分来保持边缘不变形。

3.1 九宫格的数据结构

九宫格的信息存储在Sprite的border属性中,它是一个Vector4,分别表示左、下、右、上的边界值:

[left, bottom, right, top]

在代码中,我们通过Sprite.border属性获取这些值:

Vector4 border = activeSprite.border;

3.2 Sliced模式的渲染流程

Sliced模式的渲染主要在GenerateSlicedSprite方法中实现,大致流程如下:

  1. 获取Sprite的border值
  2. 计算调整后的边界(考虑像素密度)
  3. 将图片分为9个区域
  4. 为每个区域生成顶点和UV数据
// 九宫格顶点计算的核心代码 s_VertScratch[0] = new Vector2(padding.x, padding.y); s_VertScratch[1] = new Vector2(border.x, border.y); s_VertScratch[2] = new Vector2(rect.width - border.z, rect.height - border.w); s_VertScratch[3] = new Vector2(rect.width - padding.z, rect.height - padding.w);

3.3 Sliced模式的局限性

虽然Sliced模式能很好地保持边缘不变形,但它不适合用于进度条效果,因为:

  1. 无法实现真正的裁剪效果
  2. 进度变化时,部分区域会保持显示
  3. 无法精确控制显示比例

4. 实现ExtendImage组件

结合Filled和Sliced模式的优缺点,我们需要创建一个新的ExtendImage组件,让Filled模式支持九宫格。

4.1 组件设计思路

我们的ExtendImage组件需要:

  1. 继承自UnityEngine.UI.Image
  2. 添加一个SlicedClipMode选项
  3. 重写OnPopulateMesh方法
  4. 实现自定义的GenerateSlicedSprite方法
[AddComponentMenu("UI/ExtendImage")] public class ExtendImage : Image { [SerializeField] private bool m_SlicedClipMode = false; protected override void OnPopulateMesh(VertexHelper vh) { if (type == Type.Filled && m_SlicedClipMode && hasBorder) { GenerateSlicedSprite(vh); } else { base.OnPopulateMesh(vh); } } }

4.2 核心算法实现

关键点在于如何结合Filled的裁剪和Sliced的九宫格特性。我们需要:

  1. 计算九宫格各部分的比例
  2. 根据fillAmount确定显示哪些部分
  3. 对最后一个显示的部分进行裁剪
private void GenerateSlicedSprite(VertexHelper toFill) { // 获取九宫格信息 Vector4 border = activeSprite.border / pixelsPerUnit; Vector4 adjustedBorders = GetAdjustedBorders(border, rect); // 计算各部分比例 float xLength = s_VertScratch[3].x - s_VertScratch[0].x; float len1XRatio = (s_VertScratch[1].x - s_VertScratch[0].x) / xLength; float len2XRatio = (s_VertScratch[2].x - s_VertScratch[1].x) / xLength; float len3XRatio = (s_VertScratch[3].x - s_VertScratch[2].x) / xLength; // 根据fillAmount裁剪 if (fillAmount >= (len1XRatio + len2XRatio)) { float ratio = (fillAmount - (len1XRatio + len2XRatio)) / len3XRatio; s_VertScratch[3].x = s_VertScratch[2].x + (s_VertScratch[3].x - s_VertScratch[2].x) * ratio; s_UVScratch[3].x = s_UVScratch[2].x + (s_UVScratch[3].x - s_UVScratch[2].x) * ratio; } else if (fillAmount >= len1XRatio) { float ratio = (fillAmount - len1XRatio) / len2XRatio; s_VertScratch[2].x = s_VertScratch[1].x + (s_VertScratch[2].x - s_VertScratch[1].x) * ratio; } else { float ratio = fillAmount / len1XRatio; s_VertScratch[1].x = s_VertScratch[0].x + (s_VertScratch[1].x - s_VertScratch[0].x) * ratio; } // 生成网格 toFill.Clear(); AddQuad(toFill, ...); }

4.3 编辑器扩展

为了让组件更易用,我们还需要创建一个自定义编辑器:

[CustomEditor(typeof(ExtendImage), true)] public class ExtendImageEditor : ImageEditor { private SerializedProperty m_SlicedClipMode; protected override void OnEnable() { base.OnEnable(); m_SlicedClipMode = serializedObject.FindProperty("m_SlicedClipMode"); } public override void OnInspectorGUI() { base.OnInspectorGUI(); if ((Image.Type)m_Type.enumValueIndex == Image.Type.Filled) { EditorGUILayout.PropertyField(m_SlicedClipMode); } } }

5. 性能优化与注意事项

在实现自定义UI组件时,性能是需要重点考虑的因素。以下是几个优化建议:

  1. 顶点计算优化

    • 尽量减少不必要的计算
    • 复用中间计算结果
    • 使用局部变量而非频繁访问属性
  2. 网格生成优化

    • 只生成必要的三角形
    • 避免频繁调用VertexHelper.Clear()
    • 使用对象池管理VertexHelper
  3. 内存管理

    • 避免在每帧创建新的数组
    • 使用静态数组减少GC压力
    • 合理使用缓存
// 使用静态数组减少GC private static readonly Vector2[] s_VertScratch = new Vector2[4]; private static readonly Vector2[] s_UVScratch = new Vector2[4];

在实际项目中,我发现最常遇到的问题是不正确的边界计算。特别是在处理不同分辨率和像素密度时,需要特别注意:

提示:在计算九宫格边界时,一定要考虑pixelsPerUnit的影响,否则在不同分辨率的设备上可能会出现显示异常。

另一个常见问题是填充方向的处理。我们的ExtendImage组件目前只实现了水平和垂直方向的裁剪,如果需要支持径向填充,还需要扩展算法:

// 径向填充的伪代码 if (fillMethod == FillMethod.Radial90) { // 计算角度 float angle = Mathf.Lerp(0, 90, fillAmount); // 根据角度裁剪顶点 // ... }

6. 扩展应用与进阶技巧

掌握了Image组件的核心原理后,我们可以进一步扩展其功能:

  1. 渐变填充:在顶点颜色中添加渐变效果
  2. 扭曲效果:通过修改顶点位置实现扭曲动画
  3. 自定义遮罩:结合Shader实现特殊遮罩效果

以下是一个简单的渐变填充实现思路:

// 渐变填充的顶点颜色设置 Color32 color0 = new Color32(255, 0, 0, 255); // 起始颜色 Color32 color1 = new Color32(0, 255, 0, 255); // 结束颜色 vertexHelper.AddVert(new Vector3(posMin.x, posMin.y, 0), color0, uvMin); vertexHelper.AddVert(new Vector3(posMin.x, posMax.y, 0), color0, new Vector2(uvMin.x, uvMax.y)); vertexHelper.AddVert(new Vector3(posMax.x, posMax.y, 0), color1, uvMax); vertexHelper.AddVert(new Vector3(posMax.x, posMin.y, 0), color1, new Vector2(uvMax.x, uvMin.y));

对于更复杂的效果,可以结合Shader来实现。例如,创建一个支持UV动画的Image组件:

public class AnimatedImage : Image { public Vector2 uvAnimationSpeed = Vector2.zero; protected override void OnPopulateMesh(VertexHelper vh) { base.OnPopulateMesh(vh); UIVertex vertex = new UIVertex(); for (int i = 0; i < vh.currentVertCount; i++) { vh.PopulateUIVertex(ref vertex, i); vertex.uv0 += uvAnimationSpeed * Time.unscaledTime; vh.SetUIVertex(vertex, i); } } }

在实际项目中,我发现理解UGUI的底层原理不仅能帮助我们解决具体问题,还能激发更多创意。比如,通过分析Image组件的实现,我们可以更好地理解:

  • Unity的UI渲染管线
  • 网格生成与优化的技巧
  • 性能瓶颈的分析方法
  • 自定义组件的设计模式
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/14 23:09:22

Vue2项目集成DHTMLX Gantt:从基础配置到企业级功能定制

1. 为什么选择DHTMLX Gantt与Vue2集成 在项目管理系统的开发中&#xff0c;甘特图是最核心的视图之一。我调研过市面上几乎所有主流甘特图方案&#xff0c;最终选择DHTMLX Gantt主要基于三个实际考量&#xff1a; 首先&#xff0c;它的渲染性能确实出色。在测试中&#xff0c;加…

作者头像 李华
网站建设 2026/5/14 23:08:03

AI赚钱别再只问“哪个模型最强”:向量引擎、GPT Image 2、deepseek v4、api 和 key,正在把 Agent 变成真正能干活的系统

AI赚钱别再只问“哪个模型最强”&#xff1a;向量引擎、GPT Image 2、deepseek v4、api 和 key&#xff0c;正在把 Agent 变成真正能干活的系统很多人这两年用 AI 的状态&#xff0c;像极了第一次进自助餐厅。 一开始&#xff0c;什么都想拿。 AI 写文章&#xff0c;拿。 AI 画…

作者头像 李华
网站建设 2026/5/14 23:07:58

基于YOLO与PyTorch的零售货架智能分析系统:从原理到部署实战

1. 项目概述&#xff1a;当AI视觉遇上零售货架如果你在零售行业待过&#xff0c;或者自己开过便利店&#xff0c;肯定对“盘货”这件事深恶痛绝。店员拿着纸笔&#xff0c;对着货架一个个数&#xff0c;效率低下不说&#xff0c;还容易出错。供应商的业务代表跑店&#xff0c;也…

作者头像 李华
网站建设 2026/5/14 23:07:53

RK3568核心板开发全解析:从硬件选型到AIoT应用实战

1. 项目概述&#xff1a;为什么选择RK3568核心板&#xff1f; 在嵌入式开发领域&#xff0c;选型往往是项目成败的第一步。面对市面上琳琅满目的处理器平台&#xff0c;从传统的ARM Cortex-A系列到各种专用SoC&#xff0c;如何找到一个性能、功耗、成本、生态和供应链都均衡的选…

作者头像 李华
网站建设 2026/5/14 23:05:29

英文论文怎么降AI?实测从88%降至20%的5大方法(附工具实测)

最近turnitin系统大升级&#xff0c;判定规则变得更加严格。很多不知道怎么给英文降ai的小伙伴对此都感到非常焦虑&#xff0c;检测报告里大面积的标蓝会导致稿件不合格被退回&#xff0c;手动降ai又要一直盯着屏幕改来改去&#xff0c;费时费力。 作为已经在这个领域摸爬滚打两…

作者头像 李华