Контурные шрифты с истинным шрифтом и ядром opengl (3.3)

Здесь я использовал пример отрисовки текста с сайта learnnopengl.com:

https://learnopengl.com/In-Practice/Text-Rendering

Я пытаюсь изменить этот пример, чтобы отобразить контур, но я немного теряюсь между всеми вызываемыми функциями, и код шейдера кажется немного сложным.

Вот моя попытка:

void inittext_stroked() {
    cout << "initializing text, stroken" << endl;

    glEnable(GL_BLEND);
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

    // Compile and setup the shader
    shader = Shader("text.vs", "text.fs");
        //Shader shader("text.vs", "text.fs");

    glm::mat4 projection = glm::ortho(0.0f, static_cast<GLfloat>(WIDTH), 0.0f, static_cast<GLfloat>(HEIGHT));
    shader.use();
    //shader.
    glUniformMatrix4fv(glGetUniformLocation(shader.ID, "projection"), 1, GL_FALSE, glm::value_ptr(projection));

    // FreeType
    FT_Library ft;
    // All functions return a value different than 0 whenever an error occurred
    if (FT_Init_FreeType(&ft))
        std::cout << "ERROR::FREETYPE: Could not init FreeType Library" << std::endl;

    string fontfile = base::config.getstr("fontfile");
    cout << fontfile << endl;
    // Load font as face
    FT_Face face;
    // if (FT_New_Face(ft, "DejaVuSansMono.ttf", 0, &face))
    if (FT_New_Face(ft, fontfile.c_str(), 0, &face))
        std::cout << "ERROR::FREETYPE: Failed to load font " << fontfile << std::endl;

    // stroke, added 
    // void * m_stroker;
    FT_Stroker stroker;
    if (FT_Stroker_New(static_cast<FT_Library>(ft), &stroker) != 0)
    {
        cerr << "Failed to load font \"" << fontfile << "\" (failed to create the stroker)" << std::endl;
        FT_Done_Face(face);
        return;
    }
    // m_stroker = stroker;
    // m_face = face;
    // end added


    // Set size to load glyphs as
    int fontsize;
    //cfg.SET(fontsize);
    // fontsize = 16;
    fontsize = base::config.getvar<int>("fontsize");


    FT_Set_Pixel_Sizes(face, 0, fontsize);
    //FT_Set_Pixel_Sizes(face, 0, 48);

    // Disable byte-alignment restriction
    glPixelStorei(GL_UNPACK_ALIGNMENT, 1);

    // Load first 128 characters of ASCII set
    for (GLubyte c = 0; c < 128; c++)
    {
        // Load character glyph 

        if (FT_Load_Char(face, c,
            //FT_LOAD_RENDER|
            FT_LOAD_NO_BITMAP|FT_LOAD_TARGET_NORMAL

        )) // modified, added FT_LOAD_NO_BITMAP (also 2 more so 3)
        {
            std::cout << "ERROR::FREETYTPE: Failed to load Glyph" << std::endl;
            continue;
        }
        // added outlining _________________
        float outlineThickness = 10.0f;
        // Retrieve the glyph
        FT_Glyph glyphDesc;
        if (FT_Get_Glyph(face->glyph, &glyphDesc) != 0)
        {
            cerr << "bad return from FT_Get_Glyph" << endl;
            return;
        }

        {
            // FT_Stroker stroker = static_cast<FT_Stroker>(m_stroker);

            FT_Stroker_Set(stroker, static_cast<FT_Fixed>(outlineThickness * static_cast<float>(1 << 6)), FT_STROKER_LINECAP_ROUND, FT_STROKER_LINEJOIN_ROUND, 0);
            FT_Glyph_Stroke(&glyphDesc, stroker, true);
        }

        FT_Glyph_To_Bitmap(&glyphDesc, FT_RENDER_MODE_NORMAL, 0, 1);
        // FT_Bitmap& bitmap = reinterpret_cast<FT_BitmapGlyph>(glyphDesc)->bitmap;

        // added end _________________

        // Generate texture
        GLuint texture;
        glGenTextures(1, &texture);
        glBindTexture(GL_TEXTURE_2D, texture);
        glTexImage2D(
            GL_TEXTURE_2D,
            0,
                //GL_RED,
            GL_RG16,
            face->glyph->bitmap.width,
            face->glyph->bitmap.rows,
            0,
                //GL_RED,
            GL_RG16,
            GL_UNSIGNED_BYTE,
            face->glyph->bitmap.buffer
        );
        // Set texture options
        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);
        // Now store character for later use
        Character character = {
            texture,
            glm::ivec2(face->glyph->bitmap.width, face->glyph->bitmap.rows),
            glm::ivec2(face->glyph->bitmap_left, face->glyph->bitmap_top),
            face->glyph->advance.x
        };
        Characters.insert(std::pair<GLchar, Character>(c, character));
    }
    glBindTexture(GL_TEXTURE_2D, 0);
    // Destroy FreeType once we're finished
    FT_Done_Face(face);
    FT_Done_FreeType(ft);


    // Configure VAO/VBO for texture quads
    glGenVertexArrays(1, &VAO_text);
    glGenBuffers(1, &VBO_text);
    glBindVertexArray(VAO_text);
    glBindBuffer(GL_ARRAY_BUFFER, VBO_text);
    glBufferData(GL_ARRAY_BUFFER, sizeof(GLfloat) * 6 * 4, NULL, GL_DYNAMIC_DRAW);
    glEnableVertexAttribArray(0);
    glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 4 * sizeof(GLfloat), 0);
    glBindBuffer(GL_ARRAY_BUFFER, 0);
    glBindVertexArray(0);


    texts["popo"] = "haha";
}

