c - 位图字体渲染问题

标签 c opengl fonts

在这里学习 OpenGL。尝试编写一个位图字体渲染系统。

我正在使用Hiero生成字体文件(Angel Code 字体格式)和图集(.fnt.png 文件)。

我首先解析字体文件中读取的字体和字符数据。那部分很容易。 (经验证,解析结果确实全部正确)

// (typedefs) u32: unsigned int, r32: float, string: char*

struct font_character
{
    u32 Id;
    r32 X, Y;
    r32 Width, Height;
    r32 XOffset, YOffset;
    r32 XAdvance;
};

struct font_set
{
    string Name;
    u32 Atlas;
    r32 Size;
    u32 Stretch;
    u32 Smooth;
    u32 AntiAliasing;
    u32 Padding[4];
    u32 Spacing[2];
    u32 LineHeight;
    u32 BaseLine;
    r32 Width, Height;
    u32 CharacterCount;
    font_character Characters[128];
};

// Parsing related codes...

font_set FontLoad(string FilePath)
{
    font_set Result = {};

    string Content = ReadFile(FilePath);
    if (Content)
    {
        List(string) FontSettings;
        ListAlloc(FontSettings, 1024);

        bool DoneParsing = false;
        while(!DoneParsing)
        {
            token Token = GetToken(Content);
            switch(Token.Type)
            {
                case TokenType_EOF:
                    DoneParsing = true;
                    break;

                case TokenType_Unknown:
                    Assert(!"Unknown token in font file");
                    break;

                case TokenType_Number:
                case TokenType_String:
                case TokenType_Identifier:
                    ListPush(FontSettings, Token.Content);
                    break;
            }
        }

        for (int i = 0, Count = ListCount(FontSettings); i < Count; i += 2)
        {
            string SettingKey = FontSettings[i];
            string SettingValue = FontSettings[i + 1];

            if (StringEqual(SettingKey, "face"))
                Result.Name = SettingValue;
            else if (StringEqual(SettingKey, "size"))
                Result.Size = atoi(SettingValue);
            else if (StringEqual(SettingKey, "stretchH"))
                Result.Stretch = atoi(SettingValue);
            else if (StringEqual(SettingKey, "smooth"))
                Result.Smooth = atoi(SettingValue);
            else if (StringEqual(SettingKey, "aa"))
                Result.AntiAliasing = atoi(SettingValue);
            else if (StringEqual(SettingKey, "lineHeight"))
                Result.LineHeight = atoi(SettingValue);
            else if (StringEqual(SettingKey, "base"))
                Result.BaseLine = atoi(SettingValue);
            else if (StringEqual(SettingKey, "scaleW"))
                Result.Width = atoi(SettingValue);
            else if (StringEqual(SettingKey, "scaleH"))
                Result.Height = atoi(SettingValue);
            else if (StringEqual(SettingKey, "spacing"))
            {
                // Ascii(48) = Decimal(0)
                Result.Spacing[0] = SettingValue[0] - 48;
                Result.Spacing[1] = SettingValue[2] - 48;
            }
            else if (StringEqual(SettingKey, "padding"))
            {
                Result.Padding[0] = SettingValue[0] - 48;
                Result.Padding[1] = SettingValue[2] - 48;
                Result.Padding[2] = SettingValue[4] - 48;
                Result.Padding[3] = SettingValue[6] - 48;
            }
            else if (StringEqual(SettingKey, "char"))
            {
                font_character Character;

                // Although they're 10 pairs of data, we're gonna skip the last two cause we don't care about them
                For(u32, PairIndex, 8)
                {
                    string CharKey = FontSettings[(i + 1) + PairIndex * 2];
                    string CharValue = FontSettings[(i + 2) + PairIndex * 2];

                    if (StringEqual(CharKey, "id"))
                        Character.Id = atoi(CharValue);
                    else if (StringEqual(CharKey, "x"))
                        Character.X = atoi(CharValue);
                    else if (StringEqual(CharKey, "y"))
                        Character.Y = atoi(CharValue);
                    else if (StringEqual(CharKey, "width"))
                        Character.Width = atoi(CharValue);
                    else if (StringEqual(CharKey, "height"))
                        Character.Height = atoi(CharValue);
                    else if (StringEqual(CharKey, "xoffset"))
                        Character.XOffset = atoi(CharValue);
                    else if (StringEqual(CharKey, "yoffset"))
                        Character.YOffset = atoi(CharValue);
                    else if (StringEqual(CharKey, "xadvance"))
                        Character.XAdvance = atoi(CharValue);
                }

                Result.Characters[Result.CharacterCount++] = Character;
                i += 19;
            }
            else i--;
        }
    }

    // Load texture
    char TexturePath[256];
    sprintf(TexturePath, "%s.png", FilePath);
    Result.Atlas = TextureLoad(TexturePath); // loads texture from file via stbi_load, does glGenTexture, configures texture parameters etc.

    return (Result);
}

