unity UGUI源码分析(4)Text与TextMeshPro

news2024/9/20 8:49:33

这一篇博客用于分析Text的内容的更新机制,并分析text mesh pro。

首先我们分析Text的文字是如何渲染出来的。

 

PupulateWithErrors方法会根据字符串生成顶点数据。其实Text会根据所给定的字符串生成相关的图集,然后对图集进行采样就可以渲染出文字了。由于TextGenerator没有开源,我们从unity UI优化文档上可以找到相关步骤。

protected override void OnPopulateMesh(VertexHelper toFill)
{
    if (font == null)
        return;

    // We don't care if we the font Texture changes while we are doing our Update.
    // The end result of cachedTextGenerator will be valid for this instance.
    // Otherwise we can get issues like Case 619238.
    m_DisableFontTextureRebuiltCallback = true;

    Vector2 extents = rectTransform.rect.size;

    var settings = GetGenerationSettings(extents);
    cachedTextGenerator.PopulateWithErrors(text, settings, gameObject);

    // Apply the offset to the vertices
    IList<UIVertex> verts = cachedTextGenerator.verts;
    float unitsPerPixel = 1 / pixelsPerUnit;
    int vertCount = verts.Count;

    // We have no verts to process just return (case 1037923)
    if (vertCount <= 0)
    {
        toFill.Clear();
        return;
    }

    Vector2 roundingOffset = new Vector2(verts[0].position.x, verts[0].position.y) * unitsPerPixel;
    roundingOffset = PixelAdjustPoint(roundingOffset) - roundingOffset;
    toFill.Clear();
    if (roundingOffset != Vector2.zero)
    {
        for (int i = 0; i < vertCount; ++i)
        {
            int tempVertsIndex = i & 3;
            m_TempVerts[tempVertsIndex] = verts[i];
            m_TempVerts[tempVertsIndex].position *= unitsPerPixel;
            m_TempVerts[tempVertsIndex].position.x += roundingOffset.x;
            m_TempVerts[tempVertsIndex].position.y += roundingOffset.y;
            if (tempVertsIndex == 3)
                toFill.AddUIVertexQuad(m_TempVerts);
        }
    }
    else
    {
        for (int i = 0; i < vertCount; ++i)
        {
            int tempVertsIndex = i & 3;
            m_TempVerts[tempVertsIndex] = verts[i];
            m_TempVerts[tempVertsIndex].position *= unitsPerPixel;
            if (tempVertsIndex == 3)
                toFill.AddUIVertexQuad(m_TempVerts);
        }
    }

    m_DisableFontTextureRebuiltCal

Unity内置的Text组件可以很方便地用于在UI中显示栅格化的文本字形。但是,在使用Text时有很多大家不了解却又经常遇到的与性能相关的因素。当想UI添加文本时,要始终记得——文本字形是作为独立的面片(quad)进行渲染的,每个字符都是一个面片。这些面片通常都含有大量的空白区域围绕着字形,空白区域的大小取决于字形的形状,在放置文本时很容易就会无意中破坏其他UI元素的批处理。

UI文本的网格重建是个重点问题。当Text组件发生变化时,必须重新计算用于显示实际文本的多边形。当Text组件或它的任意级别的父节点被禁用或启用时,也需要进行重新计算。

在含有大量文字标签的UI上,这一行为可能导致问题,例如排行榜页面和统计数据页面。因为在Unity中,最常见的显示和隐藏UI的方法是启用/禁用含有UI的GameObject,含有大量文本组件的UI通常在显示时会导致帧率降低。

那么Text时如何生成图集的?

当全部可现实字符集很大或者在运行时期不确定时,可以用动态字体来显示文本。在Unity的实现中,这些字体在运行时根据Text组件中出现的字符构建一个字形图集(glyph atlas)。

被加载的每个不同的Font对象会维护它自己的纹理集,即使它与其他字体属于同一个字体族。例如,在一个文本控件中使用Arial字体,并且将字体样式(Font Style)设置为粗体(Bold),在另一个文本控件中使用Arial Bold字体,这两个控件会产生一样的输出,但是Unity会维护两个不同的纹理集——一个给Arial,另一个给Arial Bold。

从性能角度看,要理解的最重要的一件事就是,动态字体为每种不同的结合(尺寸、样式&字符)在其纹理集中维护了一个字形。也就是说,如果一个UI中含有两个Text组件,都显示了字符“A”,那么:

  • 如果两个Text组件尺寸相同,那么字体图集中会有一个字形。
  • 如果两个Text组件尺寸不同,那么字体图集中会有两个不同尺寸的字母“A”。
  • 如果一个Text组件的样式是粗体而另一个不是,那么字体图集中会含有一个粗体的“A”和一个普通的“A”。

当使用动态字体的Text对象遇到了没有被栅格化到字体纹理集中的字形时,必须重建字体纹理集。如果新的字形能够加入当前图集,那么将其加入图集并重新上传到图形设备。但是,如果当前的图集太小,那么系统会尝试重建图集。这通过两步完成。

第一步,以相同的大小重建图集,只使用当前在活动的Text组件上显示的字形。这包含了父画布活动(active)但是禁用了CanvasRenderer的Text组件。如果系统成功地将当前使用的所有字形填充进新的图集中,将会栅格化此图集,不再继续进行第二步。

第二步,如果当前使用的字形不能填充进同样大小的图集中,那么会以当前图集大小的短维乘2来创建一个更大的图集。例如,一个512x512的图集或被扩充到512x1024的图集。

因为上述的算法,动态字体集只会在创建时增长一次大小。考虑到重建纹理集的开销,必须时期在重建时最小。这可以通过两种方式实现:

如果可以,使用非动态字体并预先配置对想要使用的字形集的支持。在使用具有良好的字符集约束的UI上,这样做通常效果很好,例如,只是用Latin/ASCII字符并且尺寸范围小的UI。

如果必须支持极其大量的字符,例如整个Unicode集合,那么字体必须设为动态。为了避免可预见的性能问题,使用Font.RequestCharactersInTexture在启动时填充字体字形集。

注意,每个发生变化的Text组件会单独触发字体集重建。当布置极大量Text组件时,将组件内容中全部的不重复字符收集起来并填充进字体集可能有利于提高性能。这样做能够确保字形集只需要重建一次,而不是每次出现新字形时都重建。

另一点需要注意的是,当触发字体集重建时,所有不在当前活动的Text组件中的字符都不会包含进新的图集中

TextMeshPro Text

TextMeshPro(TMP)可以作为Unity中已有的文本组件(例如TextMesh和UI Text)的替代方案。TMP使用Signed Distance Field(有向距离场SDF)作为其首选文本渲染管线,使其可以在任意尺寸和分辨率下清晰的渲染文本。使用一系列自定义的着色器来提升SDF文本渲染的能力后,TMP可以简单的通过修改材质属性来动态地改变视觉效果,例如,放大、外边框、软阴影等,并且可以通过创建材质预设来保存这些效果,在以后重新调用。

下面简述一下啊SDF的原理以及一些简单的应用。TMP基于SDF可以显著提升抗锯齿效果,并且实现一些特效也十分简单。

百度百科上符号距离函数(sign distance function),简称SDF,又可以称为定向距离函数(oriented distance function),在空间中的一个有限区域上确定一个点到区域边界的距离并同时对距离的符号进行定义:点在区域边界内部为正,外部为负,位于边界上时为0。也就是说SDF记录着当前像素点距离某一个区域的最小距离(这个区域我们可以理解为文字,一就是说可以假设像素值为0的点在区域内,像素值为255的点在区域外)。

那么如何生成SDF呢?根据定义SDF记录着当前像素点距离某一个区域的最小距离,最简单的一种办法就是遍历每一个像素点,找到每一个像素点到某一个区域的最小距离。但是这种方法复杂度太高了,对每一个像素点都需要遍历整张图像,设像素点的数量为n,那么总体时间复杂度为n2

可以利用动态规划去减少计算量。状态转移方法可以很容易列出来。

如果当前像素点 img(i,j)的值小于128的时候那么SDF(i,j)=0;

如果当前像素点 img(i,j)的值大于128的时候那么

SDF(i,j)=min{  SDF(i,j-1)+1   SDF(i,j+1)+1   SDF(i-1,j)+1  SDF(i+1,j)+1.414   SDF(i-1,j-1)+1  SDF(i+1,j-1)+1.414  SDF(i+1,j+1)+1.414    SDF(i-1,j+1)+1.414}

边界值需要特殊处理,很容易知道图像的四周边界是不会包含文字的,所以可以假设SDF(边界)=C。C是一个常数。

但是这个状态转移方程需要知道每一个点四周的8个点,一个动态规划是解决不了这个问题的,因为SDF(i,j)需要依赖SDF(i-1,j),而SDF(i-1,j)又需要依赖SDF(i,j),循环依赖了无解。

因此可以考虑使用两次动态规划求解。

第一个动态规划

SDF1(i,j)=min{  SDF1(i-1,j-1)+1.414   SDF1(i-1,j+1)+1.415   SDF1(i-1,j)+1  SDF1(i,j-1)+1}

第二个动态规划

SDF2(i,j)=min{ SDF2(i+1,j-1)+1.414   SDF2(i+1,j+1)+1.414   SDF2(i+1,j)+1  SDF2(i,j+1)+1}

最后SDF(i,j)=min{SDF1(i,j),SDF2(i,j)}这样就解决问题了。

但是上面求出来的SDF的值全为正数,只能统计区域外距离区域边界的距离,那么这么统计区域内部距离区域边界的距离呢?

这个很简单,我们只需要对 255-img再计算一次正向SDF即可。

最后双向SDF=SDF(img)-SDF(255-img)

我们在这里给出了Python代码计算SDF:

from skimage import io
import numpy as np

def dpLU(img):
    SDF=[[0 for i in range(len(img[0]))]for j in range(len(img))]

    for i in range(len(img)):
        SDF[i][0]=128 if img[i][0]>128 else 0
        SDF[i][-1] = 128 if img[i][-1] > 128 else 0
    for i in range(len(img[0])):
        SDF[0][i]=128 if img[0][i]>128 else 0
        SDF[-1][i] = 128 if SDF[-1][i] > 128 else 0

    for i in range(1, len(img) - 1):
        for j in range(1,len(img[0])-1):
            if img[i][j]<128:
                SDF[i][j]=0
            else:
                SDF[i][j]=min(SDF[i-1][j]+1,SDF[i-1][j+1]+1.414,SDF[i-1][j-1]+1.414,SDF[i][j-1]+1)
    return SDF
def dpRB(img):
    SDF=[[0 for i in range(len(img[0]))]for j in range(len(img))]

    for i in range(len(img)):
        SDF[i][0]=128 if img[i][0]>128 else 0
        SDF[i][-1] = 128 if img[i][-1] > 128 else 0
    for i in range(len(img[0])):
        SDF[0][i]=128 if img[0][i]>128 else 0
        SDF[-1][i] = 128 if SDF[-1][i] > 128 else 0

    for i in range(len(img)-2, 0,-1):
        for j in range(len(img[0])-2,0,-1):
            if img[i][j]<128:
                SDF[i][j]=0
            else:
                SDF[i][j]=min(SDF[i+1][j]+1,SDF[i+1][j+1]+1.414,SDF[i+1][j-1]+1.414,SDF[i][j+1]+1)
    return SDF
def GeneratorSDF():
    img=io.imread("xxx.bmp")[:,:,1]
    SDF1,SDF2=dpLU(img),dpRB(img)
    SDF_img1 = [[0 for i in range(len(img[0]) )] for j in range(len(img))]
    for i in range(len(SDF1)):
        for j in range(len(SDF1[0])):
            SDF_img1[i][j] = min(SDF1[i][j], SDF2[i][j])

    img=255-img
    SDF1, SDF2 = dpLU(img), dpRB(img)
    SDF_img2 = [[0 for i in range(len(img[0]) )] for j in range(len(img))]
    for i in range(len(SDF1)):
         for j in range(len(SDF1[0])):
             SDF_img2[i][j]=min(SDF1[i][j],SDF2[i][j])



    SDF=np.array(SDF_img1)-np.array(SDF_img2)
    SDF=SDF+128

    SDF=SDF.astype(np.uint8)

    io.imsave("SDF.png",SDF)
if __name__ == '__main__':

    GeneratorSDF()

最后我们再给出一个简单的示例:

我们自己手写了一个“你”字,大小为128*128

                ​​​​​​​        ​​​​​​​        ​​​​​​​        ​​​​​​​        ​​​​​​​        ​​​​​​​        

然后生成SDF

 

把原图进行四倍上采样后

 

而对SDF进行上采样然后在进行阈值分割得到,可以看出效果明显更好了。

 

还可以用SDF对字体进行描边

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/674598.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【五子棋实战】第3章 算法包装成第三方接口

【五子棋实战】第3章 算法包装成第三方接口 使用Flask开放接口 ## 定义接口输入 ## 开放接口、跨域配置、数据解析 数据预处理 ## 数据检查与异常捕获 ## 预处理数据 ## 定义接口输出 开启接口 继续学习下一篇实战&#xff01; 我们在上一章实现了博弈树负值极大alpha…

最适合入门的100个深度学习实战项目

&#x1f6a8;注意&#x1f6a8;&#xff1a;最近经粉丝反馈&#xff0c;发现有些订阅者将此专栏内容进行二次售卖&#xff0c;特在此声明&#xff0c;本专栏内容仅供学习&#xff0c;不得以任何方式进行售卖&#xff0c;未经作者许可不得对本专栏内容行使发表权、署名权、修改…

Linux系统安装nginx+入门笔记

安装过程 1.加载wget命令 yum install wget 2.拉取安装包 wget https://nginx.org/download/nginx-1.16.1.tar.gz 3.解压安装包 tar -zxvf nginx-1.16.1.tar.gz 4.执行这个命令自动配置一下 5.编译安装 make make install 6.查看nginx安装的位置 whereis nginx 7.…

基于Python的反爬虫技术的研究设计与实现

博主介绍&#xff1a;擅长Java、微信小程序、Python、Android等&#xff0c;专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专栏推荐订阅&#x1f447;&#x1f3fb; 不然下次找不到哟 Java项目精品实战案例…

链表刷题(4~8)

目录 反转链表 返回中间节点 倒数k个节点 链表分割 判断回文 反转链表 单链表刷题时我们遇到过一个反转链表&#xff0c;那时我们采用的是头插的方式达到反转的效果&#xff0c;那能不能把指针反过来呢&#xff1f;答案是可以的。 这里用三个指针是为了记录后面节点的数据&…

SpringBoot+Vue 的智慧养老系统(Java 项目,附源码,数据库)

作者&#xff1a;程序员徐师兄 个人简介&#xff1a;7 年大厂程序员经历&#xff0c;擅长Java、微信小程序、Python、Android等&#xff0c;大家有这一块的问题可以一起交流&#xff01; 各类成品java毕设 。javaweb&#xff0c;ssh&#xff0c;ssm&#xff0c;springboot等等项…

Jira 8.4.1在Windows环境下的安装和配置

一. Jira安装的环境准备 1.JDK1.8的下载和安装 Jira的运行是依赖java环境的&#xff0c;也就是说需要安装JDK并且要是1.8以上版本。(网上搜一下教程) 2.MySQL数据库的下载和安装(网上搜一下教程) 数据库版本&#xff1a;MySQL5.6&#xff0c;版本太高有时会出现连接不到的…

从双目标定到立体匹配:pyton实践指南

文章目录 前言标定立体匹配文章已经同步更新在3D视觉工坊啦&#xff0c;原文链接如下&#xff1a; 前言 立体匹配是计算机视觉中的一个重要领域&#xff0c;旨在将从不同角度拍摄的图像匹配起来&#xff0c;以创建类似人类视觉的3D效果。实现立体匹配的过程需要涉及许多步骤&a…

华为OD机试真题B卷 Java 实现【统计每个月兔子的总数】,附详细解题思路

一、题目描述 有一种兔子&#xff0c;从出生后第3个月起每个月都生一只兔子&#xff0c;小兔子长到第三个月后每个月又生一只兔子。 例如&#xff1a;假设一只兔子第3个月出生&#xff0c;那么它第5个月开始会每个月生一只兔子。 一月的时候有一只兔子&#xff0c;假如兔子都…

dvwa靶场通关(七)

第七关&#xff1a;SQL Injection&#xff08;sql注入&#xff09; low 我们输入 1,出现报错信息&#xff0c;根据报错信息可知&#xff0c;查询语句是单引号闭合的字符型 接着判断字段数 1 order by 3# 报错 1 order by 2# 正常 所以字段数就是2 利用联合查询爆出数据库名…

农村小子背井离乡北漂的这些年

人生虽不尽人意、生活也并不完美、可生活依旧很美 1. 写在前面 由于工作变动及其他种种原因吧&#xff0c;很长的一段时间内没有再去写文章。始于2019年初夏&#xff0c;止于2020年初冬&#xff0c;再次落笔于2023年的夏季。恰好今天是端午节&#xff0c;祝大家端午安康 白驹过…

springboot第28集:springboot一些概念

DataScopeAspect 数据过滤处理 此切面在执行带有ControllerDataScope注解的方法之前进行数据权限过滤。首先获取当前登录用户&#xff0c;然后判断当前用户是否为超级管理员。如果不是超级管理员&#xff0c;则获取权限字符&#xff0c;默认使用上下文中的权限字符。接下来&am…

【计算机组成原理】2、二进制和十六进制转换,进制相减、内存地址偏移计算与容量计算

文章目录 一、进制转换1.1 二进制转十六进制1.2 十六进制转二进制 二、进制相减2.1 十六进制 三、内存地址偏移计算3.1 根据首末地址&#xff0c;求存储容量3.2 根据末地址 和 存储容量&#xff0c;求首地址 一、进制转换 1.1 二进制转十六进制 因 2 4 16 2^416 2416&#…

leetcode300. 最长递增子序列(动态规划-java)

最长递增子序列 leetcode300. 最长递增子序列题目描述解题思路代码演示: 二分法改进(N * logN)动态规划专题 leetcode300. 最长递增子序列 来源&#xff1a;力扣&#xff08;LeetCode&#xff09; 链接&#xff1a;https://leetcode.cn/problems/longest-increasing-subsequenc…

C++标准输出

C将输入和输出看作字节流&#xff0c;输入时&#xff0c;程序从输入流中抽取字节&#xff0c;输出时&#xff0c;程序将自己插入到输出流中&#xff0c;流充当了程序与流源或流目标之间的桥梁&#xff0c;也就是说C通过流与硬件&#xff0c;文件相关联&#xff0c;流赋予了C程序…

[数字图像处理]第六章 彩色图像处理

第六章 彩色图像处理 引言 ​ 彩色图像处理可分为两个主要领域&#xff1a;全彩色处理和伪彩色处理。在第一类中&#xff0c; 通常要求图像用全彩色传感器获取&#xff0c;如彩色电视摄像机或彩色扫描仪。在第二类中&#xff0c;问题是对一种特定的单色灰度或灰度范围赋予一种…

设计模式之装饰者模式笔记

设计模式之装饰者模式笔记 说明Decorator(装饰)目录装饰者模式示例类图快餐类炒饭类炒面类装饰者类鸡蛋类培根类测试类 说明 记录下学习设计模式-装饰者模式的写法。JDK使用版本为1.8版本。 Decorator(装饰) 意图:动态地给一个对象添加一些额外的职责。 结构: 其中&#x…

【Neo4j教程之CQL命令基本使用】

&#x1f680; Neo4j &#x1f680; &#x1f332; 算法刷题专栏 | 面试必备算法 | 面试高频算法 &#x1f340; &#x1f332; 越难的东西,越要努力坚持&#xff0c;因为它具有很高的价值&#xff0c;算法就是这样✨ &#x1f332; 作者简介&#xff1a;硕风和炜&#xff0c;C…

Python基础篇(二):入门基础必备知识

Python基础篇(一)&#xff1a;如何使用PyCharm创建第一个Python项目(包含tools) 入门基础必备知识 1. 标识符2. 关键字2.1 关键字字典 3. 引号3.1 表示字符串3.2 在字符串中使用引号3.3 创建多行字符串3.4 在注释中使用引号 4. 编码5. 输入输出5.1 输入示例5.2 输出示例5.3 格…

王道计算机网络学习笔记(3)——数据链路层

前言 文章中的内容来自B站王道考研计算机网络课程&#xff0c;想要完整学习的可以到B站官方看完整版。 三&#xff1a;数据链路层 3.1&#xff1a;数据链路层功能概述 结点&#xff1a;主机、路由器 链路&#xff1a;网络中两个结点之间的物理通道&#xff0c;链路的传输介…