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: Перед созданием файлов скачайте необходимый ресурс:
- Download the TTF file: LiberationSans-Regular.zip Скачайте TTF-файл: LiberationSans-Regular.zip
- Note: This is a free file taken from this link. Примечание. Это бесплатный файл, который был взят по ссылке.
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) Сбербанк (только для РФ)
Direct transfer via phone number (Russia only) Перевод по номеру телефона (только для РФ)
USDT TRC20
Support via Cryptocurrency Поддержка криптовалютой