Как правильно обрабатывать рендеринг символов utf-8 размером ›= 2B?

Я хочу отображать символы с размером utf-8 >= 2 байта. Я уже все сделал. Однако есть одна проблема. Когда персонаж нарисован, за ним следует изображение.

Чтобы получить данные глифа, я использую freetype. Это самая минимальная реализация, реальный код содержит кернинг, SDF и т.д.

Что, я думаю, нуждается в объяснении, так это атлас. Метод "TextureAtlas::PackTexture(data, w, h)" упаковывает данные текстуры и возвращает положение, начало координат - верхний левый угол - в пределах диапазона w и h атласа. Таким образом, первый символ имеет начало = [0, 0], а следующий символ с шириной, скажем, 50 будет иметь начало в [50, 0]. Короче говоря.

    enum
    {
        DPI = 72,
        HIGHRES = 64
    };

    struct Glyph
    {
        uint32 codepoint = -1;
        uint32 width = 0; 
        uint32 height = 0;

        Vector2<int> bearing = 0;
        Vector2<float> advance = 0.0f;
        float s0, t0, s1, t1;
    };

    class TextureFont
    {
    public:
        TextureFont() = default;

        bool Initialize();
        void LoadFromFile(const std::string& filePath, float fontSize);

        Glyph* getGlyph(const char8_t* codepoint);
        Glyph* FindGlyph(const char8_t* codepoint);

        uint32 LoadGlyph(const char8_t* codepoint);

        int InitFreeType(float size);

        char* filename;

        vector<Glyph> glyphs;
        TextureAtlas atlas;

        FT_Library library;
        FT_Face face;

        float fontSize = 0.0f;
        float ascender = 0.0f;
        float descender = 0.0f;
        float height = 0.0f;
    };  