然后我们开始渲染,这就是我有点挣扎的地方。我的理解是,我需要使用从字体数据中获得的字符数据来构建可以渲染到屏幕上的四边形。

首先,这是我的着色器。顶点着色器:

#version 330 core

layout (location = 0) in vec2 VertPos;
layout (location = 1) in vec2 VertUV;

out vec2 FragUV;

uniform mat4 Projection;

void main()
{
    gl_Position = Projection * vec4(VertPos, 0, 1);
    FragUV = VertUV;
}

和片段着色器:

#version 330 core

out vec4 FinalColor;
in vec2 FragUV;

uniform sampler2D FontAtlas;
uniform vec3 Color;

void main()
{
    FinalColor = vec4(Color, texture(FontAtlas, FragUV).a);
}

这是我加载字体和渲染的方法:

struct font_renderer
{
    string Text;
    v3 Color;
    r32 CurrentX;
    u32 VAO, VBO;
    u32 Initialized;
    u32 Shader;
};

// In an initialization function
font_set Font = FontLoad("res/fonts/Courier New.fnt");
u32 FontShader = ShaderLoadFromFile("Font.vert", "Font.frag");

// In an update/render function
font_renderer FontRenderer = {};
FontRenderer.Text = "A";
FontRenderer.Color = V3(1, 0, 0);
FontRenderer.CurrentX = 20;
FontRenderer.Shader = FontShader;

FontRender(&FontRenderer, &Font);

渲染功能(只是想在屏幕上显示一些东西)

void FontRender(font_renderer *Renderer, font_set *Font)
{
    u32 NumChars = StringLength(Renderer->Text);

    u32 Size = NumChars * 12;

    if (!Renderer->Initialized)
    {
        glGenBuffers(1, &Renderer->VBO);
        glBindBuffer(GL_ARRAY_BUFFER, Renderer->VBO);
        glBufferData(GL_ARRAY_BUFFER, Size * 2, 0, GL_STATIC_DRAW);

        glGenVertexArrays(1, &Renderer->VAO);
        glBindVertexArray(Renderer->VAO);
        glEnableVertexAttribArray(0);
        glVertexAttribPointer(0, 2, GL_FLOAT, 0, 0, 0);
        glEnableVertexAttribArray(1);
        glVertexAttribPointer(1, 2, GL_FLOAT, 0, 0, 0);

        glBindBuffer(GL_ARRAY_BUFFER, 0);
        glBindVertexArray(0);

        Renderer->Initialized = 1;
    }

    /*r32 *VertPos = Calloc(Size, r32);
    r32 *VertUV = Calloc(Size, r32);*/

    // Temporary code, just trying to render a single character on screen
    r32 VertPos[12]; // we need a quad <=> 2 triangles <=> 6 vertices <=> 12 floats
    r32 VertUV[12]; // same for UVs

    For(u32, i, NumChars) // for loop macro
    {
        font_character Character = Font->Characters[Renderer->Text[i]]; // assuming that 'Characters' are ordered correctly
        r32 X = Character.X;
        r32 Y = Character.Y;
        r32 XOffset = Character.XOffset;
        r32 YOffset = Character.YOffset;
        r32 XAdvance = Character.XAdvance;
        r32 Width = Character.Width;
        r32 Height = Character.Height;

        // Triangle 1 (clock-wise winding order)
        {
            // Top Left
            VertPos[i] = Renderer->CurrentX + XOffset;
            VertPos[i + 1] = YOffset;

            // Bottom Left
            VertPos[i + 2] = Renderer->CurrentX + XOffset;
            VertPos[i + 3] = YOffset + Height;

            // Bottom Right
            VertPos[i + 4] = Renderer->CurrentX + XOffset + Width;
            VertPos[i + 5] = YOffset + Height;
        }
        // Triangle 2
        {
            // Bottom Right
            VertPos[i + 6] = VertPos[i + 4];
            VertPos[i + 7] = VertPos[i + 5];

            // Top Right
            VertPos[i + 8] = Renderer->CurrentX + XOffset + Width;
            VertPos[i + 9] = YOffset;

            // Top Left
            VertPos[i + 10] = VertPos[i];
            VertPos[i + 11] = VertPos[i + 1];
        }

        // UV 1
        {
            // Top left
            VertUV[i] = X / Font->Width;
            VertUV[i + 1] = Y / Font->Height;

            // Bottom left
            VertUV[i + 2] = X / Font->Width;
            VertUV[i + 3] = (Y + Height) / Font->Height;

            // Bottom right
            VertUV[i + 4] = (X + Width) / Font->Width;
            VertUV[i + 5] = (Y + Height) / Font->Height;
        }

        // UV 2
        {
            // Bottom right
            VertUV[i + 6] = VertUV[i + 4];
            VertUV[i + 7] = VertUV[i + 5];

            // Top right
            VertUV[i + 8] = (X + Width) / Font->Width;
            VertUV[i + 9] = Y / Font->Height;

            // Top left
            VertUV[i + 10] = VertUV[i];
            VertUV[i + 11] = VertUV[i + 1];
        }
    }

    glBindBuffer(GL_ARRAY_BUFFER, Renderer->VBO);
    u32 Offset = 0;
    glBufferSubData(GL_ARRAY_BUFFER, Offset, Size, VertPos);
    Offset += Size;
    glBufferSubData(GL_ARRAY_BUFFER, Offset, Size, VertUV);

    m4 FontProjection = Orthographic(0, 800, 600, 0, -1, +1);

    glDisable(GL_DEPTH_TEST);
    ShaderUse(Renderer->Shader);
    glBindVertexArray(Renderer->VAO);
    TextureBind(Font->Atlas);
    ShaderSetV3(Renderer->Shader, "Color", Renderer->Color);
    ShaderSetM4(Renderer->Shader, "Projection", &FontProjection);
    glDrawArrays(GL_TRIANGLES, 0, NumChars * 6);

    glBindBuffer(GL_ARRAY_BUFFER, 0);
    glBindVertexArray(0);
}

