OpenGL 高質(zhì)量文本渲染
前言
在實時 3D 圖形中保留盡可能高質(zhì)量的文本具有挑戰(zhàn)性。對象可以動態(tài)地改變它們的位置、旋轉(zhuǎn)、比例和視角。所有這些都會對質(zhì)量產(chǎn)生負(fù)面影響,因為文本通常只生成一次,而不是在每一幀中生成。根據(jù)字體引擎及其性能,為整個文本生成紋理需要很長時間。通常,這段時間足以影響性能。
本文檔介紹了如何在對象為半動態(tài)時獲得最佳文本質(zhì)量的方法。半動態(tài)對象是既不經(jīng)常(不是每幀)也不在動畫時間內(nèi)更改的對象。
此示例描述了如何計算字體大小,這應(yīng)該使紋理像素與屏幕像素緊密匹配。
我們將使用作為 Android 一部分的字體引擎。字體引擎生成包含整個文本形狀的 RGBA 圖像。然后將圖像上傳到紋理中,然后將紋理映射到矩形上。矩形必須具有根據(jù)紋理大小定義的適當(dāng)寬高比。
評估字體大小
要評估對象當(dāng)前轉(zhuǎn)換的字體大小,我們需要將矩形的四個角從 3D 世界空間轉(zhuǎn)換為 2D 像素屏幕空間。以像素為單位表示角,我們可以計算兩個左角之間的距離和兩個右角之間的距離。然后根據(jù)這些距離計算平均值。平均值就是我們要查找的值,因為這是我們將用于生成圖像的字體大小。
// 1. 使用當(dāng)前矩陣計算屏幕坐標(biāo)中的邊界框
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);
// 我們重用已經(jīng)為渲染計算過的矩陣,而不是再次計算矩陣。update() 方法必須在 render() 方法之后調(diào)用
cLT.makePixelCoords(mMVPMatrix, theViewportWidth, theViewportHeight);
cLB.makePixelCoords(mMVPMatrix, theViewportWidth, theViewportHeight);
cRT.makePixelCoords(mMVPMatrix, theViewportWidth, theViewportHeight);
cRB.makePixelCoords(mMVPMatrix, theViewportWidth, theViewportHeight);
// 2. 根據(jù)邊界框角的高度評估字體大小
Vector4f vl = Vector4f.sub(cLB, cLT);
Vector4f vr = Vector4f.sub(cRB, cRT);
textSize = (vl.length3() + vr.length3()) / 2.0f;
下面是 Vector4f 類中 makePixelCoords 方法的定義。該方法將 3D 頂點(diǎn)位置轉(zhuǎn)換為 2D 像素位置。
public void makePixelCoords(float[] aMatrix,
int aViewportWidth,
int aViewportHeight) {
// 將向量轉(zhuǎn)換為屏幕坐標(biāo),我們假設(shè) aMatrix 是 ModelViewProjection 矩陣
// transform 方法將此向量乘以 aMatrix
transform(aMatrix);
// 轉(zhuǎn)換為齊次坐標(biāo)
x /= w;
y /= w;
z /= w;
w = 1.0f;
// 現(xiàn)在向量標(biāo)準(zhǔn)化到了 [-1.0, 1.0] 范圍
// 轉(zhuǎn)換為標(biāo)準(zhǔn)化設(shè)備坐標(biāo)
x = 0.5f + x * 0.5f;
y = 0.5f + y * 0.5f;
z = 0.5f + z * 0.5f;
w = 1.0f;
// 現(xiàn)在值被限制到 [0.0, 1.0] 范圍
// 將坐標(biāo)移動到窗口空間(以像素為單位)
x *= (float) aViewportWidth;
y *= (float) aViewportHeight;
}
紋理生成
由于我們已經(jīng)知道字體大小,我們可以估計目標(biāo)圖像的大小。圖像必須足夠大以存儲整個文本而無需任何剪切。另一方面,它不能太大,因為以下幾何計算是基于圖像大小的。我們希望有一個精確適合字體引擎將要生成的內(nèi)容的大小。
高度計算很簡單,因為是字體的大小,但是寬度非常復(fù)雜。為了正確計算寬度,我們需要使用字體引擎來幫助我們估計它。 Android Java SDK 附帶來自 Paint 對象的 measureText 方法。在測量之前,我們需要向?qū)ο筇峁┧斜匾臄?shù)據(jù),例如:字體名稱、字體大?。ㄎ覀円呀?jīng)計算過)、抗鋸齒、ARGB 顏色(在我們的例子中它總是白色,因為著色可能是稍后在片段著色器中完成),以及其他不太重要的數(shù)據(jù)。
在我們將文本繪制到 Bitmap 對象之前,我們需要使用完全透明的白色 ARGB = (0, 255, 255, 255) 清除其內(nèi)容。使用此顏色清除背景并將 Paint 顏色也設(shè)置為白色,可以防止可能因 alpha 混合而出現(xiàn)的暗紋素。說到混合,GL 混合函數(shù)必須在渲染文本之前正確設(shè)置,混合函數(shù)必須設(shè)置為:glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA)
下面的函數(shù)完成了上面提到的所有步驟:
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);
// 創(chuàng)建一個新的 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));
// 創(chuàng)建一個渲染到 bitmap 的畫布
Canvas bitmapCanvas = new Canvas(textBitmap);
// 將開始繪圖位置設(shè)置為 [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");
}
進(jìn)一步優(yōu)化
如果程序中的文本經(jīng)常更改,那么這個概念可能適合。我們可以創(chuàng)建一個單獨(dú)的線程,它以一定的間隔連續(xù)更新紋理。在大多數(shù)情況下,將線程保持在盡可能低的優(yōu)先級,因為生成文本始終是耗時操作,有可能導(dǎo)致性能中斷。更新紋理應(yīng)該在為 GL 上下文的線程上完成。
如果沿貝塞爾曲線渲染文本或進(jìn)行一些位移,則需要更精確地估計字體大小。為此,增加矩形寬度分辨率。此時矩形將被分成垂直切片。然后評估所有這些切片的平均高度。平均高度值用于提高字體大小的精度。