OpenGL 圖形庫的使用(四十六)—— 實戰之文本渲染Text Rendering

版本記錄

版本號 時間
V1.0 2018.01.20

前言

OpenGL 圖形庫項目中一直也沒用過,最近也想學著使用這個圖形庫,感覺還是很有意思,也就自然想著好好的總結一下,希望對大家能有所幫助。下面內容來自歡迎來到OpenGL的世界
1. OpenGL 圖形庫使用(一) —— 概念基礎
2. OpenGL 圖形庫使用(二) —— 渲染模式、對象、擴展和狀態機
3. OpenGL 圖形庫使用(三) —— 著色器、數據類型與輸入輸出
4. OpenGL 圖形庫使用(四) —— Uniform及更多屬性
5. OpenGL 圖形庫使用(五) —— 紋理
6. OpenGL 圖形庫使用(六) —— 變換
7. OpenGL 圖形庫的使用(七)—— 坐標系統之五種不同的坐標系統(一)
8. OpenGL 圖形庫的使用(八)—— 坐標系統之3D效果(二)
9. OpenGL 圖形庫的使用(九)—— 攝像機(一)
10. OpenGL 圖形庫的使用(十)—— 攝像機(二)
11. OpenGL 圖形庫的使用(十一)—— 光照之顏色
12. OpenGL 圖形庫的使用(十二)—— 光照之基礎光照
13. OpenGL 圖形庫的使用(十三)—— 光照之材質
14. OpenGL 圖形庫的使用(十四)—— 光照之光照貼圖
15. OpenGL 圖形庫的使用(十五)—— 光照之投光物
16. OpenGL 圖形庫的使用(十六)—— 光照之多光源
17. OpenGL 圖形庫的使用(十七)—— 光照之復習總結
18. OpenGL 圖形庫的使用(十八)—— 模型加載之Assimp
19. OpenGL 圖形庫的使用(十九)—— 模型加載之網格
20. OpenGL 圖形庫的使用(二十)—— 模型加載之模型
21. OpenGL 圖形庫的使用(二十一)—— 高級OpenGL之深度測試
22. OpenGL 圖形庫的使用(二十二)—— 高級OpenGL之模板測試Stencil testing
23. OpenGL 圖形庫的使用(二十三)—— 高級OpenGL之混合Blending
24. OpenGL 圖形庫的使用(二十四)—— 高級OpenGL之面剔除Face culling
25. OpenGL 圖形庫的使用(二十五)—— 高級OpenGL之幀緩沖Framebuffers
26. OpenGL 圖形庫的使用(二十六)—— 高級OpenGL之立方體貼圖Cubemaps
27. OpenGL 圖形庫的使用(二十七)—— 高級OpenGL之高級數據Advanced Data
28. OpenGL 圖形庫的使用(二十八)—— 高級OpenGL之高級GLSL Advanced GLSL
29. OpenGL 圖形庫的使用(二十九)—— 高級OpenGL之幾何著色器Geometry Shader
30. OpenGL 圖形庫的使用(三十)—— 高級OpenGL之實例化Instancing
31. OpenGL 圖形庫的使用(三十一)—— 高級OpenGL之抗鋸齒Anti Aliasing
32. OpenGL 圖形庫的使用(三十二)—— 高級光照之高級光照Advanced Lighting
33. OpenGL 圖形庫的使用(三十三)—— 高級光照之Gamma校正Gamma Correction
34. OpenGL 圖形庫的使用(三十四)—— 高級光照之陰影 - 陰影映射Shadow Mapping
35. OpenGL 圖形庫的使用(三十五)—— 高級光照之陰影 - 點陰影Point Shadows
36. OpenGL 圖形庫的使用(三十六)—— 高級光照之法線貼圖Normal Mapping
37. OpenGL 圖形庫的使用(三十七)—— 高級光照之視差貼圖Parallax Mapping
38. OpenGL 圖形庫的使用(三十八)—— 高級光照之HDR
39. OpenGL 圖形庫的使用(三十九)—— 高級光照之泛光
40. OpenGL 圖形庫的使用(四十)—— 高級光照之延遲著色法Deferred Shading
41. OpenGL 圖形庫的使用(四十一)—— 高級光照之SSAO
42. OpenGL 圖形庫的使用(四十二)—— PBR之理論Theory
43. OpenGL 圖形庫的使用(四十三)—— PBR之光照Lighting
44. OpenGL 圖形庫的使用(四十四)—— PBR之幾篇沒有翻譯的英文原稿
45. OpenGL 圖形庫的使用(四十五)—— 實戰之調試Debugging