正如你所看到的,我实际上只是想在屏幕上显示一个角色。我什至没有考虑字体大小等,只是保持简单。我选择从 TopLeft->BottomLeft->BottomRight->TopRight->TopLeft 开始的顶点,如果我错了,请纠正我,但它是顺时针缠绕顺序。我的顶点着色器中有两个插槽:位置和 UV。我通过调用 glBufferSubData

指定缓冲区数据

运行程序我没有得到任何渲染输出。只是空白屏幕。很确定我错过了一些明显的事情或做了一些愚蠢的事情,我看不到它。我做错了什么?

请注意,纹理、字体数据和着色器均已正确加载。

感谢任何帮助。

最佳答案

您的代码似乎存在许多问题(甚至忽略您采用的可怕的 C-in-C++ 编码风格)。我还没有找到所有这些,所以我不能说我找到了你遇到的那个。

但我确实找到了这些:

<小时/>
u32 Size = NumChars * 12;

您使用Size * 2作为缓冲区的大小。所以字符串中的每个字符占用 24 个字节; 12 代表你的位置,12 代表你的纹理坐标。

但是你的位置和纹理坐标是float。每个 float 4 个字节 * 每个位置 2 个 float * 每个字符 6 个位置 = 每个字符 48 个字节。

所以你的尺寸计算非常错误。您可以通过检查 sizeof(VertPos) 并将其与您的 Size 变量进行比较来验证这一点。你会发现它们非常不同。

<小时/>
glVertexAttribPointer(0, 2, GL_FLOAT, 0, 0, 0);
glVertexAttribPointer(1, 2, GL_FLOAT, 0, 0, 0);

这表示您的位置和纹理坐标都具有相同的偏移量。这意味着着色器中的位置和纹理坐标将获得相同的值。我很确定这不是您想要的。

您的纹理坐标数组来自缓冲区的不同部分。所以你需要应用适当的偏移量。假设您已经正确计算了 Size,则如下所示:

glVertexAttribPointer(1, 2, GL_FLOAT, 0, 0, reinterpret_cast<void*>(Size));

关于c - 位图字体渲染问题,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/34013584/

相关文章:

c - 向后递增数组中的值

opengl - 还有什么其他方法可以绘制对象的轮廓?

安卓 : how to use Tamil font

html - 为什么我的字体颜色设置不起作用?

c - ARMv5 汇编中的 PUSH/POP

c - K&R 中边界有限的 qsort |排序过程不会在字符串中的下一个位置继续

algorithm - 通过大量球体进行光线拾取的最佳算法

swift - 如何在 subview 中保留控制文本颜色的功能?

c++ - "excess elements in scalar initializer"使用 igraph C 库生成具有幂律度分布的网络时

c++ - 我怎样才能使用 GLU_RGBA 或其他 GLU_ 参数?