基于opengl编写一个简易的2d渲染框架-05渲染文本

为了邮箱5 为了邮箱5     2022-08-30     174

关键词:

阅读文章前需要了解的知识:文本渲染 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渲染框架-04绘制图片

阅读文章前需要了解的知识,纹理:https://learnopengl-cn.github.io/01%20Getting%20started/06%20Textures/   过程简述:利用FreeImage库加载图像数据,再创建OpenGL纹理,通过Canvas2D画布绘制,最后又Renderer渲染器渲染   本来想用soil库... 查看详情

基于opengl编写一个简易的2d渲染框架-09重构渲染器-shader

  Shader只是进行一些简单的封装,主要功能:    1、编译着色程序    2、绑定Uniform数据    3、根据着色程序的顶点属性传递顶点数据到GPU   着色程序的编译GLuintShader::createShaderProgram(constchar*vsname,constchar*p... 查看详情

基于opengl编写一个简易的2d渲染框架02——搭建opengl环境

由于没有使用GLFW库,接下来得费一番功夫。阅读这篇文章前请看一下这个网页:https://learnopengl-cn.github.io/01%20Getting%20started/02%20Creating%20a%20window/ 以下,我摘取了一点片段 Windows上的OpenGL库  如果你是Windows平台,opengl32.l... 查看详情

基于opengl编写一个简易的2d渲染框架-13使用例子

 这是重构渲染器的最后一部分了,将会给出一个demo,测试模板测试、裁剪测试、半透明排序等等: 上图是本次demo的效果图,中间的绿色图形展现的是模板测试。 模板测试voidinit(Pass*&p1,Pass*&p2){p1=newPass;p2=newPass;... 查看详情

基于opengl编写一个简易的2d渲染框架-12重构渲染器-blockallocator

 BlockAllocator的内存管理情况可以用下图表示   整体思路是,先分配一大块内存Chunk,然后将Chunk分割成小块Block。由于Block是链表的一个结点,所以可以通过链表的形式把未使用的Block连接起来,并保存到pFreeLists中。当... 查看详情

基于opengl编写一个简易的2d渲染框架-08重构渲染器-整体架构

  事实上,前面编写的渲染器Renderer非常简陋,虽然能够进行一些简单的渲染,但是它并不能满足我们的要求。  当渲染粒子系统时,需要开启混合模式,但渲染其他顶点时却不需要开启混合模式。所以同时渲染粒子系统和... 查看详情

基于opengl编写一个简易的2d渲染框架-11重构渲染器-renderer

 假如要渲染一个纯色矩形在窗口上,应该怎么做?先确定顶点的格式,一个顶点应该包含位置信息vec3以及颜色信息vec4,所以顶点的结构体定义可以这样:structVertex{Vec3position;Vec4color;};然后填充矩形四个顶点是数据信息:Verte... 查看详情

基于opengl编写一个简易的2d渲染框架-07鼠标事件和键盘事件

这次为程序添加鼠标事件和键盘事件   当检测到鼠标事件和键盘事件的信息时,捕获其信息并将信息传送到需要信息的对象处理。为此,需要一个可以分派信息的对象,这个对象能够正确的把信息交到正确的对象。 实... 查看详情

使用 OpenGL 进行像素完美的 2D 渲染

】使用OpenGL进行像素完美的2D渲染【英文标题】:Pixel-Perfect2DRenderingwithOpenGL【发布时间】:2013-01-3016:05:32【问题描述】:是否可以设置OpenGL场景以实现像素完美的渲染和像素位块传输?我注意到通过以下方式设置场景:glMatrixMode... 查看详情

基于opengles的深度学习框架编写

基于OpenGLES的深度学习框架编写背景与工程定位背景项目组基于深度学习实现了视频风格化和人像抠图的功能,但这是在PC/服务端上跑的,现在需要移植到移动端,因此需要一个移动端的深度学习的计算框架。同类型的库caffe-andr... 查看详情

opengl绘制三角形(代码片段)

...对象:ElementBufferObject,EBO或IndexBufferObject,IBO渲染管线在OpenGL中,任何事物都在3D空间中,而屏幕和窗口却是2D像素数组,这导致OpenGL的大部分工作都是关于把3D坐标转变为适应你屏幕的2D像素。3D坐标转为2D坐标的处理过程是由Ope... 查看详情

OpenGL 2D像素完美渲染

】OpenGL2D像素完美渲染【英文标题】:OpenGL2Dpixelperfectrendering【发布时间】:2013-12-2115:04:10【问题描述】:我正在尝试渲染2D图像,以便它完全覆盖整个窗口。对于我的测试,我设置了一个窗口,使客户区正好是320x240,纹理也是... 查看详情

opengl渲染时画面抖动

渲一个大尺寸模型的时候模型的细节部分一直在闪烁。尝试:1.纹理用mipmap,失败。2.开启msaa,失败。3.相机近时不闪,越远闪的越厉害,怀疑是深度争夺,就把远裁剪平面调大,失败。--------------------------------------------------------... 查看详情

OpenGL 2d矩形没有被渲染

】OpenGL2d矩形没有被渲染【英文标题】:OpenGL2drectanglenotbeingrendered【发布时间】:2016-12-2200:07:43【问题描述】:我正在尝试在屏幕上渲染一个矩形。程序运行时,只显示清晰的颜色,没有矩形显示。代码如下:glClearColor(0.0,0.0,0.0,... 查看详情

OpenGL:如何在 3d 模式下优化多层相互重叠的 2d 渲染?

】OpenGL:如何在3d模式下优化多层相互重叠的2d渲染?【英文标题】:OpenGL:Howtooptimize2drenderingwithmultiplelayersoverlappingeachotherin3dmode?【发布时间】:2011-06-0612:36:31【问题描述】:我知道如何通过简单地首先渲染最近的平面来加速3d渲... 查看详情

csharpgl(31)[译]opengl渲染管道那些事

CSharpGL(31)[译]OpenGL渲染管道那些事+BIT祝威+悄悄在此留下版了个权的信息说:开始 自认为对OpenGL的掌握到了一个小瓶颈,现在回头细细地捋一遍OpenGL渲染管道应当是一个不错的突破口。本文通过阅读、翻译和扩展(https://www.op... 查看详情

一步步学opengl(33)-《实例渲染》

教程33实例渲染(GPUInstancing)原文:http://ogldev.atspace.co.uk/www/tutorial33/tutorial33.htmlCSDN完整版专栏:https://blog.csdn.net/cordova/article/category/9266966理论介绍假设我们想渲染一个有一支庞大军队经过的场景,我们要用一个士兵的模型渲出... 查看详情

opengl工作流程

  在OpenGL中,一切事物都在3D空间中,但我们的屏幕坐标确实2D像素数组,OpenGL大部分工作就是把3D坐标转换成适应屏幕的2D像素。3D坐标转换成2D屏幕坐标的过程是有OpenGL的图形渲染管线管理的。图形渲染管线的工作可以被划分... 查看详情