文本渲染

當你在圖形計算領域冒險到了一定階段以后你可能會想使用OpenGL來繪制文本。然而,可能與你想象的并不一樣,使用像OpenGL這樣的底層庫來把文本渲染到屏幕上并不是一件簡單的事情。如果你只需要繪制128種不同的字符(Character),那么事情可能會簡單一些。但是如果你要繪制的字符有著不同的寬、高和邊距,事情馬上就復雜了。根據你使用語言的不同,你可能會需要多于128個字符。再者,如果你要繪制音樂符、數學符號這些特殊的符號;或者渲染豎排文本呢?一旦你把文本這些復雜的情況考慮進來,你就不會奇怪為什么OpenGL這樣的底層API沒有包含文本處理了。

由于OpenGL本身并沒有包含任何的文本處理能力,我們必須自己定義一套全新的系統讓OpenGL繪制文本到屏幕上。由于文本字符沒有圖元,我們必須要有點創造力才行。需要使用的一些技術可以是:通過GL_LINES來繪制字形,創建文本的3D網格(Mesh),或在3D環境中將字符紋理渲染到2D四邊形上。

開發者最常用的一種方式是將字符紋理繪制到四邊形上。繪制這些紋理四邊形本身其實并不是很復雜,然而檢索要繪制文本的紋理卻變成了一項有挑戰性的工作。本教程將探索多種文本渲染的實現方法,并且使用FreeType庫實現一個更加高級但更靈活的渲染文本技術。


經典文本渲染:位圖字體

早期的時候,渲染文本是通過選擇一個需要的字體(Font)(或者自己創建一個),并提取這個字體中所有相關的字符,將它們放到一個單獨的大紋理中來實現的。這樣一張紋理叫做位圖字體(Bitmap Font),它在紋理的預定義區域中包含了我們想要使用的所有字符。字體的這些字符被稱為字形(Glyph)。每個字形都關聯著一個特定的紋理坐標區域。當你想要渲染一個字符的時候,你只需要通過渲染這一塊特定的位圖字體區域到2D四邊形上即可。

你可以看到,我們取一張位圖字體,(通過仔細選擇紋理坐標)從紋理中采樣對應的字形,并渲染它們到多個2D四邊形上,最終渲染出“OpenGL”文本。通過啟用混合,讓背景保持透明,最終就能渲染一個字符串到屏幕上。這個位圖字體是通過Codehead的位圖字體生成器生成的。

使用這種方式繪制文本有許多優勢也有很多缺點。首先,它相對來說很容易實現,并且因為位圖字體已經預光柵化了,它的效率也很高。然而,這種方式不夠靈活。當你想要使用不同的字體時,你需要重新編譯一套全新的位圖字體,而且你的程序會被限制在一個固定的分辨率。如果你對這些文本進行縮放的話你會看到文本的像素邊緣。此外,這種方式通常會局限于非常小的字符集,如果你想讓它來支持Extended或者Unicode字符的話就很不現實了。

這種繪制文本的方式曾經得益于它的高速和可移植性而非常流行,然而現在已經出現更加靈活的方式了。其中一個是我們即將討論的使用FreeType庫來加載TrueType字體的方式。


現代文本渲染:FreeType