int CharFromUtf8(unsigned int* out_char, const char* in_text, const char* in_text_end)
    {
        unsigned int c = (unsigned int)-1;
        const unsigned char* str = (const unsigned char*)in_text;
        if (!(*str & 0x80)) {
            c = (unsigned int)(*str++);
            *out_char = c;
            return 1;
        }
        if ((*str & 0xe0) == 0xc0) {
            *out_char = 0xFFFD;
            if (in_text_end && in_text_end - (const char*)str < 2) return 1;
            if (*str < 0xc2) return 2;
            c = (unsigned int)((*str++ & 0x1f) << 6);
            if ((*str & 0xc0) != 0x80) return 2;
            c += (*str++ & 0x3f);
            *out_char = c;
            return 2;
        }
        if ((*str & 0xf0) == 0xe0) {
            *out_char = 0xFFFD;
            if (in_text_end && in_text_end - (const char*)str < 3) return 1;
            if (*str == 0xe0 && (str[1] < 0xa0 || str[1] > 0xbf)) return 3;
            if (*str == 0xed && str[1] > 0x9f) return 3;
            c = (unsigned int)((*str++ & 0x0f) << 12);
            if ((*str & 0xc0) != 0x80) return 3;
            c += (unsigned int)((*str++ & 0x3f) << 6);
            if ((*str & 0xc0) != 0x80) return 3;
            c += (*str++ & 0x3f);
            *out_char = c;
            return 3;
        }
        if ((*str & 0xf8) == 0xf0) {
            *out_char = 0xFFFD;
            if (in_text_end && in_text_end - (const char*)str < 4) return 1;
            if (*str > 0xf4) return 4;
            if (*str == 0xf0 && (str[1] < 0x90 || str[1] > 0xbf)) return 4;
            if (*str == 0xf4 && str[1] > 0x8f) return 4; 
            c = (unsigned int)((*str++ & 0x07) << 18);
            if ((*str & 0xc0) != 0x80) return 4;
            c += (unsigned int)((*str++ & 0x3f) << 12);
            if ((*str & 0xc0) != 0x80) return 4;
            c += (unsigned int)((*str++ & 0x3f) << 6);
            if ((*str & 0xc0) != 0x80) return 4;
            c += (*str++ & 0x3f);
            if ((c & 0xFFFFF800) == 0xD800) return 4;
            *out_char = c;
            return 4;
        }
        *out_char = 0;
        return 0;
    }

    bool TextureFont::Initialize()
    {
        FT_Size_Metrics metrics;

        if (!InitFreeType(fontSize * 100.0f)) {
            return false;
        }

        metrics = face->size->metrics;
        ascender = (metrics.ascender >> 6) / 100.0f;
        descender = (metrics.descender >> 6) / 100.0f;
        height = (metrics.height >> 6) / 100.0f;

        FT_Done_Face(face);
        FT_Done_FreeType(library);

        return true;
    }

    int TextureFont::InitFreeType(float size)
    {
        FT_Matrix matrix = {
            static_cast<int>((1.0 / HIGHRES) * 0x10000L),
            static_cast<int>((0.0)           * 0x10000L),
            static_cast<int>((0.0)           * 0x10000L),
            static_cast<int>((1.0)           * 0x10000L)};
        FT_Error error;
        error = FT_Init_FreeType(&library);
        if (error) {
            EngineLogError("FREE_TYPE_ERROR: Could not Init FreeType!\n");
            FT_Done_FreeType(library);
            return 0;
        }

        error = FT_New_Face(library, filename, 0, &face);

        if (error) {
            EngineLogError("FREE_TYPE_ERROR: Could not create a new face!\n");
            FT_Done_FreeType(library);
            return 0;
        }

        error = FT_Select_Charmap(face, FT_ENCODING_UNICODE);
        if (error) {
            EngineLogError("FREE_TYPE_ERROR: Could not select charmap!\n");
            FT_Done_Face(face);
            return 0;
        }

        error = FT_Set_Char_Size(face, static_cast<ulong>(size * HIGHRES), 0, DPI * HIGHRES, DPI);
        if (error) {
            EngineLogError("FREE_TYPE_ERROR: Could not set char size!\n");
            FT_Done_Face(face);
            return 0;
        }

        FT_Set_Transform(face, &matrix, NULL);

        return 1;
    }

    void TextureFont::LoadFromFile(const std::string& filePath, float fontSize)
    {
        atlas.Create(512, 1);
        std::fill(atlas.buffer.begin(), atlas.buffer.end(), 0);
        this->fontSize = fontSize;  
        this->filename = strdup(filePath.c_str());

        Initialize();
    }

    Glyph* TextureFont::getGlyph(const char8_t* codepoint)
    {
        if (Glyph* glyph = FindGlyph(codepoint)) {
            return glyph;
        }

        if (LoadGlyph(codepoint)) {
            return FindGlyph(codepoint);
        }

        return nullptr;
    }

    Glyph* TextureFont::FindGlyph(const char8_t* codepoint)
    {
        Glyph* glyph = nullptr;
        uint32 ucodepoint;
        CharFromUtf8(&ucodepoint, (char*)codepoint, NULL);
        for (uint32 i = 0; i < glyphs.size(); ++i) {
            glyph = &glyphs[i];
            if (glyph->codepoint == ucodepoint) {
                return glyph;
            }
        }

        return nullptr;
    }

    uint32 TextureFont::LoadGlyph(const char8_t* codepoint)
    {
        FT_Error error = NULL;
        FT_Glyph ftGlyph = nullptr;
        FT_GlyphSlot slot = nullptr;
        FT_Bitmap bitmap;

        if (!InitFreeType(fontSize)) {
            return 0;
        }

        if (FindGlyph(codepoint)) {
            FT_Done_Face(face);
            FT_Done_FreeType(library);
            return 1;
        }

        unsigned int cp;
        CharFromUtf8(&cp, (char*)codepoint, NULL);
        uint32 glyphIndex = FT_Get_Char_Index(face, cp);

        int flag = 0;
        flag |= FT_LOAD_RENDER;
        flag |= FT_LOAD_FORCE_AUTOHINT;

        error = FT_Load_Glyph(face, glyphIndex, flag);
        if (error) {
            EngineLogError("FREE_TYPE_ERROR: Could not load the glyph (line {})!\n", __LINE__);
            FT_Done_Face(face);
            FT_Done_FreeType(library);
            return 0;
        }

        slot = face->glyph;
        bitmap = slot->bitmap;
        int glyphTop = slot->bitmap_top;
        int glyphLeft = slot->bitmap_left;

        uint32 srcWidth = bitmap.width / atlas.bytesPerPixel;
        uint32 srcHeight = bitmap.rows;

        uint32 tgtWidth = srcWidth;
        uint32 tgtHeight = srcHeight;

        auto buffer = std::make_unique<uchar[]>(tgtWidth * tgtHeight * atlas.bytesPerPixel);

        uchar* destPointer = buffer.get();
        uchar* srcPointer = bitmap.buffer;

        for (uint32 i = 0; i < srcHeight; ++i) {
            memcpy(destPointer, srcPointer, bitmap.width);
            destPointer += tgtWidth * atlas.bytesPerPixel;
            srcPointer += bitmap.pitch;
        }

        auto origin = atlas.PackTexture(buffer.get(), { tgtWidth, tgtHeight });

        float x = origin.x;
        float y = origin.y;

        Glyph current;
        current.codepoint = cp;
        current.width = tgtWidth;
        current.height = tgtHeight;
        current.bearing.x = glyphLeft;
        current.bearing.y = glyphTop;
        current.s0 = x / (float)atlas.textureSize.w;
        current.t0 = y / (float)atlas.textureSize.h;
        current.s1 = (x + tgtWidth) / (float)atlas.textureSize.w;
        current.t1 = (y + tgtHeight) / (float)atlas.textureSize.h;

        current.advance.x = slot->advance.x / (float)HIGHRES;
        current.advance.y = slot->advance.y / (float)HIGHRES;

        glyphs.push_back(current);

        FT_Done_Glyph(ftGlyph);
        FT_Done_Face(face);
        FT_Done_FreeType(library);

        return 1;
    } 

