SDL3 & OpenGL: Text Rendering with SDL3_ttf SDL3 и OpenGL: Рендеринг текста с помощью SDL3_ttf

A guide to rendering TrueType fonts into GPU memory using Python, PySDL3, and PyOpenGL. Руководство по рендерингу шрифтов TrueType в память GPU с использованием Python, PySDL3 и PyOpenGL.

1. Installation 1. Установка

Ensure you have Python installed. Install PySDL3 and PyOpenGL using pip:

Убедитесь, что у вас установлен Python. Установите PySDL3 и PyOpenGL через pip:

pip PySDL3 numpy PyOpenGL PyGLM

2. Assets 2. Ресурсы

Before creating the files, download the required asset: Перед созданием файлов скачайте необходимый ресурс:

3. Project Structure 3. Структура проекта

pysdl3-ttf-opengl-app/
├── assets/fonts/
│   └── LiberationSans-Regular.ttf
└── main.py

4. Source Code 4. Исходный код

Create main.py and paste the following implementation:

Создайте файл main.py и вставьте следующую реализацию:

import os
import ctypes
import numpy as np
import glm
from OpenGL.GL import *
from OpenGL.GL.shaders import *

os.environ["SDL_MAIN_USE_CALLBACKS"] = "1"
os.environ["SDL_RENDER_DRIVER"] = "opengl"

import sdl3

# Global state
window, glContext, font, textTexture = None, None, None, 0
textW, textH = 0, 0
shaderProgram, vao, vbo = 0, 0, 0
projLoc, modelLoc = 0, 0
canvasWidth, canvasHeight = 430, 430

@sdl3.SDL_AppInit_func
def SDL_AppInit(appstate, argc, argv):
    global window, glContext, font, textTexture, textW, textH, shaderProgram, vao, vbo, projLoc, modelLoc

    if not sdl3.SDL_Init(sdl3.SDL_INIT_VIDEO): return sdl3.SDL_APP_FAILURE
    if not sdl3.TTF_Init(): return sdl3.SDL_APP_FAILURE

    sdl3.SDL_GL_SetAttribute(sdl3.SDL_GL_CONTEXT_PROFILE_MASK, sdl3.SDL_GL_CONTEXT_PROFILE_CORE)
    sdl3.SDL_GL_SetAttribute(sdl3.SDL_GL_CONTEXT_MAJOR_VERSION, 3)
    sdl3.SDL_GL_SetAttribute(sdl3.SDL_GL_CONTEXT_MINOR_VERSION, 3)

    window = sdl3.SDL_CreateWindow(b"SDL3_ttf OpenGL", canvasWidth, canvasHeight, sdl3.SDL_WINDOW_OPENGL)
    glContext = sdl3.SDL_GL_CreateContext(window)
    sdl3.SDL_GL_SetSwapInterval(1)

    # Shaders
    vs = compileShader(b"""#version 330 core
        layout (location = 0) in vec4 aVertex;
        uniform mat4 projection;
        uniform mat4 model;
        out vec2 vTexCoord;
        void main() {
            gl_Position = projection * model * vec4(aVertex.xy, 0.0, 1.0);
            vTexCoord = aVertex.zw;
        }""", GL_VERTEX_SHADER)
    
    fs = compileShader(b"""#version 330 core
        precision mediump float;
        in vec2 vTexCoord;
        out vec4 fragColor;
        uniform sampler2D textSampler;
        void main() { fragColor = texture(textSampler, vTexCoord); }""", GL_FRAGMENT_SHADER)
    
    shaderProgram = compileProgram(vs, fs)
    
    # Enable Blending (CRITICAL for text)
    glEnable(GL_BLEND)
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)

    # Text rendering
    font = sdl3.TTF_OpenFont(b"assets/fonts/LiberationSans-Regular.ttf", 36)
    color = sdl3.SDL_Color(170, 255, 195, 255)
    text = "Hello, OpenGL!\n\nПривет, OpenGL!"
    surf = sdl3.TTF_RenderText_Blended_Wrapped(font, text.encode('utf-8'), 0, color, 0)
    
    conv = sdl3.SDL_ConvertSurface(surf, sdl3.SDL_PIXELFORMAT_RGBA32)
    textW, textH = conv.contents.w, conv.contents.h
    
    textTexture = glGenTextures(1)
    glBindTexture(GL_TEXTURE_2D, textTexture)
    glPixelStorei(GL_UNPACK_ALIGNMENT, 1)
    
    data_size = textW * textH * 4
    pixel_pointer = ctypes.cast(conv.contents.pixels, ctypes.POINTER(ctypes.c_ubyte * data_size))

    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, textW, textH, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixel_pointer.contents)
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
    
    sdl3.SDL_DestroySurface(surf)
    sdl3.SDL_DestroySurface(conv)

    # Unit Quad Setup
    verts = np.array([
        0.0, 1.0, 0.0, 1.0,
        1.0, 0.0, 1.0, 0.0,
        0.0, 0.0, 0.0, 0.0,
        0.0, 1.0, 0.0, 1.0,
        1.0, 1.0, 1.0, 1.0,
        1.0, 0.0, 1.0, 0.0
    ], dtype=np.float32)

    vao = glGenVertexArrays(1)
    vbo = glGenBuffers(1)
    glBindVertexArray(vao)
    glBindBuffer(GL_ARRAY_BUFFER, vbo)
    glBufferData(GL_ARRAY_BUFFER, verts.nbytes, verts, GL_STATIC_DRAW)
    glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 16, None)
    glEnableVertexAttribArray(0)

    projLoc = glGetUniformLocation(shaderProgram, "projection")
    modelLoc = glGetUniformLocation(shaderProgram, "model")
    return sdl3.SDL_APP_CONTINUE

