Unity技术文档-Text与TextMeshPro原理解析,性能分析_unitytext和textmeshpro-程序员宅基地

技术标签: c#  unity  ui  瞎编瞎写  

这一篇博客用于分析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对字体进行描边

 

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/qq_27489007/article/details/127567194

智能推荐

python opencv resize函数_python opencv 等比例调整(缩放)图片分辨率大小代码 cv2.resize()...-程序员宅基地

文章浏览阅读1.3k次。# -*- coding: utf-8 -*-"""@File : 200113_等比例调整图像分辨率大小.py@Time : 2020/1/13 13:38@Author : Dontla@Email : [email protected]@Software: PyCharm"""import cv2def img_resize(image):height, width = image...._opencv小图等比例缩放

【OFDM、OOK、PPM、QAM的BER仿真】绘制不同调制方案的误码率曲线研究(Matlab代码实现)-程序员宅基地

文章浏览阅读42次。对于这些调制技术的误码率(BER)研究是非常重要的,因为它们可以帮助我们了解在不同信道条件下系统的性能表现。通过以上步骤,您可以进行OFDM、OOK、PPM和QAM的误码率仿真研究,并绘制它们的误码率曲线,以便更好地了解它们在不同信道条件下的性能特点。针对这些调制技术的BER研究是非常重要的,可以帮助我们更好地了解这些技术在不同信道条件下的性能表现,从而指导系统设计和优化。6. 分析结果:根据误码率曲线的比较,分析每种调制方案在不同信噪比条件下的性能,包括其容忍的信道条件和适用的应用场景。_ber仿真

【已解决】Vue的Element框架,日期组件(el-date-picker)的@change事件,不会触发。_el-date-picker @change不触发-程序员宅基地

文章浏览阅读2.5w次,点赞3次,收藏3次。1、场景照抄官方的实例,绑定了 myData.Age 这个值。实际选择某个日期后,从 vuetool(开发工具)看,值已经更新了,但视图未更新。2、尝试绑定另一个值: myData,可以正常的触发 @change 方法。可能是:值绑定到子对象时,组件没有侦测到。3、解决使用 @blur 代替 @change 方法。再判断下 “值有没有更新” 即可。如有更好的方法,欢迎评论!..._el-date-picker @change不触发

PCL学习:滤波—Projectlnliers投影滤波_projectinliers-程序员宅基地

文章浏览阅读1.5k次,点赞2次,收藏8次。Projectlnliersclass pcl: : Projectlnliers< PointT >类 Projectlnliers 使用一个模型和一组的内点的索引,将内点投影到模型形成新的一个独立点云。关键成员函数 void setModelType(int model) 通过用户给定的参数设置使用的模型类型 ,参数 Model 为模型类型(见 mo..._projectinliers

未处理System.BadImageFormatException”类型的未经处理的异常在 xxxxxxx.exe 中发生_“system.badimageformatexception”类型的未经处理的异常在 未知模块。 -程序员宅基地

文章浏览阅读2.4k次。“System.BadImageFormatException”类型的未经处理的异常在 xxxx.exe 中发生其他信息: 未能加载文件或程序集“xxxxxxx, Version=xxxxxx,xxxxxxx”或它的某一个依赖项。试图加载格式不正确的程序。此原因是由于 ” 目标程序的目标平台与 依赖项的目标编译平台不一致导致,把所有的项目都修改到同一目标平台下(X86、X64或AnyCPU)进行编译,一般即可解决问题“。若果以上方式不能解决,可采用如下方式:右键选择配置管理器,在这里修改平台。_“system.badimageformatexception”类型的未经处理的异常在 未知模块。 中发生

PC移植安卓---2018/04/26_电脑软件移植安卓-程序员宅基地

文章浏览阅读2.4k次。记录一下碰到的问题:1.Assetbundle加载问题: 原PC打包后的AssetBundle导入安卓工程后,加载会出问题。同时工程打包APK时,StreamingAssets中不能有中文。解决方案: (1).加入PinYinConvert类,用于将中文转换为拼音(多音字可能会出错,例如空调转换为KongDiao||阿拉伯数字不支持,如Ⅰ、Ⅱ、Ⅲ、Ⅳ(IIII)、Ⅴ、Ⅵ、Ⅶ、Ⅷ、Ⅸ、Ⅹ..._电脑软件移植安卓

随便推点

聊聊线程之run方法_start 是同步还是异步-程序员宅基地

文章浏览阅读2.4k次。话不多说参考书籍 汪文君补充知识:start是异步,run是同步,start的执行会经过JNI方法然后被任务执行调度器告知给系统内核分配时间片进行创建线程并执行,而直接调用run不经过本地方法就是普通对象执行实例方法。什么是线程?1.现在几乎百分之百的操作系统都支持多任务的执行,对计算机来说每一个人物就是一个进程(Process),在每一个进程内部至少要有一个线程实在运行中,有时线..._start 是同步还是异步

制作非缘勿扰页面特效----JQuery_单击标题“非缘勿扰”,<dd>元素中有id属性的<span>的文本(主演、导演、标签、剧情-程序员宅基地

文章浏览阅读5.3k次,点赞9次,收藏34次。我主要用了层次选择器和属性选择器可以随意选择,方便简单为主大体CSS格式 大家自行构造网页主体<body> <div class='main' > <div class='left'> <img src="images/pic.gif" /> <br/><br/> <img src="images/col.gif" alt="收藏本片"/&_单击标题“非缘勿扰”,元素中有id属性的的文本(主演、导演、标签、剧情

有了这6款浏览器插件,浏览器居然“活了”?!媳妇儿直呼“大开眼界”_浏览器插件助手-程序员宅基地

文章浏览阅读901次,点赞20次,收藏23次。浏览器是每台电脑的必装软件,去浏览器搜索资源和信息已经成为我们的日常,我媳妇儿原本也以为浏览器就是上网冲浪而已,哪有那么强大,但经过我的演示之后她惊呆了,直接给我竖起大拇指道:“原来浏览器还能这么用?大开眼界!今天来给大家介绍几款实用的浏览器插件,学会之后让你的浏览器“活过来”!_浏览器插件助手

NumPy科学数学库_数学中常用的环境有numpy-程序员宅基地

文章浏览阅读101次。NumPy是Python中最常用的科学数学计算库之一,它提供了高效的多维数组对象以及对这些数组进行操作的函数NumPy的核心是ndarray(N-dimensional array)对象,它是一个用于存储同类型数据的多维数组Numpy通常与SciPy(Scientific Python)和 Matplotlib(绘图库)一起使用,用于替代MatLabSciPy是一个开源的Python算法库和数学工具包;Matplotlib是Python语言及其Numpy的可视化操作界面'''_数学中常用的环境有numpy

dind(docker in docker)学习-程序员宅基地

文章浏览阅读1.1w次。docker in docker说白了,就是在docker容器内启动一个docker daemon,对外提供服务。优点在于:镜像和容器都在一个隔离的环境,保持操作者的干净环境。想到了再补充 :)一:低版本启动及访问启动1.12.6-dinddocker run --privileged -d --name mydocker docker:1.12.6-dind在其他容器访问d..._dind

推荐文章

热门文章

相关标签