и код шейдера:

#version 330 core
in vec2 TexCoords;
out vec4 color;

uniform sampler2D text;
uniform vec3 textColor;
uniform vec3 outlinecolor;

void main()
{   
    // old shader code:
    // vec4 sampled = vec4(1.0, 1.0, 1.0, texture(text, TexCoords).r);
    // color = vec4(textColor, 1.0) * sampled;

    vec2 tex = texture2D(text, TexCoords).rg;
    color = vec4(textColor, tex.g)
    +vec4(outlinecolor, tex.g)
    ;
}  

Как видите, я использовал GL_RG16 вместо RG_RED, чтобы иметь один канал для заливки и один для контура. Я не понимаю всех этих вызовов freetype, и я не знаком с понятиями лица, глифа и штриховки, и я не уверен, что мой код шейдера адекватен.

Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
2
0
1 173
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Самый простой способ сделать это - нарисовать каждую букву второй раз с некоторым масштабированием и другим цветом в той же позиции. Просто убедитесь, что вы рисуете их в правильном порядке (сначала увеличенный, а затем нормальный).

Ответ принят как подходящий

То, что вы хотите сделать, не так просто. Первое недоразумение состоит в том, что FT_Glyph_To_Bitmap не создаст 2 канала, один для контуров штрихов и один для заполненного тела, как вы и ожидали. Вам нужно создать 2 растровых изображения из глифа и вручную объединить их в одно растровое изображение с 2 каналами.

Проблема в том, что растровые изображения даже не будут иметь одинаковый размер. "Контурное" растровое изображение будет немного выше, чем "заполненное".

Сначала создайте растровое изображение контура:

FT_Error err_code = FT_Load_Char( face, i, FT_LOAD_NO_BITMAP | FT_LOAD_TARGET_NORMAL );
if ( err_code != 0 )
{
  // error handling
}

FT_Glyph glyphDescStroke;
err_code = FT_Get_Glyph( face->glyph, &glyphDescStroke );

static double outlineThickness = 2.0;
FT_Stroker_Set( stroker, static_cast<FT_Fixed>(outlineThickness * static_cast<float>(1 << 6)), FT_STROKER_LINECAP_ROUND, FT_STROKER_LINEJOIN_ROUND, 0 );
if ( err_code == 0 )
  err_code = FT_Glyph_Stroke( &glyphDescStroke, stroker, true );