@sdl3.SDL_AppEvent_func
def SDL_AppEvent(appstate, event):
    if sdl3.SDL_DEREFERENCE(event).type == sdl3.SDL_EVENT_QUIT:
        return sdl3.SDL_APP_SUCCESS
    return sdl3.SDL_APP_CONTINUE

@sdl3.SDL_AppIterate_func
def SDL_AppIterate(appstate):
    glClearColor(0.2, 0.2, 0.2, 1.0)
    glClear(GL_COLOR_BUFFER_BIT)

    glUseProgram(shaderProgram)
    proj = glm.ortho(0.0, float(canvasWidth), float(canvasHeight), 0.0, -1.0, 1.0)
    model = glm.translate(glm.mat4(1.0), glm.vec3(20.0, 100.0, 0.0))
    model = glm.scale(model, glm.vec3(float(textW), float(textH), 1.0))

    glUniformMatrix4fv(projLoc, 1, GL_FALSE, glm.value_ptr(proj))
    glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm.value_ptr(model))

    glBindTexture(GL_TEXTURE_2D, textTexture)
    glBindVertexArray(vao)
    glDrawArrays(GL_TRIANGLES, 0, 6)

    sdl3.SDL_GL_SwapWindow(window)
    return sdl3.SDL_APP_CONTINUE

@sdl3.SDL_AppQuit_func
def SDL_AppQuit(appstate, result):
    # Free GPU memory for the texture
    if textTexture != 0:
        glDeleteTextures(1, np.array([textTexture], dtype=np.uint32))

    # Free other resources
    sdl3.TTF_CloseFont(font)
    sdl3.TTF_Quit()
    sdl3.SDL_DestroyWindow(window)

4. Running the Script 4. Запуск скрипта

Run the script via your terminal:

Запустите скрипт через терминал:

python main.py

5. Dynamic Rendering (The Game Loop) 5. Динамический рендеринг (Игровой цикл)

In real-world applications, you often need to update UI elements like score counters. Since OpenGL textures are static once uploaded to the GPU, you must regenerate the texture whenever the text content changes.