FreeType是一個能夠用于加載字體并將他們渲染到位圖以及提供多種字體相關的操作的軟件開發庫。它是一個非常受歡迎的跨平臺字體庫,它被用于Mac OS X、Java、PlayStation主機、Linux、Android等平臺。FreeType的真正吸引力在于它能夠加載TrueType字體。

TrueType字體不是用像素或其他不可縮放的方式來定義的,它是通過數學公式(曲線的組合)來定義的。類似于矢量圖像,這些光柵化后的字體圖像可以根據需要的字體高度來生成。通過使用TrueType字體,你可以輕易渲染不同大小的字形而不造成任何質量損失。

FreeType可以在他們的官方網站中下載到。你可以選擇自己用源碼編譯這個庫,如果支持你的平臺的話,你也可以使用他們預編譯好的庫。請確認你將freetype.lib添加到你項目的鏈接庫中,并且確認編譯器知道頭文件的位置。

然后請確認包含合適的頭文件:

#include <ft2build.h>
#include FT_FREETYPE_H  

由于FreeType的開發方式(至少在我寫這篇文章的時候),你不能將它們的頭文件放到一個新的目錄下。它們應該保存在你include目錄的根目錄下。通過使用像 #include <FreeType/ft2build.h>這樣的方式導入FreeType可能會出現一些頭文件沖突的問題。

FreeType所做的事就是加載TrueType字體并為每一個字形生成位圖以及計算幾個度量值(Metric)。我們可以提取出它生成的位圖作為字形的紋理,并使用這些度量值定位字符的字形。

要加載一個字體,我們只需要初始化FreeType庫,并且將這個字體加載為一個FreeType稱之為面(Face)的東西。這里為我們加載一個從Windows/Fonts目錄中拷貝來的TrueType字體文件arial.ttf

FT_Library ft;
if (FT_Init_FreeType(&ft))
    std::cout << "ERROR::FREETYPE: Could not init FreeType Library" << std::endl;

FT_Face face;
if (FT_New_Face(ft, "fonts/arial.ttf", 0, &face))
    std::cout << "ERROR::FREETYPE: Failed to load font" << std::endl;

這些FreeType函數在出現錯誤時將返回一個非零的整數值。

當面加載完成之后,我們需要定義字體大小,這表示著我們要從字體面中生成多大的字形:

FT_Set_Pixel_Sizes(face, 0, 48);  

此函數設置了字體面的寬度和高度,將寬度值設為0表示我們要從字體面通過給定的高度中動態計算出字形的寬度。

一個FreeType面中包含了一個字形的集合。我們可以調用FT_Load_Char函數來將其中一個字形設置為激活字形。這里我們選擇加載字符字形’X’:

if (FT_Load_Char(face, 'X', FT_LOAD_RENDER))
    std::cout << "ERROR::FREETYTPE: Failed to load Glyph" << std::endl;

通過將FT_LOAD_RENDER設為加載標記之一,我們告訴FreeType去創建一個8位的灰度位圖,我們可以通過face->glyph->bitmap來訪問這個位圖。

使用FreeType加載的每個字形沒有相同的大小(不像位圖字體那樣)。使用FreeType生成的位圖的大小恰好能包含這個字符可見區域。例如生成用于表示’.’的位圖的大小要比表示’X’的小得多。因此,FreeType同樣也加載了一些度量值來指定每個字符的大小和位置。下面這張圖展示了FreeType對每一個字符字形計算的所有度量值。

每一個字形都放在一個水平的基準線(Baseline)上(即上圖中水平箭頭指示的那條線)。一些字形恰好位于基準線上(如’X’),而另一些則會稍微越過基準線以下(如’g’或’p’)(譯注:即這些帶有下伸部的字母,可以見這里)。這些度量值精確定義了擺放字形所需的每個字形距離基準線的偏移量,每個字形的大小,以及需要預留多少空間來渲染下一個字形。下面這個表列出了我們需要的所有屬性。

