OpenGL 高质量文本渲染

OpenGL 高质量文本渲染

High Quality Text Rendering

前言

在实时 3D 图形中保留尽可能高质量的文本具有挑战性。对象可以动态地改变它们的位置、旋转、比例和视角。所有这些都会对质量产生负面影响,因为文本通常只生成一次,而不是在每一帧中生成。根据字体引擎及其性能,为整个文本生成纹理需要很长时间。通常,这段时间足以影响性能。

本文档介绍了如何在对象为半动态时获得最佳文本质量的方法。半动态对象是既不经常(不是每帧)也不在动画时间内更改的对象。

此示例描述了如何计算字体大小,这应该使纹理像素与屏幕像素紧密匹配。

我们将使用作为 Android 一部分的字体引擎。字体引擎生成包含整个文本形状的 RGBA 图像。然后将图像上传到纹理中,然后将纹理映射到矩形上。矩形必须具有根据纹理大小定义的适当宽高比。

评估字体大小

要评估对象当前转换的字体大小,我们需要将矩形的四个角从 3D 世界空间转换为 2D 像素屏幕空间。以像素为单位表示角,我们可以计算两个左角之间的距离和两个右角之间的距离。然后根据这些距离计算平均值。平均值就是我们要查找的值,因为这是我们将用于生成图像的字体大小。

// 1. 使用当前矩阵计算屏幕坐标中的边界框
Vector4f cLT = new Vector4f(-0.5f,-0.5f, 0.0f, 1.0f);
Vector4f cLB = new Vector4f(-0.5f, 0.5f, 0.0f, 1.0f);
Vector4f cRT = new Vector4f( 0.5f,-0.5f, 0.0f, 1.0f);
Vector4f cRB = new Vector4f( 0.5f, 0.5f, 0.0f, 1.0f);

// 我们重用已经为渲染计算过的矩阵,而不是再次计算矩阵。update() 方法必须在 render() 方法之后调用
cLT.makePixelCoords(mMVPMatrix, theViewportWidth, theViewportHeight);
cLB.makePixelCoords(mMVPMatrix, theViewportWidth, theViewportHeight);
cRT.makePixelCoords(mMVPMatrix, theViewportWidth, theViewportHeight);
cRB.makePixelCoords(mMVPMatrix, theViewportWidth, theViewportHeight);

// 2. 根据边界框角的高度评估字体大小
Vector4f vl = Vector4f.sub(cLB, cLT);
Vector4f vr = Vector4f.sub(cRB, cRT);
textSize = (vl.length3() + vr.length3()) / 2.0f;

下面是 Vector4f 类中 makePixelCoords 方法的定义。该方法将 3D 顶点位置转换为 2D 像素位置。

public void makePixelCoords(float[] aMatrix,
                            int aViewportWidth,
                            int aViewportHeight) {
  // 将向量转换为屏幕坐标,我们假设 aMatrix 是 ModelViewProjection 矩阵
  // transform 方法将此向量乘以 aMatrix
  transform(aMatrix);
  
  // 转换为齐次坐标
  x /= w;
  y /= w;
  z /= w;
  w = 1.0f;
  // 现在向量标准化到了 [-1.0, 1.0] 范围
  
  // 转换为标准化设备坐标
  x = 0.5f + x * 0.5f;
  y = 0.5f + y * 0.5f;
  z = 0.5f + z * 0.5f;
  w = 1.0f;
  // 现在值被限制到 [0.0, 1.0] 范围
  
  // 将坐标移动到窗口空间(以像素为单位)
  x *= (float) aViewportWidth;
  y *= (float) aViewportHeight;
}

纹理生成

由于我们已经知道字体大小,我们可以估计目标图像的大小。图像必须足够大以存储整个文本而无需任何剪切。另一方面,它不能太大,因为以下几何计算是基于图像大小的。我们希望有一个精确适合字体引擎将要生成的内容的大小。

