目标
在实时 3D 图形中尽可能保持最高质量的文本极富挑战。对象的位置、旋转、缩放以及视角都可能发生动态变化。所有这些都对质量造成负面影响,因为文本通常只生成一次,而不是每一帧都生成。为整个文本生成纹理非常耗时,具体取决于字体引擎及其性能。这一时长通常足以让性能下滑。
本文演示了一种在对象为半动态时尽可能获得最佳文本质量的方式。半动态对象只是一个对象,它不会经常变化(并非每一帧),也不会在动画时间中改变。
概述
本白皮书介绍如何计算字体大小,其应当可将纹素紧密匹配至屏幕像素。
我们将使用属于 Android 一部分的字体引擎。该字体引擎产生包含整个文本形状的 RGBA 图像。接着该图像上传到纹理中,而后纹理再映射到矩形上。该矩形几何体必须根据纹理大小定义适当的纵横比。
估算字体大小
要估算对象当前转换的字体大小,我们需要将矩形的四个角从 3D 世界空间转换到 2D 像素屏幕空间。以像素表示角后,我们可以计算两个左角之间的距离,以及两个右角之间的距离。然后根据这两个距离计算平均值。平均值正是我们要找到的,它就是我们要用于生成图像的字体大小。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
// 1. Calculate bounding box in screen coordinates with current matrices
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
)
;
// Instead of calculating matrices again lets reuse ones which were already calculated
// for rendering purpose. One important thing is the update() method must be called
// after render() method
cLT
.
makePixelCoords
(
mMVPMatrix
,
theViewportWidth
,
theViewportHeight
)
;
cLB
.
makePixelCoords
(
mMVPMatrix
,
theViewportWidth
,
theViewportHeight
)
;
cRT
.
makePixelCoords
(
mMVPMatrix
,
theViewportWidth
,
theViewportHeight
)
;
cRB
.
makePixelCoords
(
mMVPMatrix
,
theViewportWidth
,
theViewportHeight
)
;
// 2. Evaluate font size based on the heights of bounding box corners
Vector4f
vl
=
Vector4f
.
sub
(
cLB
,
cLT
)
;
Vector4f
vr
=
Vector4f
.
sub
(
cRB
,
cRT
)
;
textSize
=
(
vl
.
length3
(
)
+
vr
.
length3
(
)
)
/
2.0f
;
|
以下是 Vector4f 类中 makePixelCoords 方法的定义。该方法将 3D 顶点位置转换为 2D 像素位置。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
public
void
makePixelCoords
(
float
[
]
aMatrix
,
int
aViewportWidth
,
int
aViewportHeight
)
{
// Transform the vector into screen coordinates we assumes aMatrix is ModelViewProjection matrix
// transform method multiplies this vector by the matrix
transform
(
aMatrix
)
;
// Make coordinates as homogenous
x
/=
w
;
y
/=
w
;
z
/=
w
;
w
=
1.0f
;
// ... now the vector is normalized to the range [-1.0, 1.0]
// Normalize values into NDC.
x
=
0.5f
+
x *
0.5f
;
y
=
0.5f
+
y *
0.5f
;
z
=
0.5f
+
z *
0.5f
;
w
=
1.0f
;
// ... currently the values are clipped to the [0.0, 1.0] range
// Move coordinates into window space (in pixels)
x *
=
(
float
)
aViewportWidth
;
y *
=
(
float
)
aViewportHeight
;
}
|
纹理生成
由于我们已经知道字体大小,因此可以估计目标图像的大小。图像必须足够大,能够存储整个文本且无任何截断。另一方面,它也不能太大,因为后续的几何计算以图像大小为基础。我们希望这一大小刚好适合字体引擎将要生成的内容。
高度计算比较简单,因为这就是字体的尺寸;但宽度就非常复杂了。要正确计算宽度,需要使用字体引擎来帮助我们进行估计。Android Java SDK 在 Paint 对象中附带了 measureText 方法。在执行测量之前,我们需要将所有必要的数据传送给该对象,例如:字体名称、字体大小(已经计算出)、抗锯齿、颜色 ARGB(此情形中始终为白色,因为以后可在片段着色器中进行上色),以及其他较不重要的数据。
在将文本绘制到位图对象前,我们需要使用白色(Alpha 值为完全透明 ARGB = (0, 255, 255, 255))来清除其内容。通过使用此颜色清除背景并将绘制颜色也设为白色,可以防止 α 混合可能导致的暗纹素。谈到混合的话,就务必要提一下 GL 混合函数,必须正确设置该函数,然后再渲染文本。该混合函数必须设为:glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA)
下面的函数执行上述所有步骤:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
|
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
(
true
)
;
textPaint
.
setAntiAlias
(
true
)
;
textPaint
.
setARGB
(
255
,
255
,
255
,
255
)
;
textPaint
.
setSubpixelText
(
true
)
;
textPaint
.
setXfermode
(
new
PorterDuffXfermode
(
Mode
.
SCREEN
)
)
;
// Set hinting if available
//textPaint.setHinting(Paint.HINTING_ON);
float
realTextWidth
=
textPaint
.
measureText
(
aText
)
;
//Create a new mutable bitmap
bitmapWidth
=
(
int
)
(
realTextWidth
+
2.0f
)
;
//text.length() * textSize;
bitmapHeight
=
(
int
)
aFontSize
+
2
;
Bitmap
textBitmap
=
Bitmap
.
createBitmap
(
bitmapWidth
,
bitmapHeight
,
Bitmap
.
Config
.
ARGB_8888
)
;
textBitmap
.
eraseColor
(
Color
.
argb
(
0
,
255
,
255
,
255
)
)
;
//creates a new canvas that will draw into a bitmap instead of rendering into the screen
Canvas
bitmapCanvas
=
new
Canvas
(
textBitmap
)
;
// Set start drawing position to [1, base_line_position]
// The base_line_position may vary from one font to another but it usually is equal to 75% of font size (height).
bitmapCanvas
.
drawText
(
aText
,
1
,
1.0f
+
aFontSize *
0.75f
,
textPaint
)
;
GLES20
.
glBindTexture
(
GLES20
.
GL_TEXTURE_2D
,
textureId
[
0
]
)
;
// Assign the OpenGL texture with the Bitmap
GLUtils
.
texImage2D
(
GLES20
.
GL_TEXTURE_2D
,
0
,
GLES20
.
GL_RGBA
,
textBitmap
,
0
)
;
//free memory resources associated with this texture
textBitmap
.
recycle
(
)
;
// After the image has been subloaded to texture, regenerate mipmaps
GLES20
.
glGenerateMipmap
(
GLES20
.
GL_TEXTURE_2D
)
;
}
|
进一步改进
- 如果您的程序中文本经常变化,那么此概念可能也会适合。我们建议创建一个独立的线程,它将以一定间隔持续更新相关纹理。在大多数情形中,最需要的是将该线程维持在尽可能最低的优先级上,因为生成文本总是被视为重量级运算,很有可能导致性能降级。不言而喻,更新纹理应当在服务于 GL 上下文的线程上执行。
- 如果您要沿着贝塞尔曲线渲染文本,或者要进行一些位移,您需要更加精准地估计字体大小。为此,还要增加矩形的水平分辨率。此时,矩形将分割为垂直切面。然后,估算所有这些切面的平均高度。其平均高度值用于提高字体大小的精度。
原文:http://malideveloper.arm.com/cn/develop-for-mali/sample-code/%E9%AB%98%E8%B4%A8%E9%87%8F%E6%96%87%E6%9C%AC%E6%B8%B2%E6%9F%93/