在需要渲染字符時,我們可以加載一個字符字形,獲取它的度量值,并生成一個紋理,但每一幀都這樣做會非常沒有效率。我們應將這些生成的數據儲存在程序的某一個地方,在需要渲染字符的時候再去調用。我們會定義一個非常方便的結構體,并將這些結構體存儲在一個map中。

struct Character {
    GLuint     TextureID;  // 字形紋理的ID
    glm::ivec2 Size;       // 字形大小
    glm::ivec2 Bearing;    // 從基準線到字形左部/頂部的偏移值
    GLuint     Advance;    // 原點距下一個字形原點的距離
};

std::map<GLchar, Character> Characters;

對于這個教程來說,本著讓一切簡單的目的,我們只生成ASCII字符集的前128個字符。對每一個字符,我們生成一個紋理并保存相關數據至Character結構體中,之后再添加至Characters這個映射表中。這樣子,渲染一個字符所需的所有數據就都被儲存下來備用了。

glPixelStorei(GL_UNPACK_ALIGNMENT, 1); //禁用字節對齊限制
for (GLubyte c = 0; c < 128; c++)
{
    // 加載字符的字形 
    if (FT_Load_Char(face, c, FT_LOAD_RENDER))
    {
        std::cout << "ERROR::FREETYTPE: Failed to load Glyph" << std::endl;
        continue;
    }
    // 生成紋理
    GLuint texture;
    glGenTextures(1, &texture);
    glBindTexture(GL_TEXTURE_2D, texture);
    glTexImage2D(
        GL_TEXTURE_2D,
        0,
        GL_RED,
        face->glyph->bitmap.width,
        face->glyph->bitmap.rows,
        0,
        GL_RED,
        GL_UNSIGNED_BYTE,
        face->glyph->bitmap.buffer
    );
    // 設置紋理選項
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    // 儲存字符供之后使用
    Character character = {
        texture, 
        glm::ivec2(face->glyph->bitmap.width, face->glyph->bitmap.rows),
        glm::ivec2(face->glyph->bitmap_left, face->glyph->bitmap_top),
        face->glyph->advance.x
    };
    Characters.insert(std::pair<GLchar, Character>(c, character));
}

在這個for循環中我們遍歷了ASCII集中的全部128個字符,并獲取它們對應的字符字形。對每一個字符,我們生成了一個紋理,設置了它的選項,并儲存了它的度量值。有趣的是我們這里將紋理的internalFormatformat設置為GL_RED。通過字形生成的位圖是一個8位灰度圖,它的每一個顏色都由一個字節來表示。因此我們需要將位圖緩沖的每一字節都作為紋理的顏色值。這是通過創建一個特殊的紋理實現的,這個紋理的每一字節都對應著紋理顏色的紅色分量(顏色向量的第一個字節)。如果我們使用一個字節來表示紋理的顏色,我們需要注意OpenGL的一個限制:

glPixelStorei(GL_UNPACK_ALIGNMENT, 1);   

OpenGL要求所有的紋理都是4字節對齊的,即紋理的大小永遠是4字節的倍數。通常這并不會出現什么問題,因為大部分紋理的寬度都為4的倍數并/或每像素使用4個字節,但是現在我們每個像素只用了一個字節,它可以是任意的寬度。通過將紋理解壓對齊參數設為1,這樣才能確保不會有對齊問題(它可能會造成段錯誤)。

當你處理完字形后不要忘記清理FreeType的資源。

FT_Done_Face(face);
FT_Done_FreeType(ft);

1. 著色器

我們將使用下面的頂點著色器來渲染字形:

#version 330 core
layout (location = 0) in vec4 vertex; // <vec2 pos, vec2 tex>
out vec2 TexCoords;

uniform mat4 projection;

void main()
{
    gl_Position = projection * vec4(vertex.xy, 0.0, 1.0);
    TexCoords = vertex.zw;
}