高度计算很简单,因为是字体的大小,但是宽度非常复杂。为了正确计算宽度,我们需要使用字体引擎来帮助我们估计它。 Android Java SDK 附带来自 Paint 对象的 measureText 方法。在测量之前,我们需要向对象提供所有必要的数据,例如:字体名称、字体大小(我们已经计算过)、抗锯齿、ARGB 颜色(在我们的例子中它总是白色,因为着色可能是稍后在片段着色器中完成),以及其他不太重要的数据。

在我们将文本绘制到 Bitmap 对象之前,我们需要使用完全透明的白色 ARGB = (0, 255, 255, 255) 清除其内容。使用此颜色清除背景并将 Paint 颜色也设置为白色,可以防止可能因 alpha 混合而出现的暗纹素。说到混合,GL 混合函数必须在渲染文本之前正确设置,混合函数必须设置为:glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA)

下面的函数完成了上面提到的所有步骤:

private void drawCanvasToTexture(
        String aText,
        float aFontSize) {
  
  if (aFontSize < 8.0f)
  aFontSize = 8.0f;
  
  if (aFontSize > 500.0f)
  aFontSize = 500.0f;
  
  Paint textPaint = new Paint();
  textPaint.setTextSize(aFontSize);
  textPaint.setFakeBoldText(false);
  textPaint.setAntiAlias(true);
  textPaint.setARGB(255, 255, 255, 255);
  // 如果支持 hinting,需要启用(取消注释下面一行)
  // textPaint.setHinting(Paint.HINTING_ON);
  textPaint.setSubpixelText(true);
  textPaint.setXfermode(new PorterDuffXfermode(Mode.SCREEN));
  
  float realTextWidth = textPaint.measureText(aText);
  
  // 创建一个新的 bitmap,宽高为128像素
  bitmapWidth = (int)(realTextWidth + 2.0f);
  bitmapHeight = (int)aFontSize + 2;
  
  Bitmap textBitmap = Bitmap.createBitmap(bitmapWidth, bitmapHeight, Bitmap.Config.ARGB_8888);
  textBitmap.eraseColor(Color.argb(0, 255, 255, 255));
  // 创建一个渲染到 bitmap 的画布
  Canvas bitmapCanvas = new Canvas(textBitmap);
  // 将开始绘图位置设置为 [1, base_line_position]
  // base_line_position 可能因字体而异,但通常等于字体大小(高度)的 75%。
  bitmapCanvas.drawText(aText, 1, 1.0f + aFontSize * 0.75f, textPaint);
  
  GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId[0]);
  HighQualityTextRenderer.checkGLError("glBindTexture");
  // 上传 bitmap 像素到 OpenGL 纹理
  GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_RGBA, textBitmap, 0);
  // 释放 bitmap
  textBitmap.recycle();
  
  // 图像上传到 texture 后,重新生成 mipmap
  GLES20.glGenerateMipmap(GLES20.GL_TEXTURE_2D);
  HighQualityTextRenderer.checkGLError("glGenerateMipmap");
}

进一步优化

  • 如果程序中的文本经常更改,那么这个概念可能适合。我们可以创建一个单独的线程,它以一定的间隔连续更新纹理。在大多数情况下,将线程保持在尽可能低的优先级,因为生成文本始终是耗时操作,有可能导致性能中断。更新纹理应该在为 GL 上下文的线程上完成。

  • 如果沿贝塞尔曲线渲染文本或进行一些位移,则需要更精确地估计字体大小。为此,增加矩形宽度分辨率。此时矩形将被分成垂直切片。然后评估所有这些切片的平均高度。平均高度值用于提高字体大小的精度。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,293评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,604评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,958评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,729评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,719评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,630评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,000评论 3 397
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,665评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,909评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,646评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,726评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,400评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,986评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,959评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,996评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,481评论 2 342

推荐阅读更多精彩内容