if ( err_code == 0 )
  err_code = FT_Glyph_To_Bitmap( &glyphDescStroke, FT_RENDER_MODE_NORMAL, 0, 1);

FT_BitmapGlyph glyph_bitmap;
FT_Bitmap *bitmap_stroke = nullptr;
if ( err_code == 0 )
{
    glyph_bitmap  = (FT_BitmapGlyph)glyphDescStroke;
    bitmap_stroke = &glyph_bitmap->bitmap;
}

Создайте временный буфер с двумя цветовыми каналами и скопируйте растровое изображение контура во второй цветовой канал буфера:

#include <vector>

unsigned int cx = 0, cy = 0, ox = 0, oy = 0;

std::vector<unsigned char> buffer
if ( error_code == 0 && bitmap_stroke )
{
    cx = bitmap_stroke->width;
    cy = bitmap_stroke->rows;
    ox = glyph_bitmap->left;
    oy = glyph_bitmap->top;

    buffer = std::vector<unsigned char>(cx * cy * 2, 0); // * 2 -> 2 color channels (red and green)
    for ( unsigned int i = 0; i < cx * cy; ++ i)
        buffer[i*2 + 1] = bitmap_stroke->buffer[i];      // + 1 -> 2nd color channel
}

FT_Done_Glyph( glyphDescStroke );

Создайте "заполненное" растровое изображение:

FT_Glyph glyphDescFill;
err_code = FT_Get_Glyph( face->glyph, &glyphDescFill );
if ( err_code == 0 )
    err_code = FT_Glyph_To_Bitmap( &glyphDescFill, FT_RENDER_MODE_NORMAL, 0, 1);

FT_Bitmap *bitmap_fill = nullptr;
if ( err_code == 0 )
{
    FT_BitmapGlyph glyph_bitmap = (FT_BitmapGlyph)glyphDescFill;
    bitmap_fill = &glyph_bitmap->bitmap;
}

Добавьте "заполненное" растровое изображение к 1-му цветовому каналу буфера:

if ( err_code == 0 && bitmap_fill )
{
    unsigned int cx_fill  = bitmap_fill->width;
    unsigned int cy_fill  = bitmap_fill->rows;
    unsigned int offset_x = (cx - cx_fill) / 2; // offset because the bitmap my be smaller, 
    unsigned int offset_y = (cy - cy_fill) / 2; // then the former

    for ( unsigned int y = 0; y < cy_fill; ++ y )
    {
        for ( unsigned int x = 0; x < cx_fill; ++ x )
        {
            unsigned int i_source = y * cx_fill + x;
            unsigned int i_target = (y + offset_y) * cx + x + offset_x;
            buffer[i_target*2 + 0] = bitmap_fill->buffer[i_source]; // + 0 -> 1st color channel
        }
    }
}

FT_Done_Glyph( glyphDescFill );

Когда вы загружаете буфер в объект текстуры, вы должны установить параметр GL_UNPACK_ALIGNMENT, потому что битовая карта глифа buffer плотно упакована и строка битовой карты может не быть выровнена по 4 байтам. См. glPixelStore.

GLuint texture;
glGenTextures(1, &texture);

glBindTexture(GL_TEXTURE_2D, texture);

glPixelStore( GL_UNPACK_ALIGNMENT, 1 ); // of course 2 will work too (2 channels)     
glTexImage2D( 
    GL_TEXTURE_2D,
    0,
    GL_RG8, // 2 channels each with 8 bits
    cx,
    cy,
    GL_RG,  // "GL_RG" - "GL_RG16" is not a valid format
    GL_UNSIGNED_BYTE,
    buffer.data() );
glPixelStore( GL_UNPACK_ALIGNMENT, 4 );

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);

Добавьте символьные данные в структуру данных:

Character character = {
    texture,
    glm::ivec2(cx, cy),
    glm::ivec2(ox, oy),
    face->glyph->advance.x
};
Characters.insert(std::pair<GLchar, Character>(c, character));

