Python OpenGL文字渲染——SDL(高效+无限缩放)
最近研究PyOpenGL,苦恼于无法正确显示文字。在经过了5个小时和AI讨论渲染方案,最终修改制作出了这个高效+无线缩放+仿SDL的OpenGL文字渲染方法类。

完整代码
import freetype
import numpy as np
from OpenGL.GL import *class SDLStyleFontRenderer:def __init__(self, engine, font_path, font_size=24, max_atlas_size=1024):"""字体渲染器参数:engine: 渲染引擎实例font_path: 字体文件路径font_size: 默认字体大小max_atlas_size: 最大图集尺寸"""self.engine = engineself.font_path = font_pathself.default_font_size = font_sizeself.max_atlas_size = max_atlas_size# 字体缓存self.font_cache = {}# 图集缓存self.atlases = {}# 字符信息缓存self.glyph_cache = {}# 加载默认字体self.load_font(font_path, font_size)self.preload_characters("".join(chr(i) for i in range(32, 127)))def load_font(self, font_path, font_size=None):"""加载字体并初始化图集"""if font_size is None:font_size = self.default_font_sizefont_key = (font_path, font_size)if font_key in self.font_cache:return self.font_cache[font_key]# 创建新的字体缓存font_cache = {'face': freetype.Face(font_path),'size': font_size,'atlas_id': None,'glyphs': {},'max_glyphs': 256, # 每个图集最大字符数'ascender': 0,'descender': 0,'line_height': 0}# 设置字体大小font_cache['face'].set_char_size(font_size * 64)# 获取字体度量信息font_cache['ascender'] = font_cache['face'].size.ascender >> 6font_cache['descender'] = font_cache['face'].size.descender >> 6font_cache['line_height'] = font_cache['face'].size.height >> 6# 创建初始图集self._create_atlas_for_font(font_cache)# 缓存字体self.font_cache[font_key] = font_cachereturn font_cachedef _create_atlas_for_font(self, font_cache):"""为字体创建新图集"""atlas_id = len(self.atlases)atlas = {'texture_id': glGenTextures(1),'width': self.max_atlas_size,'height': self.max_atlas_size,'x': 0,'y': 0,'next_row_height': 0,'glyphs': {}}# 初始化纹理glBindTexture(GL_TEXTURE_2D, atlas['texture_id'])glTexImage2D(GL_TEXTURE_2D, 0, GL_RED,atlas['width'] - 1, atlas['height'],0, GL_RED, GL_UNSIGNED_BYTE, None)# 设置纹理参数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)glBindTexture(GL_TEXTURE_2D, 0)# 保存图集self.atlases[atlas_id] = atlasfont_cache['atlas_id'] = atlas_idreturn atlas_iddef _add_glyph_to_atlas(self, font_cache, char):"""添加字符到图集"""atlas_id = font_cache['atlas_id']atlas = self.atlases[atlas_id]# 渲染字符font_cache['face'].load_char(char, freetype.FT_LOAD_RENDER)bitmap = font_cache['face'].glyph.bitmapglyph = font_cache['face'].glyph# 检查是否有足够空间if atlas['x'] + bitmap.width > atlas['width']:# 换行atlas['x'] = 0atlas['y'] += atlas['next_row_height']atlas['next_row_height'] = 0# 检查是否需要新图集if atlas['y'] + bitmap.rows > atlas['height']:# 创建新图集atlas_id = self._create_atlas_for_font(font_cache)atlas = self.atlases[atlas_id]# 上传字符到位图glBindTexture(GL_TEXTURE_2D, atlas['texture_id'])if bitmap.buffer:# 创建临时数组确保数据连续性data = np.frombuffer(bytearray(bitmap.buffer), dtype=np.ubyte)data = data.reshape(bitmap.rows, bitmap.width)glPixelStorei(GL_UNPACK_ALIGNMENT, 1) # 关键:设置1字节对齐glTexSubImage2D(GL_TEXTURE_2D, 0,atlas['x'], atlas['y'],bitmap.width, bitmap.rows,GL_RED, GL_UNSIGNED_BYTE,data)# 创建字符信息glyph_info = {'atlas_id': atlas_id,'x': atlas['x'],'y': atlas['y'],'width': bitmap.width,'height': bitmap.rows,'advance': glyph.advance.x >> 6,'bearing_x': glyph.bitmap_left,'bearing_y': glyph.bitmap_top}# 更新图集位置atlas['x'] += bitmap.widthif bitmap.rows > atlas['next_row_height']:atlas['next_row_height'] = bitmap.rows# 缓存字符信息font_cache['glyphs'][char] = glyph_infoatlas['glyphs'][char] = glyph_infoglBindTexture(GL_TEXTURE_2D, 0)return glyph_infodef get_glyph_info(self, font_path, font_size, char):"""获取字符信息,如果不存在则创建"""font_cache = self.load_font(font_path, font_size)# 检查是否已缓存if char in font_cache['glyphs']:return font_cache['glyphs'][char]# 添加新字符到图集return self._add_glyph_to_atlas(font_cache, char)def render_text(self, text, position, font_path=None, font_size=None,fg_color=(1.0, 1.0, 1.0, 1.0), bg_color=None):"""渲染文本参数:text: 要渲染的文本position: 渲染位置 (x, y)font_path: 字体路径 (None使用默认字体)font_size: 字体大小 (None使用默认大小)fg_color: 前景色 (RGBA)bg_color: 背景色 (RGBA) 或 None(透明背景)"""if font_path is None:font_path = self.font_pathif font_size is None:font_size = self.default_font_sizefont_cache = self.load_font(font_path, font_size)# 设置使用纹理glUseProgram(self.engine.shader)glUniform1i(glGetUniformLocation(self.engine.shader, "useTexture"), 1)# 设置背景色if bg_color is None:bg_color = (0.0, 0.0, 0.0, 0.0)bg_loc = glGetUniformLocation(self.engine.shader, "bgColor")glUniform4f(bg_loc, *bg_color)# 获取基线位置x, y = positiony_base = y + font_cache['ascender']# 创建VAO和VBOvao = glGenVertexArrays(1)vbo = glGenBuffers(1)glBindVertexArray(vao)glBindBuffer(GL_ARRAY_BUFFER, vbo)# 设置顶点属性指针glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 8 * 4, ctypes.c_void_p(0))glEnableVertexAttribArray(0)glVertexAttribPointer(1, 4, GL_FLOAT, GL_FALSE, 8 * 4, ctypes.c_void_p(2 * 4))glEnableVertexAttribArray(1)glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * 4, ctypes.c_void_p(6 * 4))glEnableVertexAttribArray(2)# 当前绑定图集IDcurrent_atlas_id = -1# 渲染每个字符for char in text:glyph_info = self.get_glyph_info(font_path, font_size, char)# 切换到正确的图集if glyph_info['atlas_id'] != current_atlas_id:current_atlas_id = glyph_info['atlas_id']atlas = self.atlases[current_atlas_id]glActiveTexture(GL_TEXTURE0)glBindTexture(GL_TEXTURE_2D, atlas['texture_id'])glUniform1i(glGetUniformLocation(self.engine.shader, "textureSampler"), 0)# 计算字符位置xpos = x + glyph_info['bearing_x']# 重要修复:y位置计算ypos = y_base - glyph_info['bearing_y']# 计算纹理坐标 - 修复纹理翻转问题atlas = self.atlases[glyph_info['atlas_id']]u0 = glyph_info['x'] / atlas['width']v0 = glyph_info['y'] / atlas['height']u1 = (glyph_info['x'] + glyph_info['width']) / atlas['width']v1 = (glyph_info['y'] + glyph_info['height']) / atlas['height']# 重要修复:顶点位置和纹理坐标vertices = np.array([# 位置 颜色 纹理坐标# 左下角xpos, ypos + glyph_info[&