В реальных приложениях часто требуется обновлять элементы интерфейса, такие как счетчики очков. Поскольку текстуры OpenGL статичны после загрузки в GPU, необходимо пересоздавать текстуру каждый раз, когда меняется содержимое текста.

Important: Always call glDeleteTextures before generating a new texture to prevent GPU memory leaks.

Важно: Всегда вызывайте glDeleteTextures перед созданием новой текстуры, чтобы избежать утечек памяти GPU.

You can implement a timer within SDL_AppIterate to update your UI dynamically:

Вы можете реализовать таймер внутри SDL_AppIterate для динамического обновления интерфейса:

import ctypes
import os
import time

import glm
import numpy as np
from OpenGL.GL import *
from OpenGL.GL.shaders import *

os.environ["SDL_MAIN_USE_CALLBACKS"] = "1"
os.environ["SDL_RENDER_DRIVER"] = "opengl"
import sdl3

# Global state
window, glContext, font, textTexture = None, None, None, 0
textW, textH = 0, 0
shaderProgram, vao, vbo = 0, 0, 0
projLoc, modelLoc = 0, 0
canvasWidth, canvasHeight = 430, 430

# Counter state
coins = 0
last_time = 0

def update_text_texture(text_string):
    global textTexture, textW, textH
    color = sdl3.SDL_Color(170, 255, 195, 255)
    surf = sdl3.TTF_RenderText_Blended_Wrapped(font, text_string.encode('utf-8'), 0, color, 0)
    conv = sdl3.SDL_ConvertSurface(surf, sdl3.SDL_PIXELFORMAT_RGBA32)
    
    # Delete old texture before creating new one
    if textTexture != 0:
        glDeleteTextures(1, np.array([textTexture], dtype=np.uint32))
        
    textW, textH = conv.contents.w, conv.contents.h
    textTexture = glGenTextures(1)
    glBindTexture(GL_TEXTURE_2D, textTexture)
    glPixelStorei(GL_UNPACK_ALIGNMENT, 1)
    
    data_size = textW * textH * 4
    pixel_pointer = ctypes.cast(conv.contents.pixels, ctypes.POINTER(ctypes.c_ubyte * data_size))
    
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, textW, textH, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixel_pointer.contents)
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
    
    sdl3.SDL_DestroySurface(surf)
    sdl3.SDL_DestroySurface(conv)

@sdl3.SDL_AppInit_func
def SDL_AppInit(appstate, argc, argv):
    global window, glContext, font, shaderProgram, vao, vbo, projLoc, modelLoc, last_time

    if not sdl3.SDL_Init(sdl3.SDL_INIT_VIDEO): return sdl3.SDL_APP_FAILURE
    if not sdl3.TTF_Init(): return sdl3.SDL_APP_FAILURE
    
    sdl3.SDL_GL_SetAttribute(sdl3.SDL_GL_CONTEXT_PROFILE_MASK, sdl3.SDL_GL_CONTEXT_PROFILE_CORE)
    sdl3.SDL_GL_SetAttribute(sdl3.SDL_GL_CONTEXT_MAJOR_VERSION, 3)
    sdl3.SDL_GL_SetAttribute(sdl3.SDL_GL_CONTEXT_MINOR_VERSION, 3)

    window = sdl3.SDL_CreateWindow(b"SDL3_ttf OpenGL", canvasWidth, canvasHeight, sdl3.SDL_WINDOW_OPENGL)
    glContext = sdl3.SDL_GL_CreateContext(window)
    sdl3.SDL_GL_SetSwapInterval(1)

    # Shaders
    vs = compileShader(b"""#version 330 core
        layout (location = 0) in vec4 aVertex;
        uniform mat4 projection;
        uniform mat4 model;
        out vec2 vTexCoord;
        void main() {
            gl_Position = projection * model * vec4(aVertex.xy, 0.0, 1.0);
            vTexCoord = aVertex.zw;
        }""", GL_VERTEX_SHADER)
    
    fs = compileShader(b"""#version 330 core
        precision mediump float;
        in vec2 vTexCoord;
        out vec4 fragColor;
        uniform sampler2D textSampler;
        void main() { fragColor = texture(textSampler, vTexCoord); }""", GL_FRAGMENT_SHADER)
    
    shaderProgram = compileProgram(vs, fs)
    
    # Enable Blending (CRITICAL for text)
    glEnable(GL_BLEND)
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)

    font = sdl3.TTF_OpenFont(b"assets/fonts/LiberationSans-Regular.ttf", 36)
    last_time = time.time()
    update_text_texture("Coins: 0\nМонеты: 0")
    
    # Unit Quad Setup
    verts = np.array([
        0.0, 1.0, 0.0, 1.0,
        1.0, 0.0, 1.0, 0.0,
        0.0, 0.0, 0.0, 0.0,
        0.0, 1.0, 0.0, 1.0,
        1.0, 1.0, 1.0, 1.0,
        1.0, 0.0, 1.0, 0.0
    ], dtype=np.float32)

    vao = glGenVertexArrays(1)
    vbo = glGenBuffers(1)
    glBindVertexArray(vao)
    glBindBuffer(GL_ARRAY_BUFFER, vbo)
    glBufferData(GL_ARRAY_BUFFER, verts.nbytes, verts, GL_STATIC_DRAW)
    glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 16, None)
    glEnableVertexAttribArray(0)

    projLoc = glGetUniformLocation(shaderProgram, "projection")
    modelLoc = glGetUniformLocation(shaderProgram, "model")
    
    return sdl3.SDL_APP_CONTINUE