для рендеринга строки (в данном случае одного символа) я перебираю размер строки, получаю глиф, обновляю атлас и настраиваю данные рендеринга.

текст представляет собой простой четырехугольник с текстурой и правильными UV-развертками. Я не думаю, что нужно объяснять, что находится внутри AddVertexData, потому что это не вызывает затруднений.

void DrawString(const std::u8string& string, float x, float y)
    {
        for (const auto& c : string) {
            auto glyph = textureFont.getGlyph(&c);

            auto& t = *(Texture2D*)texture.get();
            t.UpdateData(textureFont.atlas.buffer.data());

            float x0 = x + static_cast<float>(glyph->bearing.x);
            float y0 = y + (textureFont.ascender + textureFont.descender - static_cast<float>(glyph->bearing.y));
            float x1 = x0 + static_cast<float>(glyph->width);
            float y1 = y0 + static_cast<float>(glyph->height);

            float u0 = glyph->s0;
            float v0 = glyph->t0;
            float u1 = glyph->s1;
            float v1 = glyph->t1;

            //            position                uv                      color
            AddVertexData(Vector2<float>(x0, y0), Vector2<float>(u0, v0), 0xff0000ff);
            AddVertexData(Vector2<float>(x0, y1), Vector2<float>(u0, v1), 0xff0000ff);
            AddVertexData(Vector2<float>(x1, y1), Vector2<float>(u1, v1), 0xff0000ff);
            AddVertexData(Vector2<float>(x1, y0), Vector2<float>(u1, v0), 0xff0000ff);

            // indices for DrawElements() call
            // 0, 1, 2, 2, 3, 0
            AddRectElements();

            x += glyph->advance.x;
        }
    }

ę имеет размер utf-8 == 2, поэтому цикл выполняется дважды, но отображает только 1 символ и не знает второго символа (потому что второго символа нет), поэтому он отображает пустой квадрат.

Как избавиться от четырехугольника, следующего за персонажем, который я хочу отрендерить?


person BrodaJarek3    schedule 01.10.2019    source источник
comment
Использование const auto& c : string для доступа к нескольким символам вызывает удивление.   -  person Jarod42    schedule 01.10.2019
comment
Примечание: есть две проблемы: кодовые точки Unicode требуют от 1 до 4 байтов в UTF-8, но для отображения одного глифа/символа может потребоваться до 8 кодовых точек (или более) (модификаторы), и большее количество символов может быть объединено в лигатуру шрифтами (так что глифы отображаются правильно). [курсивный шрифт использует много лигатур (по определению скорописи)]. Это ссылка на передовой опыт: unicode.org/reports/tr29/tr29- 35.html   -  person Giacomo Catenazzi    schedule 02.10.2019