Код GLSL, который рисует глиф с отдельным цветом для контура и внутренней заливки, который использует функцию GLSL mix, может выглядеть следующим образом:

#version 330 core
in vec2 TexCoords;
out vec4 color;

uniform sampler2D text;
uniform vec3 textColor;
uniform vec3 outlinecolor;

void main()
{  
    vec3 fill_col    = vec3(0.0, 0.0, 1.0); // e.g blue
    vec3 outline_col = vec3(1.0, 0.0, 0.0); // e.g red

    vec2 tex = texture2D(text, TexCoords).rg;
    float fill    = tex.r;
    float outline = tex.g;

    float alpha    = max( fill, outline );
    vec3 mix_color = mix( mix(vec3(0.0), fill_col, fill), outline_col, outline );

    color = vec4(mix_color, alpha);
}

Превью со шрифтом pixlim (2) .ttf из репозитория, на который вы указали ссылки в комментариях:

text

Вы тестировали этот код? Мне удалось заставить его работать, но символы не выровнены i.imgur.com/HNM82IA.png Использование другого действительного TTF, чем шрифты deja, вылетает.

jokoon 30.08.2018 19:05

@jokoon Да, я тестировал, но не эту часть кода. Я протестировал часть, которая создает растровые изображения и шейдер, в существующем примере в моем наборе тестов. Я посмотрю на вашу проблему.

Rabbid76 30.08.2018 19:08

@jokoon Настройка структуры Character также должна быть адаптирована. Смотрите изменения в ответе.

Rabbid76 30.08.2018 19:38

Текст теперь выровнен! К сожалению, он вылетает внутри ftobjs.c в строке 2955, когда я загружаю шрифт TTF, который я сделал ...

jokoon 30.08.2018 20:12

@jokoon Я тестировал код с помощью FreeSans.ttf, 37043_SYMBOL.ttf (греческие буквы) и Pacifico.ttf (скрипт), и он работал нормально.

Rabbid76 30.08.2018 20:17
github.com/jokoon/eio/blob/master/__data__/fonts/mine/… это приведет к сбою ... Хотя я не знаю почему, и я предполагаю, что вы не разработчик freetype. Он был создан с помощью этого онлайн-генератора шрифтов: pentacom.jp/pentacom/bitfontmaker2
jokoon 30.08.2018 20:31

@jokoon pixlim (2) .ttf, кажется, работает для меня. Может быть, это зависит от версии библиотеки. Пользуюсь версией 2.9

Rabbid76 30.08.2018 20:47
i.imgur.com/qCRtE20.png верхний текст отображается с использованием SFML, тот же шрифт, нижний - с помощью opengl. Я пробовал разные значения обводки, но текст выглядит призрачно и пустым ... Как вы думаете?
jokoon 30.08.2018 21:34

SFML использует FT_LOAD_FORCE_AUTOHINT, и я видел, как другой шейдер шрифтов делает что-то вроде fill = clamp(fill * 2.0, 0.0, 1.0);wildfiregames.com/forum/index.php?/topic/…. На вашем скриншоте пространство для заполнения результата кажется хорошим ... Возможно, мне что-то не хватает ... Какое значение контура / размер шрифта вы используете?

jokoon 30.08.2018 22:20
i.imgur.com/MmLuTHu.png результатом доволен, спасибо! Деталь устанавливает контур на 64 даже при небольшом размере, не совсем уверен, почему, но я предполагаю, что он определяет радиус. Зажим не использовал. Последняя проблема - кернинг, так как вы можете видеть, что многие контуры обрезаются. Понятия не имею, как это исправить, но код SFML может помочь.
jokoon 30.08.2018 22:48

Хотя это не похоже на проблему кернинга, это просто текстуры, по какой-то причине перекрывающие альфу ...

jokoon 30.08.2018 23:00

К вашему сведению, если вы хотите отобразить пример строки со всеми буквами, найдите панграмму, например «Быстрая коричневая лиса перепрыгивает через ленивую собаку».

ratchet freak 31.08.2018 14:26

Другие вопросы по теме