我們將位置和紋理紋理坐標的數據合起來存在一個vec4中。這個頂點著色器將位置坐標與一個投影矩陣相乘,并將紋理坐標傳遞給片段著色器:

#version 330 core
in vec2 TexCoords;
out vec4 color;

uniform sampler2D text;
uniform vec3 textColor;

void main()
{    
    vec4 sampled = vec4(1.0, 1.0, 1.0, texture(text, TexCoords).r);
    color = vec4(textColor, 1.0) * sampled;
}

片段著色器有兩個uniform變量:一個是單顏色通道的字形位圖紋理,另一個是顏色uniform,它可以用來調整文本的最終顏色。我們首先從位圖紋理中采樣顏色值,由于紋理數據中僅存儲著紅色分量,我們就采樣紋理的r分量來作為取樣的alpha值。通過變換顏色的alpha值,最終的顏色在字形背景顏色上會是透明的,而在真正的字符像素上是不透明的。我們也將RGB顏色與textColor這個uniform相乘,來變換文本顏色。

當然我們需要啟用混合才能讓這一切行之有效:

glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);  

對于投影矩陣,我們將使用一個正射投影矩陣(Orthographic Projection Matrix)。對于文本渲染我們(通常)都不需要透視,使用正射投影同樣允許我們在屏幕坐標系中設定所有的頂點坐標,如果我們使用如下方式配置:

glm::mat4 projection = glm::ortho(0.0f, 800.0f, 0.0f, 600.0f);

我們設置投影矩陣的底部參數為0.0f,并將頂部參數設置為窗口的高度。這樣做的結果是我們指定了y坐標的范圍為屏幕底部(0.0f)至屏幕頂部(600.0f)。這意味著現在點(0.0, 0.0)對應左下角(譯注:而不再是窗口正中間)。

最后要做的事是創建一個VBOVAO用來渲染四邊形。現在我們在初始化VBO時分配足夠的內存,這樣我們可以在渲染字符的時候再來更新VBO的內存。

GLuint VAO, VBO;
glGenVertexArrays(1, &VAO);
glGenBuffers(1, &VBO);
glBindVertexArray(VAO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(GLfloat) * 6 * 4, NULL, GL_DYNAMIC_DRAW);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 4 * sizeof(GLfloat), 0);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindVertexArray(0);      

每個2D四邊形需要6個頂點,每個頂點又是由一個4float向量(譯注:一個紋理坐標和一個頂點坐標)組成,因此我們將VBO的內存分配為6 * 4個float的大小。由于我們會在繪制字符時經常更新VBO的內存,所以我們將內存類型設置為GL_DYNAMIC_DRAW

2. 渲染一行文本

要渲染一個字符,我們從之前創建的Characters映射表中取出對應的Character結構體,并根據字符的度量值來計算四邊形的維度。根據四邊形的維度我們就能動態計算出6個描述四邊形的頂點,并使用glBufferSubData函數更新VBO所管理內存的內容。

我們創建一個叫做RenderText的函數渲染一個字符串:

void RenderText(Shader &s, std::string text, GLfloat x, GLfloat y, GLfloat scale, glm::vec3 color)
{
    // 激活對應的渲染狀態
    s.Use();
    glUniform3f(glGetUniformLocation(s.Program, "textColor"), color.x, color.y, color.z);
    glActiveTexture(GL_TEXTURE0);
    glBindVertexArray(VAO);

    // 遍歷文本中所有的字符
    std::string::const_iterator c;
    for (c = text.begin(); c != text.end(); c++)
    {
        Character ch = Characters[*c];

        GLfloat xpos = x + ch.Bearing.x * scale;
        GLfloat ypos = y - (ch.Size.y - ch.Bearing.y) * scale;

        GLfloat w = ch.Size.x * scale;
        GLfloat h = ch.Size.y * scale;
        // 對每個字符更新VBO
        GLfloat vertices[6][4] = {
            { xpos,     ypos + h,   0.0, 0.0 },            
            { xpos,     ypos,       0.0, 1.0 },
            { xpos + w, ypos,       1.0, 1.0 },

            { xpos,     ypos + h,   0.0, 0.0 },
            { xpos + w, ypos,       1.0, 1.0 },
            { xpos + w, ypos + h,   1.0, 0.0 }           
        };
        // 在四邊形上繪制字形紋理
        glBindTexture(GL_TEXTURE_2D, ch.textureID);
        // 更新VBO內存的內容
        glBindBuffer(GL_ARRAY_BUFFER, VBO);
        glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(vertices), vertices); 
        glBindBuffer(GL_ARRAY_BUFFER, 0);
        // 繪制四邊形
        glDrawArrays(GL_TRIANGLES, 0, 6);
        // 更新位置到下一個字形的原點,注意單位是1/64像素
        x += (ch.Advance >> 6) * scale; // 位偏移6個單位來獲取單位為像素的值 (2^6 = 64)
    }
    glBindVertexArray(0);
    glBindTexture(GL_TEXTURE_2D, 0);
}

這個函數的內容應該非常明顯了:我們首先計算出四邊形的原點坐標(為xposypos)和它的大小(為w和h),并生成6個頂點形成這個2D四邊形;注意我們將每個度量值都使用scale進行縮放。接下來我們更新了VBO的內容、并渲染了這個四邊形。

其中這行代碼需要加倍留意:

GLfloat ypos = y - (ch.Size.y - ch.Bearing.y);   

一些字符(如’p’或’q’)需要被渲染到基準線以下,因此字形四邊形也應該被擺放在RenderText的y值以下。ypos的偏移量可以從字形的度量值中得出:

要計算這段距離,即偏移量,我們需要找出字形在基準線之下延展出去的距離。在上圖中這段距離用紅色箭頭標出。從度量值中可以看到,我們可以通過用字形的高度減去bearingY來計算這段向量的長度。對于那些正好位于基準線上的字符(如’X’),這個值正好是0.0。而對于那些超出基準線的字符(如’g’或’j’),這個值則是正的。

如果你每件事都做對了,那么你現在已經可以使用下面的語句成功渲染字符串了:

RenderText(shader, "This is sample text", 25.0f, 25.0f, 1.0f, glm::vec3(0.5, 0.8f, 0.2f));
RenderText(shader, "(C) LearnOpenGL.com", 540.0f, 570.0f, 0.5f, glm::vec3(0.3, 0.7f, 0.9f));

渲染效果看上去像這樣:

你可以從這里獲取這個例子的源代碼。

// Std. Includes
#include <iostream>
#include <map>
#include <string>
// GLEW
#define GLEW_STATIC
#include <GL/glew.h>
// GLFW
#include <GLFW/glfw3.h>
// GLM
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
// FreeType
#include <ft2build.h>
#include FT_FREETYPE_H
// GL includes
#include "Shader.h"

// Properties
const GLuint WIDTH = 800, HEIGHT = 600;

/// Holds all state information relevant to a character as loaded using FreeType
struct Character {
    GLuint TextureID;   // ID handle of the glyph texture
    glm::ivec2 Size;    // Size of glyph
    glm::ivec2 Bearing;  // Offset from baseline to left/top of glyph
    GLuint Advance;    // Horizontal offset to advance to next glyph
};

std::map<GLchar, Character> Characters;
GLuint VAO, VBO;

void RenderText(Shader &shader, std::string text, GLfloat x, GLfloat y, GLfloat scale, glm::vec3 color);

