位图字体渲染问题

时间:2015-12-01 05:56:16

标签: 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

指定缓冲区数据

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

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

感谢任何帮助。

1 个答案:

答案 0 :(得分:1)

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

但我确实找到了这些:

u32 Size = NumChars * 12;

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

但你的位置和texcoords是float。每个浮点数4个字节*每个位置2个浮点数*每个字符6个位置=每个字符48个字节。

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

glVertexAttribPointer(0, 2, GL_FLOAT, 0, 0, 0);
glVertexAttribPointer(1, 2, GL_FLOAT, 0, 0, 0);

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

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

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