OpenGL 高質(zhì)量文本渲染

OpenGL 高質(zhì)量文本渲染

High Quality Text Rendering

前言

在實時 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)行一些位移,則需要更精確地估計字體大小。為此,增加矩形寬度分辨率。此時矩形將被分成垂直切片。然后評估所有這些切片的平均高度。平均高度值用于提高字體大小的精度。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容