// The MAIN function, from here we start our application and run the Game loop
int main()
{
    // Init GLFW
    glfwInit();
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
    glfwWindowHint(GLFW_RESIZABLE, GL_FALSE);

    GLFWwindow* window = glfwCreateWindow(WIDTH, HEIGHT, "LearnOpenGL", nullptr, nullptr); // Windowed
    glfwMakeContextCurrent(window);

    // Initialize GLEW to setup the OpenGL Function pointers
    glewExperimental = GL_TRUE;
    glewInit();

    // Define the viewport dimensions
    glViewport(0, 0, WIDTH, HEIGHT);

    // Set OpenGL options
    glEnable(GL_CULL_FACE);
    glEnable(GL_BLEND);
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

    // Compile and setup the shader
    Shader shader("shaders/text.vs", "shaders/text.frag");
    glm::mat4 projection = glm::ortho(0.0f, static_cast<GLfloat>(WIDTH), 0.0f, static_cast<GLfloat>(HEIGHT));
    shader.Use();
    glUniformMatrix4fv(glGetUniformLocation(shader.Program, "projection"), 1, GL_FALSE, glm::value_ptr(projection));

    // FreeType
    FT_Library ft;
    // All functions return a value different than 0 whenever an error occurred
    if (FT_Init_FreeType(&ft))
        std::cout << "ERROR::FREETYPE: Could not init FreeType Library" << std::endl;

    // Load font as face
    FT_Face face;
    if (FT_New_Face(ft, "fonts/arial.ttf", 0, &face))
        std::cout << "ERROR::FREETYPE: Failed to load font" << std::endl;

    // Set size to load glyphs as
    FT_Set_Pixel_Sizes(face, 0, 48);

    // Disable byte-alignment restriction
    glPixelStorei(GL_UNPACK_ALIGNMENT, 1); 

    // Load first 128 characters of ASCII set
    for (GLubyte c = 0; c < 128; c++)
    {
        // Load character glyph 
        if (FT_Load_Char(face, c, FT_LOAD_RENDER))
        {
            std::cout << "ERROR::FREETYTPE: Failed to load Glyph" << std::endl;
            continue;
        }
        // Generate texture
        GLuint texture;
        glGenTextures(1, &texture);
        glBindTexture(GL_TEXTURE_2D, texture);
        glTexImage2D(
            GL_TEXTURE_2D,
            0,
            GL_RED,
            face->glyph->bitmap.width,
            face->glyph->bitmap.rows,
            0,
            GL_RED,
            GL_UNSIGNED_BYTE,
            face->glyph->bitmap.buffer
        );
        // Set texture options
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
        // Now store character for later use
        Character character = {
            texture,
            glm::ivec2(face->glyph->bitmap.width, face->glyph->bitmap.rows),
            glm::ivec2(face->glyph->bitmap_left, face->glyph->bitmap_top),
            face->glyph->advance.x
        };
        Characters.insert(std::pair<GLchar, Character>(c, character));
    }
    glBindTexture(GL_TEXTURE_2D, 0);
    // Destroy FreeType once we're finished
    FT_Done_Face(face);
    FT_Done_FreeType(ft);

    
    // Configure VAO/VBO for texture quads
    glGenVertexArrays(1, &VAO);
    glGenBuffers(1, &VBO);
    glBindVertexArray(VAO);
    glBindBuffer(GL_ARRAY_BUFFER, VBO);
    glBufferData(GL_ARRAY_BUFFER, sizeof(GLfloat) * 6 * 4, NULL, GL_DYNAMIC_DRAW);
    glEnableVertexAttribArray(0);
    glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 4 * sizeof(GLfloat), 0);
    glBindBuffer(GL_ARRAY_BUFFER, 0);
    glBindVertexArray(0);

    // Game loop
    while (!glfwWindowShouldClose(window))
    {
        // Check and call events
        glfwPollEvents();

        // Clear the colorbuffer
        glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT);

        RenderText(shader, "This is sample text", 25.0f, 25.0f, 1.0f, glm::vec3(0.5, 0.8f, 0.2f));
        RenderText(shader, "(C) LearnOpenGL.com", 540.0f, 570.0f, 0.5f, glm::vec3(0.3, 0.7f, 0.9f));
       
        // Swap the buffers
        glfwSwapBuffers(window);
    }

    glfwTerminate();
    return 0;
}