Ответы (4)


В вашей функции DrawString у вас есть цикл

for (const auto& c : string)

Этот цикл будет перебирать строку байт за байтом. Таким образом, если строка содержит двухбайтовый символ "ę", то первая итерация получит первый байт, а вторая итерация — второй байт.

Вы не можете использовать здесь цикл for на основе диапазона, так как вам нужно пропустить байты в строке. Либо используйте цикл на основе итератора, либо цикл на основе индекса.

Например

for (size_t i = 0; i < string.size(); /* nothing */) {
    // Here you need to get the number of bytes for the current character
    // Then you should increment the index by that amount
    i += byte_count_for_current_character;

    // ... rest of code
}
person Some programmer dude    schedule 01.10.2019

Ваша проблема находится в DrawString с for (const auto& c : string)

Вы должны пропустить лишние символы, используемые для кодирования предыдущего глифа, совпадающие с 0b10......:

for (const auto& c : string) {
    if ((c & 0b1100'0000) == 0b1000'0000) {
        continue;
    }
// ...
}

или перейти к количеству байтов, прочитанных последним глифом.

person Jarod42    schedule 01.10.2019

Оба вызова вашей фактической функции декодирования UTF-8 CharFromUtf8 игнорируют ее возвращаемое значение, которое представляет собой количество байтов, на которые должен быть сдвинут указатель строки. Вместо for (const auto& c : string) у вас должен быть указатель, который вы продвигаете на возвращаемое значение на каждой итерации.

Кроме того, поскольку вы будете использовать функцию CharFromUtf8 внутри этого цикла, вы будете знать как кодовую точку Unicode, так и количество байтов для продвижения. Затем вы можете реорганизовать свой TextureFont, чтобы использовать unsigned int (то есть кодовые точки) в качестве аргументов, а не позволять ему выполнять декодирование UTF-8. Это было бы лучшим разделением интересов.

person Aurel Bílý    schedule 01.10.2019

Другие ответы уже определили проблему с использованием цикла for на основе диапазона непосредственно с переменной std::u8string. Предполагая, что перечисление на основе кодовых точек - это то, что вам нужно (вероятно, это не так, поскольку, как правило, правильный выбор глифа зависит от окружающих кодовых точек; вы, вероятно, хотите перебирать расширенные кластеры графем), вы можете использовать библиотеку, например text_view, чтобы обеспечить поддержку на основе диапазона для итерации кодовых точек. Тогда эта петля выглядит так:

auto tv = make_text_view<utf8_encoding>(string);
for (const auto& cp : tv) {
  ...
}
person Tom Honermann    schedule 02.10.2019
comment
Интересно, я не совсем понимаю correct glyph selection depends on surrounding code points; you probably want to iterate over extended grapheme clusters. Что вы подразумеваете под surrounding codepoints и что такое grapheme clusters? - person BrodaJarek3; 02.10.2019
comment
Расширенные кластеры графем (EGC) — это термин Юникода, используемый для описания последовательности кодовых точек, которые определенным образом связаны друг с другом. - person Tom Honermann; 02.10.2019
comment
Рассмотрим эти два способа кодирования буквы 'á': U+00E1 (ЛАТИНСКАЯ СТРОЧНАЯ БУКВА A С ОСТРОЙ ЧАСТЬЮ) и U+0061 (ЛАТИНСКАЯ СТРОЧНАЯ БУКВА A) + U+0301 (КОМБИНИРОВАНИЕ ОСТРОЙ АКЦЕНТНОСТИ). Первый представляет символ с помощью одной кодовой точки; второй использует две кодовые точки. Обе формы кодируют один EGC, и обе формы должны отображаться с одним глифом. См. Нормализация Unicode. - person Tom Honermann; 02.10.2019
comment
Это особенно актуально для эмодзи. Например, семейные смайлики состоят из последовательностей кодовых точек, которые выбирают членов семьи, соединенных вместе U+200D (ОБЪЕДИНИТЕЛЬ НУЛЕВОЙ ШИРИНЫ). - person Tom Honermann; 02.10.2019
comment
Некоторые полезные ссылки: - unicode.org/glossary - gankra.github.io/blah/text-hates-you - hsivonen.fi/string-length. - person Tom Honermann; 02.10.2019