基於OpenGL編寫一個簡易的2D渲染框架-05 渲染文本
閱讀文章前需要了解的知識:文本渲染 https://learnopengl-cn.github.io/06%20In%20Practice/02%20Text%20Rendering/
簡要步驟:
獲取要繪制的字符的 Unicode 碼,使用 FreeType 庫獲取對應的位圖數據,添加到字符表中(後面同樣的字符可以再表中直接索引),將字符表上的字符填充到一張紋理上。計算每個字符的紋理坐標,使用渲染器繪制
註意的問題:
對於中英文混合的字符串,使用 char 存儲時,英文字符占 1 個字節,而中文字符占 2 個字符。必須轉換為寬字符,即中英文字符都占 2 個字節。
void TextRender::toWchar(wchar_t* dest, constchar* src, int size) { const char* old_local = setlocale(LC_CTYPE, "chs"); mbstowcs(dest, src, size); setlocale(LC_CTYPE, old_local); }
通過上面的函數,可以將 char 轉為 wchar_t。
添加 FreeType 庫到工程
註意添加新的包含路徑就好了,我把 External 目錄也設置為包含路徑,否則使用 FreeType 庫會發生錯誤
渲染文本
首先,對 FreeType 庫初始化
size = 48; /* 初始化 FreeType 庫 */ assert(FT_Init_FreeType(&ftLibrary) == 0); /* 加載字體 */ assert(FT_New_Face(ftLibrary, PathHelper::fullPath("Font/msyh.ttc").c_str(), 0, &ftFace) == 0); /* 設定為 UNICODE,默認也是 */ FT_Select_Charmap(ftFace, FT_ENCODING_UNICODE);/* 定義字體大小 */ FT_Set_Pixel_Sizes(ftFace, size, size);
字體在 Font 文件夾中,為 微軟雅黑
定義一個字符結構
struct Character { Vec2 texcoord[4]; /* 紋理坐標 */ Vec2 size; /* 字型大小 */ Vec2 bearing; /* 從基準線到字形左部/頂部的偏移值 */ int advance; /* 原點距下一個字形原點的距離 */ bool space; std::vector<unsigned char> data; };
包含渲染字符所需要的數據,對於空白字符(沒有位圖數據,只有到下一個字符的偏移量)需要特別處理。 data 存儲位圖數據。
上面只是繪制一個字符的數據,對於一串字符,還需要定義一個文本結構
struct Text { Vec2 pos; float scale; Color color; std::vector<Character*> vCharacters; };
包含繪制文本的坐標,縮放比,顏色以及字符的索引。
創建一個新類 TextRender 使用渲染文本
調用函數 DrawText 直接繪制文本
void TextRender::drawText(int x, int y, float scale, const std::string& text, Color& color) { static wchar_t buffer[2048]; this->toWchar(buffer, text.c_str(), text.size()); int count = 0; for ( int i = 0; i < 2048; i++ ) { if ( buffer[i] == 0 ) break; count++; } Text t = { Vec2(x, y), scale, color }; Character* character = nullptr; for ( int i = 0; i < count; i++ ) { int index = buffer[i]; auto it = characterMap.find(index); if ( it == characterMap.end() ) { character = new Character(); character->space = (index == ‘ ‘ ); this->loadCharacter(character, index); characterMap.insert(CharacterMap::value_type(index, character)); } else { character = it->second; } t.vCharacters.push_back(character); } vTexts.push_back(t); }
遍歷所有要繪制的字符,查找字符表,如果字符表存則返回字符。最後把所有字符存儲到 Text 中,而 Text 存儲在一個數組中(數組存儲一幀需要繪制的字符串)。
如果字符表中沒有該字符,則加載該字符
void TextRender::loadCharacter(Character* character, unsigned long id) { if ( bUpdateTexture == false ) bUpdateTexture = true; FT_Load_Char(ftFace, id, FT_LOAD_RENDER); /* 填充結構數據 */ character->size.set(ftFace->glyph->bitmap.width, ftFace->glyph->bitmap.rows); character->bearing.set(ftFace->glyph->bitmap_left, ftFace->glyph->bitmap_top); character->advance = ftFace->glyph->advance.x; if ( character->space ) return; /* 儲存位圖數據 */ character->data.resize(character->size.x * character->size.y); memcpy(&character->data[0], ftFace->glyph->bitmap.buffer, character->data.size()); }
函數中使用 FreeType 庫加載字符位圖數據,填充繪制字符的數據信息。一旦這個函數被調用,證明字符表需要添加新的字符了,在渲染文本前需要更新紋理以及紋理坐標。這裏使用 bool 值的 bUpdateTexture 標誌,待會要更新紋理。
更新紋理的函數
void TextRender::updateTextTexture() { glPixelStorei(GL_UNPACK_ALIGNMENT, 1); if ( texture.texture != -1 ) { glDeleteTextures(1, &texture.texture); } glGenTextures(1, &texture.texture); glBindTexture(GL_TEXTURE_2D, texture.texture); int count = characterMap.size(); int col, row; if ( count < nRowCharCount ) { col = count; row = 1; } else { col = nRowCharCount; row = ceilf(float(count) / col); } textureData.resize(row * col * size * size); for ( auto &ele : textureData ) ele = 0; int tex_size_w = col * size; int tex_size_h = row * size; /* 合並所有字符的位圖數據 */ int characterCount = 0; for ( auto it = characterMap.begin(); it != characterMap.end(); ++it ) { this->copyCharacterData(characterCount / col, characterCount % col, size * col, it->second, tex_size_w, tex_size_h); characterCount++; } /* 設置紋理數據 */ glTexImage2D(GL_TEXTURE_2D, 0, GL_RED, col * size, row * size, 0, GL_RED, GL_UNSIGNED_BYTE, &textureData[0]); /* 設置紋理選項 */ 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); /* 解綁紋理 */ glBindTexture(GL_TEXTURE_2D, 0); }
函數中的重點是如何把字符表中字符的位圖數據合並的一張紋理上。方法是創建一張足夠大的紋理,再將紋理切分為 M x N 的塊
然後將字符表中字符的位圖數據拷貝到對應的格子上,計算字符的紋理坐標
void TextRender::copyCharacterData(int row, int col, int stride, Character* character, float sizew, float sizeh) { int w = character->size.x; int h = character->size.y; int index = 0; if ( character->space ) return; for ( int i = 0; i < h; i++ ) { for ( int j = 0; j < w; j++ ) { index = (row * size + i) * stride + (col * size + j); textureData[index] = character->data[i * w + j]; } } character->texcoord[0].set(float(col * size + 0) / sizew, float(row * size + h) / sizeh); character->texcoord[1].set(float(col * size + 0) / sizew, float(row * size + 0) / sizeh); character->texcoord[2].set(float(col * size + w) / sizew, float(row * size + 0) / sizeh); character->texcoord[3].set(float(col * size + w) / sizew, float(row * size + h) / sizeh); }
最後就是渲染了
void TextRender::render() { /* 更新紋理 */ if ( bUpdateTexture ) { bUpdateTexture = false; this->updateTextTexture(); } /* 獲取頂點數據 */ for ( auto& ele : vTexts ) { GLfloat x = ele.pos.x; GLfloat y = ele.pos.y; int positionCount = ele.vCharacters.size() * 4; int indexCount = ele.vCharacters.size() * 6; if ( vPositions.size() < positionCount ) { vPositions.resize(positionCount); vTexCoords.resize(positionCount); vIndices.resize(indexCount); } nPositionIndex = nIndexIndex = 0; int beginIndex = 0; for ( auto& character : ele.vCharacters ) { GLfloat xpos = x + character->bearing.x * ele.scale; GLfloat ypos = y - (character->size.y - character->bearing.y) * ele.scale; x += (character->advance >> 6) * ele.scale; if ( character->space ) continue; GLfloat w = character->size.x * ele.scale; GLfloat h = character->size.y * ele.scale; vPositions[nPositionIndex + 0].set(xpos + 0, ypos + 0, 0); vPositions[nPositionIndex + 1].set(xpos + 0, ypos + h, 0); vPositions[nPositionIndex + 2].set(xpos + w, ypos + h, 0); vPositions[nPositionIndex + 3].set(xpos + w, ypos + 0, 0); vTexCoords[nPositionIndex + 0] = (character->texcoord[0]); vTexCoords[nPositionIndex + 1] = (character->texcoord[1]); vTexCoords[nPositionIndex + 2] = (character->texcoord[2]); vTexCoords[nPositionIndex + 3] = (character->texcoord[3]); vIndices[nIndexIndex + 0] = (4 * beginIndex + 0); vIndices[nIndexIndex + 1] = (4 * beginIndex + 2); vIndices[nIndexIndex + 2] = (4 * beginIndex + 1); vIndices[nIndexIndex + 3] = (4 * beginIndex + 0); vIndices[nIndexIndex + 4] = (4 * beginIndex + 3); vIndices[nIndexIndex + 5] = (4 * beginIndex + 2); beginIndex++; nPositionIndex += 4; nIndexIndex += 6; } static RenderUnit unit; unit.pPositions = &vPositions[0]; unit.nPositionCount = nPositionIndex; unit.pTexcoords = &vTexCoords[0]; unit.pIndices = &vIndices[0]; unit.nIndexCount = nIndexIndex; unit.color = ele.color; unit.texture = &texture; unit.renderType = RENDER_TYPE_TEXTURE; pRenderer->pushRenderUnit(unit); nPositionIndex = nIndexIndex = 0; } vTexts.clear(); }
在主函數繪制文本
textRender.drawText(20, 80, 0.8, "基於 OpenGL 編寫簡易遊戲框架 Simple2D", Color(0, 0, 1, 1)); textRender.drawText(20, 160, 0.8, "基於 OpenGL 編寫簡易遊戲框架 Simple2D", Color(0, 1, 0, 1)); textRender.drawText(20, 240, 0.8, "基於 OpenGL 編寫簡易遊戲框架 Simple2D", Color(0, 1, 1, 1)); textRender.drawText(20, 320, 0.8, "基於 OpenGL 編寫簡易遊戲框架 Simple2D", Color(1, 0, 0, 1)); textRender.drawText(20, 400, 0.8, "基於 OpenGL 編寫簡易遊戲框架 Simple2D", Color(1, 0, 1, 1)); textRender.drawText(20, 480, 0.8, "基於 OpenGL 編寫簡易遊戲框架 Simple2D", Color(1, 1, 0, 1)); textRender.drawText(20, 570, 0.45, buffer, Color(0, 0, 0, 1)); textRender.render();
運行程序後的結果
源碼下載:http://pan.baidu.com/s/1skOmP21
基於OpenGL編寫一個簡易的2D渲染框架-05 渲染文本