void RenderText(Shader &shader, std::string text, GLfloat x, GLfloat y, GLfloat scale, glm::vec3 color)
{
    // Activate corresponding render state  
    shader.Use();
    glUniform3f(glGetUniformLocation(shader.Program, "textColor"), color.x, color.y, color.z);
    glActiveTexture(GL_TEXTURE0);
    glBindVertexArray(VAO);

    // Iterate through all characters
    std::string::const_iterator c;
    for (c = text.begin(); c != text.end(); c++) 
    {
        Character ch = Characters[*c];

        GLfloat xpos = x + ch.Bearing.x * scale;
        GLfloat ypos = y - (ch.Size.y - ch.Bearing.y) * scale;

        GLfloat w = ch.Size.x * scale;
        GLfloat h = ch.Size.y * scale;
        // Update VBO for each character
        GLfloat vertices[6][4] = {
            { xpos,     ypos + h,   0.0, 0.0 },            
            { xpos,     ypos,       0.0, 1.0 },
            { xpos + w, ypos,       1.0, 1.0 },

            { xpos,     ypos + h,   0.0, 0.0 },
            { xpos + w, ypos,       1.0, 1.0 },
            { xpos + w, ypos + h,   1.0, 0.0 }           
        };
        // Render glyph texture over quad
        glBindTexture(GL_TEXTURE_2D, ch.TextureID);
        // Update content of VBO memory
        glBindBuffer(GL_ARRAY_BUFFER, VBO);
        glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(vertices), vertices); // Be sure to use glBufferSubData and not glBufferData

        glBindBuffer(GL_ARRAY_BUFFER, 0);
        // Render quad
        glDrawArrays(GL_TRIANGLES, 0, 6);
        // Now advance cursors for next glyph (note that advance is number of 1/64 pixels)
        x += (ch.Advance >> 6) * scale; // Bitshift by 6 to get value in pixels (2^6 = 64 (divide amount of 1/64th pixels by 64 to get amount of pixels))
    }
    glBindVertexArray(0);
    glBindTexture(GL_TEXTURE_2D, 0);
}

為了讓你更好理解我們是怎么計算四邊形頂點的,我們可以關閉混合來看看真正渲染出來的四邊形是什么樣子的:

可以看到,大部分的四邊形都位于一條(想象中的)基準線上,而對應’p’或’(‘字形的四邊形則稍微向下偏移了一些。


更進一步

本教程演示了如何使用FreeType庫繪制TrueType文本。這種方式靈活、可縮放并支持多種字符編碼。然而,由于我們對每一個字形都生成并渲染了紋理,你的應用程序可能并不需要這么強大的功能。性能更好的位圖字體也許是更可取的,因為對所有的字形我們只需要一個紋理。當然,最好的方式是結合這兩種方式,動態生成包含所有字符字形的位圖字體紋理,并用FreeType加載。這為渲染器節省了大量紋理切換的開銷,并且根據字形的排列緊密程度也可以節省很多的性能開銷。

另一個使用FreeType字體的問題是字形紋理是儲存為一個固定的字體大小的,因此直接對其放大就會出現鋸齒邊緣。此外,對字形進行旋轉還會使它們看上去變得模糊。這個問題可以通過儲存每個像素距最近的字形輪廓的距離,而不是光柵化的像素顏色,來緩解。這項技術被稱為有向距離場(Signed Distance Fields),Valve在幾年前發表過一篇了論文,討論了他們通過這項技術來獲得非常棒的3D渲染效果。

后記

本篇已結束,后面更精彩~~~~

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,316評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,481評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,241評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,939評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,697評論 6 409
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,182評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,247評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,406評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,933評論 1 334
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,772評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,973評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,516評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,209評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,638評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,866評論 1 285
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,644評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,953評論 2 373

推薦閱讀更多精彩內容