1. 程式人生 > >基於OpenGL編寫一個簡易的2D渲染框架-05 渲染文本

基於OpenGL編寫一個簡易的2D渲染框架-05 渲染文本

new 坐標 false 證明 ont 獲取 simple 了解 param

閱讀文章前需要了解的知識:文本渲染 https://learnopengl-cn.github.io/06%20In%20Practice/02%20Text%20Rendering/

簡要步驟:

獲取要繪制的字符的 Unicode 碼,使用 FreeType 庫獲取對應的位圖數據,添加到字符表中(後面同樣的字符可以再表中直接索引),將字符表上的字符填充到一張紋理上。計算每個字符的紋理坐標,使用渲染器繪制

註意的問題:

對於中英文混合的字符串,使用 char 存儲時,英文字符占 1 個字節,而中文字符占 2 個字符。必須轉換為寬字符,即中英文字符都占 2 個字節。

    void TextRender::toWchar(wchar_t* dest, const
char* 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 渲染文本