@sdl3.SDL_AppEvent_func
def SDL_AppEvent(appstate, event):
    if sdl3.SDL_DEREFERENCE(event).type == sdl3.SDL_EVENT_QUIT:
        return sdl3.SDL_APP_SUCCESS
    return sdl3.SDL_APP_CONTINUE

@sdl3.SDL_AppIterate_func
def SDL_AppIterate(appstate):
    global coins, last_time

    current_time = time.time()
    if current_time - last_time >= 1.0:
        coins += 1
        last_time = current_time
        update_text_texture(f"Coins: {coins}\nМонеты: {coins}")

    glClearColor(0.2, 0.2, 0.2, 1.0)
    glClear(GL_COLOR_BUFFER_BIT)

    glUseProgram(shaderProgram)
    proj = glm.ortho(0.0, float(canvasWidth), float(canvasHeight), 0.0, -1.0, 1.0)
    model = glm.translate(glm.mat4(1.0), glm.vec3(20.0, 100.0, 0.0))
    model = glm.scale(model, glm.vec3(float(textW), float(textH), 1.0))

    glUniformMatrix4fv(projLoc, 1, GL_FALSE, glm.value_ptr(proj))
    glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm.value_ptr(model))

    glBindTexture(GL_TEXTURE_2D, textTexture)
    glBindVertexArray(vao)
    glDrawArrays(GL_TRIANGLES, 0, 6)

    sdl3.SDL_GL_SwapWindow(window)

    return sdl3.SDL_APP_CONTINUE

@sdl3.SDL_AppQuit_func
def SDL_AppQuit(appstate, result):
    # Free resources
    sdl3.TTF_CloseFont(font)
    sdl3.TTF_Quit()
    sdl3.SDL_DestroyWindow(window)

Run the script via your terminal:

Запустите скрипт через терминал:

python main.py

Support My Work Поддержать проект

If these tutorials helped you, consider buying me a coffee! Если эти туториалы вам помогли, вы можете поддержать автора.

Sberbank (Russia only) Сбербанк (только для РФ)

Sberbank SBP QR Code

Direct transfer via phone number (Russia only) Перевод по номеру телефона (только для РФ)

+7 (917) 212-29-59

USDT TRC20

USDT TRC20 QR Code

Support via Cryptocurrency Поддержка криптовалютой

TMtY1YifNf6FKvgeFmqKGQR4NStKr3csGp