From ec934bf2f0f3536c1b4d31b4ca002f6f38ada9fe Mon Sep 17 00:00:00 2001 From: rom Date: Sat, 24 Apr 2021 03:19:22 +0200 Subject: font rendering --- txtgameengine/__main__.py | 12 +++-- .../builtin_res/shaders/font/fragment.glsl | 12 +++++ txtgameengine/builtin_res/shaders/font/vertex.glsl | 12 +++++ txtgameengine/fonts.py | 61 ++++++++++++++++++++-- txtgameengine/platform.py | 13 +++-- txtgameengine/shaders/__init__.py | 2 +- txtgameengine/shaders/integrated.py | 6 +++ 7 files changed, 106 insertions(+), 12 deletions(-) create mode 100644 txtgameengine/builtin_res/shaders/font/fragment.glsl create mode 100644 txtgameengine/builtin_res/shaders/font/vertex.glsl diff --git a/txtgameengine/__main__.py b/txtgameengine/__main__.py index 71085e1..7aaa64f 100644 --- a/txtgameengine/__main__.py +++ b/txtgameengine/__main__.py @@ -1,5 +1,6 @@ import numpy as np +from .fonts import TextRenderer, BitmapFont from .scenes import SceneTxtGameApp, Scene from pathlib import Path from .shaders import TextureShader @@ -54,20 +55,23 @@ class TextureScene(Scene): -1.0, 1.0, 1.0, 1.0, -1.0, -1.0, + 1.0, -1.0, ], np.float32)) self.uvs = self.app.render.setup_buffer( np.array([ 0, 0, 1, 0, 0, 1, + 1, 1, ], np.float32)) + self.text_renderer = TextRenderer(self.app) + self.text_renderer.use_font(BitmapFont.fira_mono(self.app)) def update(self, delta: float): - print(self.app.coords.from_pixels_to_screen(0, 0)) - print(self.app.coords.from_screen_to_pixels(0, 0)) with self.texture_shaders: - self.app.render.textured_triangle(self.texture_shaders.textureSampler, self.texture, self.triangle, - self.uvs) + self.app.render.bind_texture(self.texture_shaders.textureSampler, self.texture) + self.app.render.textured_triangle(self.triangle, self.uvs, 4) + self.text_renderer.render_text(0, 0, "HEE") class TestApp(SceneTxtGameApp): diff --git a/txtgameengine/builtin_res/shaders/font/fragment.glsl b/txtgameengine/builtin_res/shaders/font/fragment.glsl new file mode 100644 index 0000000..d9d36fd --- /dev/null +++ b/txtgameengine/builtin_res/shaders/font/fragment.glsl @@ -0,0 +1,12 @@ +#version 330 core + +in vec2 UV; + +out vec3 color; + +uniform sampler2D textureSampler; + +void main() { + color = texture(textureSampler, UV).rgb; +} + diff --git a/txtgameengine/builtin_res/shaders/font/vertex.glsl b/txtgameengine/builtin_res/shaders/font/vertex.glsl new file mode 100644 index 0000000..dd93afb --- /dev/null +++ b/txtgameengine/builtin_res/shaders/font/vertex.glsl @@ -0,0 +1,12 @@ +#version 330 core + +layout(location=0) in vec3 vertexPosition_modelspace; +layout(location=1) in vec2 vertexUV; + +out vec2 UV; + +void main() { + gl_Position = vec4(vertexPosition_modelspace, 1); + UV = vertexUV; +} + diff --git a/txtgameengine/fonts.py b/txtgameengine/fonts.py index fcbf8b5..afa9d87 100644 --- a/txtgameengine/fonts.py +++ b/txtgameengine/fonts.py @@ -5,12 +5,16 @@ from typing import Optional import typing import xml.dom.minidom as minidom +from .shaders import FontShader + if typing.TYPE_CHECKING: from .twod import Texture from .app import TxtGameApp class Font(ABC): + texture: 'Texture' + def __init__(self): self.glyphs = dict() @@ -31,14 +35,15 @@ class Glyph: class BitmapFont(Font): - def __init__(self, texture: Texture, xml_path: str): + def __init__(self, texture: 'Texture', xml_path: str): + super().__init__() self.texture = texture - dom = minidom.parse(xml_path) + dom = minidom.parse(str(xml_path)) for char in dom.getElementsByTagName('Char'): code = char.attributes['code'].value width = char.attributes['width'].value x_render_offset, y_render_offset = char.attributes['offset'].value.split(' ') - x_texture_offset, y_texture_offset, x_texture_width, y_texture_width = char.attributes['rect'].split(' ') + x_texture_offset, y_texture_offset, x_texture_width, y_texture_width = char.attributes['rect'].value.split(' ') self.glyphs[code] = \ Glyph(self, int(x_texture_offset), int(y_texture_offset), int(x_texture_width), int(y_texture_width), int(x_render_offset), int(y_render_offset), int(width)) @@ -48,10 +53,60 @@ class BitmapFont(Font): from .twod import Texture return cls(Texture(app, image_path), xml_path) + @classmethod + def fira_mono(cls, app: 'TxtGameApp'): + from .app import builtin_resource_path + return cls.load(app, builtin_resource_path / 'fonts/fira_code/regular.png', + builtin_resource_path / 'fonts/fira_code/regular.xml') + class TextRenderer: def __init__(self, app: 'TxtGameApp'): self.app = app + self.font: Font = None + self.shader = FontShader(app) def use_font(self, font: Font): self.font = font + + def render_text(self, x: int, y: int, text: str) -> typing.Tuple[int, int]: + """No support for newlines""" + if self.font is None: + raise ValueError("No font set") + glyphs = list(map(self.font.get_glyph, text)) + missing = [t for t, g in zip(text, glyphs) if g is None] + if missing: + raise ValueError("No glyph found for character '%s'" % missing[0]) + with self.shader: + self.app.render.bind_texture(self.shader.textureSampler, self.font.texture) + for glyph in glyphs: + x, y = self._render_glyph(glyph, x, y) + return x, y + + def _render_glyph(self, glyph: Glyph, x: int, y: int) -> typing.Tuple[int, int]: + low_x = x + glyph.x_render_offset + high_x = low_x + glyph.x_texture_width + low_y = y + glyph.y_render_offset + high_y = low_y + glyph.y_texture_width + low_x, low_y = self.app.coords.from_pixels_to_screen(low_x, low_y) + high_x, high_y = self.app.coords.from_pixels_to_screen(high_x, high_y) + tex_low_x = glyph.x_texture_offset + tex_high_x = tex_low_x + glyph.x_texture_width + tex_low_y = glyph.y_texture_offset + tex_high_y = tex_low_y + glyph.y_texture_width + tex_low_x, tex_low_y = self.app.coords.from_pixels_to_screen(tex_low_x, tex_low_y) + tex_high_x, tex_high_y = self.app.coords.from_pixels_to_screen(tex_high_x, tex_high_y) + render = self.app.render.setup_buffer([ + low_x, low_y, + high_x, low_y, + low_x, high_y, + high_x, high_y, + ]) + uvs = self.app.render.setup_buffer([ + tex_low_x, tex_low_y, + tex_high_x, tex_low_y, + tex_low_x, tex_high_y, + tex_high_x, tex_high_y, + ]) + self.app.render.textured_triangle(render, uvs, 4) + return x + glyph.x_advance, y diff --git a/txtgameengine/platform.py b/txtgameengine/platform.py index 8258d33..bcf3d10 100644 --- a/txtgameengine/platform.py +++ b/txtgameengine/platform.py @@ -149,6 +149,8 @@ class RenderComponent: @staticmethod def setup_buffer(arr, mode=GL_STATIC_DRAW): + if not hasattr(arr, 'itemsize'): + arr = np.array(arr, np.float32) buf = glGenBuffers(1) glBindBuffer(GL_ARRAY_BUFFER, buf) glBufferData(GL_ARRAY_BUFFER, arr.itemsize * @@ -156,21 +158,24 @@ class RenderComponent: return buf @staticmethod - def triangle(buf): + def triangle(buf, count=3): glEnableVertexAttribArray(0) glBindBuffer(GL_ARRAY_BUFFER, buf) glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 0, None) - glDrawArrays(GL_TRIANGLES, 0, 3) + glDrawArrays(GL_TRIANGLE_STRIP, 0, count) glDisableVertexAttribArray(0) - def textured_triangle(self, shader_location, texture, triangle, uvs): + @staticmethod + def bind_texture(shader_location, texture): glActiveTexture(GL_TEXTURE0) glBindTexture(GL_TEXTURE_2D, texture.gl_texid) glUniform1i(shader_location, 0) + + def textured_triangle(self, triangle, uvs, count=3): glEnableVertexAttribArray(1) glBindBuffer(GL_ARRAY_BUFFER, uvs) glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 0, None) - self.triangle(triangle) + self.triangle(triangle, count) glDisableVertexAttribArray(1) def setup_texture(self, width: int, height: int, data: np.ndarray): diff --git a/txtgameengine/shaders/__init__.py b/txtgameengine/shaders/__init__.py index 64b0cc8..989ba98 100644 --- a/txtgameengine/shaders/__init__.py +++ b/txtgameengine/shaders/__init__.py @@ -1,2 +1,2 @@ from .shader import Shader -from .integrated import TextureShader, BasicShader +from .integrated import TextureShader, BasicShader, FontShader diff --git a/txtgameengine/shaders/integrated.py b/txtgameengine/shaders/integrated.py index d9e5ae5..180225a 100644 --- a/txtgameengine/shaders/integrated.py +++ b/txtgameengine/shaders/integrated.py @@ -14,3 +14,9 @@ class TextureShader(Shader): UNIFORMS = dict(textureSampler="textureSampler") VERTEX_PATH = shader_base_path / 'texture/vertex.glsl' FRAGMENT_PATH = shader_base_path / 'texture/fragment.glsl' + + +class FontShader(Shader): + UNIFORMS = dict(textureSampler="textureSampler") + VERTEX_PATH = shader_base_path / 'font/vertex.glsl' + FRAGMENT_PATH = shader_base_path / 'font/fragment.glsl' -- cgit