ncca-ngl 0.1.1__py3-none-any.whl → 0.1.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. ncca/ngl/.ruff_cache/.gitignore +2 -0
  2. ncca/ngl/.ruff_cache/0.13.0/10564494386971134025 +0 -0
  3. ncca/ngl/.ruff_cache/0.13.0/7783445477288392980 +0 -0
  4. ncca/ngl/.ruff_cache/CACHEDIR.TAG +1 -0
  5. ncca/ngl/PrimData/Primitives.npz +0 -0
  6. ncca/ngl/PrimData/buddah.npy +0 -0
  7. ncca/ngl/PrimData/bunny.npy +0 -0
  8. ncca/ngl/PrimData/cube.npy +0 -0
  9. ncca/ngl/PrimData/dodecahedron.npy +0 -0
  10. ncca/ngl/PrimData/dragon.npy +0 -0
  11. ncca/ngl/PrimData/football.npy +0 -0
  12. ncca/ngl/PrimData/icosahedron.npy +0 -0
  13. ncca/ngl/PrimData/octahedron.npy +0 -0
  14. ncca/ngl/PrimData/pack_arrays.py +20 -0
  15. ncca/ngl/PrimData/teapot.npy +0 -0
  16. ncca/ngl/PrimData/tetrahedron.npy +0 -0
  17. ncca/ngl/PrimData/troll.npy +0 -0
  18. ncca/ngl/__init__.py +100 -0
  19. ncca/ngl/abstract_vao.py +85 -0
  20. ncca/ngl/base_mesh.py +170 -0
  21. ncca/ngl/base_mesh.pyi +11 -0
  22. ncca/ngl/bbox.py +224 -0
  23. ncca/ngl/bezier_curve.py +75 -0
  24. ncca/ngl/first_person_camera.py +174 -0
  25. ncca/ngl/image.py +94 -0
  26. ncca/ngl/log.py +44 -0
  27. ncca/ngl/mat2.py +128 -0
  28. ncca/ngl/mat3.py +466 -0
  29. ncca/ngl/mat4.py +456 -0
  30. ncca/ngl/multi_buffer_vao.py +49 -0
  31. ncca/ngl/obj.py +416 -0
  32. ncca/ngl/plane.py +47 -0
  33. ncca/ngl/primitives.py +706 -0
  34. ncca/ngl/pyside_event_handling_mixin.py +318 -0
  35. ncca/ngl/quaternion.py +112 -0
  36. ncca/ngl/random.py +167 -0
  37. ncca/ngl/shader.py +229 -0
  38. ncca/ngl/shader_lib.py +536 -0
  39. ncca/ngl/shader_program.py +785 -0
  40. ncca/ngl/shaders/checker_fragment.glsl +35 -0
  41. ncca/ngl/shaders/checker_vertex.glsl +19 -0
  42. ncca/ngl/shaders/colour_fragment.glsl +8 -0
  43. ncca/ngl/shaders/colour_vertex.glsl +11 -0
  44. ncca/ngl/shaders/diffuse_fragment.glsl +21 -0
  45. ncca/ngl/shaders/diffuse_vertex.glsl +24 -0
  46. ncca/ngl/shaders/text_fragment.glsl +10 -0
  47. ncca/ngl/shaders/text_geometry.glsl +53 -0
  48. ncca/ngl/shaders/text_vertex.glsl +18 -0
  49. ncca/ngl/simple_index_vao.py +65 -0
  50. ncca/ngl/simple_vao.py +42 -0
  51. ncca/ngl/text.py +342 -0
  52. ncca/ngl/texture.py +75 -0
  53. ncca/ngl/transform.py +95 -0
  54. ncca/ngl/util.py +128 -0
  55. ncca/ngl/vao_factory.py +34 -0
  56. ncca/ngl/vec2.py +350 -0
  57. ncca/ngl/vec2_array.py +124 -0
  58. ncca/ngl/vec3.py +401 -0
  59. ncca/ngl/vec3_array.py +128 -0
  60. ncca/ngl/vec4.py +229 -0
  61. ncca/ngl/vec4_array.py +124 -0
  62. ncca_ngl-0.1.4.dist-info/METADATA +22 -0
  63. ncca_ngl-0.1.4.dist-info/RECORD +64 -0
  64. ncca_ngl-0.1.4.dist-info/WHEEL +4 -0
  65. ncca_ngl-0.1.1.dist-info/METADATA +0 -23
  66. ncca_ngl-0.1.1.dist-info/RECORD +0 -4
  67. ncca_ngl-0.1.1.dist-info/WHEEL +0 -4
  68. ncca_ngl-0.1.1.dist-info/licenses/LICENSE.txt +0 -7
@@ -0,0 +1,35 @@
1
+ #version 410 core
2
+ in vec3 fragmentNormal;
3
+ in vec2 uv;
4
+
5
+ layout (location =0) out vec4 fragColour;
6
+ uniform vec4 colour1;
7
+ uniform vec4 colour2;
8
+
9
+ uniform vec3 lightPos;
10
+ uniform vec4 lightDiffuse;
11
+ uniform float checkSize=10.0;
12
+ uniform bool checkOn;
13
+
14
+ vec4 checker( vec2 uv )
15
+ {
16
+ if(checkOn == false)
17
+ return colour1;
18
+ else
19
+ {
20
+ float v = floor( checkSize * uv.x ) +floor( checkSize * uv.y );
21
+ if( mod( v, 2.0 ) < 1.0 )
22
+ return colour2;
23
+ else
24
+ return colour1;
25
+
26
+ }
27
+ }
28
+
29
+ void main ()
30
+ {
31
+ fragColour= vec4(0.);
32
+ vec3 N = normalize(fragmentNormal);
33
+ vec3 L = normalize(lightPos);
34
+ fragColour += checker(uv)*lightDiffuse *dot(L, N);
35
+ }
@@ -0,0 +1,19 @@
1
+ #version 410 core
2
+
3
+
4
+ /// @brief the vertex passed in
5
+ layout (location = 0) in vec3 inVert;
6
+ /// @brief the normal passed in
7
+ layout (location = 1) in vec3 inNormal;
8
+ /// @brief the in uv
9
+ layout (location = 2) in vec2 inUV;
10
+ out vec3 fragmentNormal;
11
+ out vec2 uv;
12
+ uniform mat4 MVP;
13
+ uniform mat3 normalMatrix;
14
+ void main()
15
+ {
16
+ fragmentNormal = (normalMatrix*inNormal);
17
+ uv=inUV;
18
+ gl_Position = MVP*vec4(inVert,1.0);
19
+ }
@@ -0,0 +1,8 @@
1
+ #version 410 core
2
+ uniform vec4 Colour;
3
+ layout(location=0) out vec4 outColour;
4
+
5
+ void main ()
6
+ {
7
+ outColour = Colour;
8
+ }
@@ -0,0 +1,11 @@
1
+ #version 410 core
2
+
3
+ uniform mat4 MVP;
4
+
5
+ layout(location=0) in vec3 inVert;
6
+ uniform vec4 Colour;
7
+
8
+ void main(void)
9
+ {
10
+ gl_Position = MVP*vec4(inVert, 1.0);
11
+ }
@@ -0,0 +1,21 @@
1
+ #version 410
2
+ in vec3 fragmentNormal;
3
+ in vec3 fragmentPosition; // Receive fragment position
4
+
5
+ layout (location =0) out vec4 fragColour;
6
+
7
+ uniform vec4 Colour;
8
+ uniform vec3 lightPos; // Light's position in view space
9
+ uniform vec4 lightDiffuse;
10
+
11
+ void main ()
12
+ {
13
+ // Ensure fragment normal is unit length
14
+ vec3 N = normalize(fragmentNormal);
15
+ // Calculate vector from fragment to light
16
+ vec3 L = normalize(lightPos - fragmentPosition);
17
+ // Calculate diffuse factor, ensuring it's not negative
18
+ float diffuse = max(dot(L, N), 0.0);
19
+ // Final colour
20
+ fragColour = Colour * lightDiffuse * diffuse;
21
+ }
@@ -0,0 +1,24 @@
1
+ #version 410
2
+ out vec3 fragmentNormal;
3
+ out vec3 fragmentPosition;
4
+
5
+ layout(location=0) in vec3 inVert;
6
+ layout(location=1) in vec3 inNormal;
7
+
8
+ uniform mat4 MVP;
9
+ uniform mat4 MV;
10
+ uniform mat3 normalMatrix;
11
+
12
+ void main()
13
+ {
14
+ // Transform normal into view space but DO NOT normalize it here.
15
+ // The interpolation and per-fragment normalization is key to smooth shading.
16
+ fragmentNormal = normalMatrix * inNormal;
17
+
18
+ // Transform vertex position into view space
19
+ vec4 viewPosition = MV * vec4(inVert, 1.0);
20
+ fragmentPosition = viewPosition.xyz;
21
+
22
+ // Transform vertex to clip space
23
+ gl_Position = MVP * vec4(inVert, 1.0);
24
+ }
@@ -0,0 +1,10 @@
1
+ #version 410 core
2
+ in vec2 v_uv;
3
+ uniform sampler2D textureID;
4
+ uniform vec4 textColour;
5
+ out vec4 fragColor;
6
+ void main()
7
+ {
8
+ float a = texture(textureID, v_uv).a;
9
+ fragColor = vec4(textColour.rgb, textColour.a * a);
10
+ }
@@ -0,0 +1,53 @@
1
+ #version 410 core
2
+ layout(points) in;
3
+ layout(triangle_strip, max_vertices=4) out;
4
+
5
+ uniform vec2 screenSize;
6
+ uniform float fontSize;
7
+
8
+ in VS_OUT
9
+ {
10
+ vec2 pos;
11
+ vec4 uvRect;
12
+ vec2 size;
13
+ }
14
+ gs_in[];
15
+
16
+ out vec2 v_uv;
17
+
18
+ vec2 toNDC(vec2 screenPos)
19
+ {
20
+ return vec2(
21
+ (screenPos.x / screenSize.x) * 2.0 - 1.0,
22
+ 1.0 - (screenPos.y /screenSize.y) * 2.0
23
+ );
24
+ }
25
+
26
+ void main()
27
+ {
28
+ vec2 base = gs_in[0].pos;
29
+ vec2 gsize = gs_in[0].size * fontSize;
30
+ vec4 uv = gs_in[0].uvRect;
31
+ // generate a quad
32
+ // Top Left
33
+ gl_Position = vec4(toNDC(base), 0.0, 1.0);
34
+ v_uv = uv.xy;
35
+ EmitVertex();
36
+
37
+ // Bottom Left
38
+ gl_Position = vec4(toNDC(base + vec2(0.0, gsize.y)), 0.0, 1.0);
39
+ v_uv = vec2(uv.x, uv.w);
40
+ EmitVertex();
41
+
42
+ // Top Right
43
+ gl_Position = vec4(toNDC(base + vec2(gsize.x, 0.0)), 0.0, 1.0);
44
+ v_uv = vec2(uv.z, uv.y);
45
+ EmitVertex();
46
+
47
+ // Bottom Right
48
+ gl_Position = vec4(toNDC(base + gsize), 0.0, 1.0);
49
+ v_uv = uv.zw;
50
+ EmitVertex();
51
+
52
+ EndPrimitive();
53
+ }
@@ -0,0 +1,18 @@
1
+ #version 410 core
2
+ layout(location=0) in vec2 position;
3
+ layout(location=1) in vec4 uvRect;
4
+ layout(location=2) in vec2 size;
5
+
6
+ out VS_OUT
7
+ {
8
+ vec2 pos;
9
+ vec4 uvRect;
10
+ vec2 size;
11
+ } vs_out;
12
+
13
+ void main()
14
+ {
15
+ vs_out.pos = position;
16
+ vs_out.uvRect = uvRect;
17
+ vs_out.size = size;
18
+ }
@@ -0,0 +1,65 @@
1
+ import numpy as np
2
+ import OpenGL.GL as gl
3
+
4
+ from .abstract_vao import AbstractVAO, VertexData
5
+ from .log import logger
6
+
7
+
8
+ class IndexVertexData(VertexData):
9
+ def __init__(self, data, size, indices, index_type, mode=gl.GL_STATIC_DRAW):
10
+ super().__init__(data, size, mode)
11
+ gl.GL_to_numpy_type = {
12
+ gl.GL_UNSIGNED_INT: np.uint32,
13
+ gl.GL_UNSIGNED_SHORT: np.uint16,
14
+ gl.GL_UNSIGNED_BYTE: np.uint8,
15
+ }
16
+ numpy_dtype = gl.GL_to_numpy_type.get(index_type)
17
+ if numpy_dtype is None:
18
+ logger.error("SimpleIndexVAO: Unsupported index type")
19
+ raise TypeError(f"Unsupported index type: {index_type}")
20
+
21
+ self.indices = np.array(indices, dtype=numpy_dtype)
22
+ self.index_type = index_type
23
+
24
+
25
+ class SimpleIndexVAO(AbstractVAO):
26
+ def __init__(self, mode=gl.GL_TRIANGLES):
27
+ super().__init__(mode)
28
+ self.buffer = gl.glGenBuffers(1)
29
+ self.idx_buffer = gl.glGenBuffers(1)
30
+ self.index_type = gl.GL_UNSIGNED_INT
31
+
32
+ def draw(self):
33
+ if self.bound and self.allocated:
34
+ gl.glDrawElements(self.mode, self.indices_count, self.index_type, None)
35
+ else:
36
+ logger.error("SimpleIndexVAO not bound or not allocated")
37
+
38
+ def set_data(self, data):
39
+ if not isinstance(data, IndexVertexData):
40
+ logger.error("SimpleIndexVAO: Unsupported index type")
41
+ raise TypeError("data must be of type IndexVertexData")
42
+
43
+ gl.glBindBuffer(gl.GL_ARRAY_BUFFER, self.buffer)
44
+ gl.glBufferData(gl.GL_ARRAY_BUFFER, data.data.nbytes, data.data, data.mode)
45
+
46
+ gl.glBindBuffer(gl.GL_ELEMENT_ARRAY_BUFFER, self.idx_buffer)
47
+ gl.glBufferData(
48
+ gl.GL_ELEMENT_ARRAY_BUFFER, data.indices.nbytes, data.indices, data.mode
49
+ )
50
+
51
+ self.allocated = True
52
+ self.indices_count = len(data.indices)
53
+ self.index_type = data.index_type
54
+
55
+ def remove_vao(self):
56
+ gl.glDeleteBuffers(1, [self.buffer])
57
+ gl.glDeleteBuffers(1, [self.idx_buffer])
58
+ gl.glDeleteVertexArrays(1, [self.id])
59
+
60
+ def get_buffer_id(self, index=0):
61
+ return self.buffer
62
+
63
+ def map_buffer(self, index=0, access_mode=gl.GL_READ_WRITE):
64
+ gl.glBindBuffer(gl.GL_ARRAY_BUFFER, self.buffer)
65
+ return gl.glMapBuffer(gl.GL_ARRAY_BUFFER, access_mode)
ncca/ngl/simple_vao.py ADDED
@@ -0,0 +1,42 @@
1
+ import OpenGL.GL as gl
2
+
3
+ from .abstract_vao import AbstractVAO, VertexData
4
+ from .log import logger
5
+
6
+
7
+ class SimpleVAO(AbstractVAO):
8
+ def __init__(self, mode=gl.GL_TRIANGLES):
9
+ super().__init__(mode)
10
+ self.buffer = gl.glGenBuffers(1)
11
+
12
+ def draw(self):
13
+ if self.bound and self.allocated:
14
+ gl.glDrawArrays(self.mode, 0, self.indices_count)
15
+ else:
16
+ logger.error("SimpleVAO not bound or not allocated")
17
+
18
+ def set_data(self, data):
19
+ if not isinstance(data, VertexData):
20
+ logger.error("SimpleVAO: Invalid data type")
21
+ raise TypeError("data must be of type VertexData")
22
+ if not self.bound:
23
+ logger.error("SimpleVAO not bound")
24
+ raise RuntimeError("SimpleVAO not bound")
25
+ gl.glBindBuffer(gl.GL_ARRAY_BUFFER, self.buffer)
26
+ gl.glBufferData(gl.GL_ARRAY_BUFFER, data.data.nbytes, data.data, data.mode)
27
+ self.allocated = True
28
+ self.indices_count = data.size
29
+
30
+ def num_indices(self):
31
+ return self.indices_count
32
+
33
+ def remove_vao(self):
34
+ gl.glDeleteBuffers(1, [self.buffer])
35
+ gl.glDeleteVertexArrays(1, [self.id])
36
+
37
+ def get_buffer_id(self, index=0):
38
+ return self.buffer
39
+
40
+ def map_buffer(self, index=0, access_mode=gl.GL_READ_WRITE):
41
+ gl.glBindBuffer(gl.GL_ARRAY_BUFFER, self.buffer)
42
+ return gl.glMapBuffer(gl.GL_ARRAY_BUFFER, access_mode)
ncca/ngl/text.py ADDED
@@ -0,0 +1,342 @@
1
+ """
2
+ A module for rendering text in OpenGL using pre-rendered font atlases.
3
+
4
+ This implementation uses the 'freetype-py' library to rasterize characters
5
+ from a given font file into a single texture atlas. This atlas is then
6
+ used by a set of shaders (vertex, geometry, and fragment) to render
7
+ text efficiently.
8
+
9
+ The process is as follows:
10
+ 1. FontAtlas class:
11
+ - Loads a font file.
12
+ - Renders glyphs for a range of characters (ASCII ' ' to '~').
13
+ - Packs these glyphs into a single large texture atlas in memory.
14
+ - Calculates and stores metadata for each glyph (size, bearing,
15
+ advance, and UV coordinates within the atlas).
16
+ - Generates a single OpenGL texture for the atlas.
17
+
18
+ 2. _Text class (exported as Text):
19
+ - Manages multiple fonts by creating and storing FontAtlas objects.
20
+ - Provides a `render_dynamic_text` method to draw text strings.
21
+ - `_build_instances`: For a given string, this method generates a
22
+ list of vertex attributes for each character. Each character is
23
+ represented as a single point with attributes for position, UVs,
24
+ and size.
25
+ - `render_dynamic_text`: This method sends the generated instance
26
+ data to the GPU and draws it using GL_POINTS.
27
+
28
+ 3. Shaders:
29
+ - Vertex Shader: A simple pass-through shader that sends point
30
+ data to the geometry shader.
31
+ - Geometry Shader: Receives points and generates a textured quad
32
+ for each character on the fly.
33
+ - Fragment Shader: Samples the font atlas texture to color the
34
+ quad, effectively drawing the character.
35
+ """
36
+
37
+ from typing import Any, Dict, List
38
+
39
+ import freetype
40
+ import numpy as np
41
+ import OpenGL.GL as gl
42
+
43
+ from .log import logger
44
+ from .shader_lib import DefaultShader, ShaderLib
45
+ from .simple_vao import VertexData
46
+ from .vao_factory import VAOFactory, VAOType
47
+ from .vec3 import Vec3
48
+
49
+
50
+ class FontAtlas:
51
+ """
52
+ Manages the creation of a font texture atlas for efficient text rendering.
53
+
54
+ This class uses FreeType to render glyphs for a specified font and packs them
55
+ into a single texture. It also stores metadata for each glyph.
56
+ """
57
+
58
+ def __init__(self, font_path: str, font_size: int = 48, debug: bool = False):
59
+ """
60
+ Initializes the FontAtlas.
61
+
62
+ Args:
63
+ font_path: The file path to the font (e.g., a .ttf file).
64
+ font_size: The font size in pixels to be used for rendering the atlas.
65
+ debug: If True, saves the generated atlas as a PNG for debugging.
66
+ """
67
+ try:
68
+ self.face = freetype.Face(font_path)
69
+ self.face.set_pixel_sizes(0, font_size)
70
+ self.font_size: int = font_size
71
+ self.glyphs: Dict[str, Dict[str, Any]] = {}
72
+ self.texture: int = 0
73
+ self.atlas_w: int = 0
74
+ self.atlas_h: int = 0
75
+ self.atlas: np.ndarray | None = None
76
+ self.build_atlas(debug)
77
+
78
+ except freetype.FT_Exception as e:
79
+ logger.error(f"{font_path} could not be loaded {e}")
80
+
81
+ def __str__(self) -> str:
82
+ """Returns a string representation of the FontAtlas."""
83
+ return f"TextureID: {self.texture}, FontSize: {self.font_size}"
84
+
85
+ def build_atlas(self, debug: bool = False) -> None:
86
+ """
87
+ Renders characters and packs them into a texture atlas.
88
+
89
+ This method iterates through ASCII characters 32-126, renders each one
90
+ using FreeType, and arranges them in a single large numpy array which
91
+ will later be used to create an OpenGL texture.
92
+
93
+ Args:
94
+ debug: If True, saves the generated atlas as 'debug_atlas.png'.
95
+ """
96
+ padding = 2 # Padding between glyphs in the atlas
97
+ atlas_w = 1024 # Fixed width for the atlas texture
98
+ x, y, row_h = 0, 0, 0
99
+ bitmaps_data = []
100
+
101
+ # Iterate through printable ASCII characters
102
+ for charcode in range(ord(" "), ord("~")):
103
+ self.face.load_char(chr(charcode), freetype.FT_LOAD_RENDER)
104
+ bmp = self.face.glyph.bitmap
105
+ w, h = bmp.width, bmp.rows
106
+
107
+ # Move to the next row if the current glyph doesn't fit
108
+ if x + w + padding > atlas_w:
109
+ x = 0
110
+ y += row_h + padding
111
+ row_h = 0
112
+
113
+ # Copy bitmap data as the buffer is overwritten for each glyph
114
+ if w > 0 and h > 0:
115
+ buffer_copy = np.array(bmp.buffer, dtype=np.ubyte).reshape(h, w)
116
+ bitmaps_data.append((buffer_copy, x, y))
117
+
118
+ # Store glyph metadata
119
+ self.glyphs[chr(charcode)] = {
120
+ "size": (w, h),
121
+ "bearing": (self.face.glyph.bitmap_left, self.face.glyph.bitmap_top),
122
+ "advance": self.face.glyph.advance.x >> 6, # Advance is in 1/64 pixels
123
+ "uv": (x, y, x + w, y + h), # UVs in pixel coordinates
124
+ }
125
+ x += w + padding
126
+ row_h = max(row_h, h)
127
+
128
+ atlas_h = y + row_h + padding
129
+ self.atlas_w, self.atlas_h = atlas_w, atlas_h
130
+ atlas = np.zeros((atlas_h, atlas_w), dtype=np.ubyte)
131
+
132
+ # Blit all the individual glyph bitmaps onto the atlas
133
+ for arr, dest_x, dest_y in bitmaps_data:
134
+ h, w = arr.shape
135
+ atlas[dest_y : dest_y + h, dest_x : dest_x + w] = arr
136
+
137
+ self.atlas = atlas
138
+ if debug:
139
+ from PIL import Image
140
+
141
+ img = Image.fromarray(self.atlas, mode="L")
142
+ img.save("debug_atlas.png")
143
+ print(f"Saved debug_atlas.png, size: {self.atlas.shape}")
144
+
145
+ def generate_texture(self) -> None:
146
+ """Generates and configures the OpenGL texture for the font atlas."""
147
+ if self.atlas is None:
148
+ return
149
+ tex = gl.glGenTextures(1)
150
+ gl.glPixelStorei(gl.GL_UNPACK_ALIGNMENT, 1)
151
+
152
+ gl.glBindTexture(gl.GL_TEXTURE_2D, tex)
153
+ # Create a single-channel RED texture from our numpy atlas
154
+ gl.glTexImage2D(
155
+ gl.GL_TEXTURE_2D,
156
+ 0,
157
+ gl.GL_RED,
158
+ self.atlas_w,
159
+ self.atlas_h,
160
+ 0,
161
+ gl.GL_RED,
162
+ gl.GL_UNSIGNED_BYTE,
163
+ self.atlas,
164
+ )
165
+
166
+ gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MIN_FILTER, gl.GL_LINEAR)
167
+ gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MAG_FILTER, gl.GL_LINEAR)
168
+ gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_WRAP_S, gl.GL_CLAMP_TO_EDGE)
169
+ gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_WRAP_T, gl.GL_CLAMP_TO_EDGE)
170
+
171
+ # Use texture swizzling to use the RED channel as ALPHA.
172
+ # This allows us to color the font using a uniform in the shader,
173
+ # while using the glyph's intensity for transparency.
174
+ # We set the texture's RGB channels to 1.0, and the A channel to the
175
+ # value from the RED channel of the source.
176
+ gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_SWIZZLE_R, gl.GL_ONE)
177
+ gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_SWIZZLE_G, gl.GL_ONE)
178
+ gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_SWIZZLE_B, gl.GL_ONE)
179
+ gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_SWIZZLE_A, gl.GL_RED)
180
+
181
+ self.texture = tex
182
+
183
+
184
+ class _Text:
185
+ """
186
+ Main class for managing and rendering text.
187
+
188
+ This class acts as a controller, loading fonts and providing methods
189
+ to render text strings to the screen. It is designed to be used as a
190
+ singleton instance.
191
+ """
192
+
193
+ def __init__(self) -> None:
194
+ """Initializes the Text renderer."""
195
+ self._fonts: Dict[str, FontAtlas] = {}
196
+ self._static_text: List[Any] = [] # Reserved for future use
197
+
198
+ def add_font(self, name: str, font_file: str, size: int) -> None:
199
+ """
200
+ Loads a font and makes it available for rendering.
201
+
202
+ Args:
203
+ name: A unique name to identify this font (e.g., "main_font").
204
+ font_file: The path to the font file.
205
+ size: The font size in pixels.
206
+ """
207
+ if not hasattr(self, "vao"):
208
+ self.vao = VAOFactory.create_vao(VAOType.SIMPLE, gl.GL_POINTS)
209
+ font = FontAtlas(font_file, size)
210
+ font.generate_texture()
211
+ print(f"Font '{name}' added with texture ID: {font.texture}")
212
+ self._fonts[name] = font
213
+
214
+ def set_screen_size(self, w: int, h: int) -> None:
215
+ """
216
+ Sets the screen dimensions for the text shader.
217
+
218
+ This should be called whenever the window is resized.
219
+
220
+ Args:
221
+ w: The width of the screen in pixels.
222
+ h: The height of the screen in pixels.
223
+ """
224
+ ShaderLib.use(DefaultShader.TEXT)
225
+ ShaderLib.set_uniform("textureID", 0)
226
+ ShaderLib.set_uniform("screenSize", float(w), float(h))
227
+ ShaderLib.set_uniform("fontSize", 1.0)
228
+ ShaderLib.set_uniform("textColour", 1.0, 1.0, 1.0, 1.0)
229
+
230
+ def render_text(self, font: str, x: int, y: int, text: str, colour: Vec3 = Vec3(1.0, 1.0, 1.0)) -> None:
231
+ """
232
+ Renders a string of text to the screen.
233
+
234
+ Args:
235
+ font: The name of the font to use (previously added with add_font).
236
+ x: The x-coordinate of the starting position (baseline).
237
+ y: The y-coordinate of the starting position (baseline).
238
+ text: The string of text to render.
239
+ colour: The color of the text as a Vec4.
240
+ """
241
+ render_data = self._build_instances(font, text, x, y)
242
+ if not render_data:
243
+ return
244
+
245
+ buffer_data = np.array(render_data, dtype=np.float32)
246
+ atlas = self._fonts[font]
247
+
248
+ # Enable blending for transparency
249
+ gl.glEnable(gl.GL_BLEND)
250
+ gl.glBlendFunc(gl.GL_SRC_ALPHA, gl.GL_ONE_MINUS_SRC_ALPHA)
251
+
252
+ # Ensure text is rendered filled and restore state afterwards.
253
+ polygon_mode = gl.glGetIntegerv(gl.GL_POLYGON_MODE)[0]
254
+ if polygon_mode != gl.GL_FILL:
255
+ gl.glPolygonMode(gl.GL_FRONT_AND_BACK, gl.GL_FILL)
256
+
257
+ # Disable depth testing to ensure text is always drawn on top
258
+ depth_test_enabled = gl.glIsEnabled(gl.GL_DEPTH_TEST)
259
+ if depth_test_enabled:
260
+ gl.glDisable(gl.GL_DEPTH_TEST)
261
+
262
+ with self.vao as vao:
263
+ data = VertexData(data=buffer_data, size=buffer_data.nbytes)
264
+ stride = 32 # 8 floats * 4 bytes
265
+ vao.set_data(data)
266
+ # Vertex Attributes:
267
+ # 0: vec2 a_position (screen position of the glyph)
268
+ # 1: vec4 a_uvRect (u0, v0, u1, v1)
269
+ # 2: vec2 a_size (width, height of the glyph quad)
270
+ vao.set_vertex_attribute_pointer(0, 2, gl.GL_FLOAT, stride, 0)
271
+ vao.set_vertex_attribute_pointer(1, 4, gl.GL_FLOAT, stride, 8)
272
+ vao.set_vertex_attribute_pointer(2, 2, gl.GL_FLOAT, stride, 24)
273
+
274
+ gl.glActiveTexture(gl.GL_TEXTURE0)
275
+ gl.glBindTexture(gl.GL_TEXTURE_2D, atlas.texture)
276
+ ShaderLib.use(DefaultShader.TEXT)
277
+ ShaderLib.set_uniform("textColour", float(colour.x), float(colour.y), float(colour.z), 1.0)
278
+ # We are drawing one point per character
279
+ vao.set_num_indices(len(render_data) // 8)
280
+ vao.draw()
281
+
282
+ # Restore OpenGL state
283
+ gl.glDisable(gl.GL_BLEND)
284
+ if depth_test_enabled:
285
+ gl.glEnable(gl.GL_DEPTH_TEST)
286
+ if polygon_mode != gl.GL_FILL:
287
+ gl.glPolygonMode(gl.GL_FRONT_AND_BACK, polygon_mode)
288
+
289
+ def _build_instances(self, font: str, text: str, start_x: int, start_y: int) -> List[float]:
290
+ """
291
+ Generates vertex attribute data for each character in a string.
292
+
293
+ This data is sent to the GPU as a single buffer. The geometry shader
294
+ then uses this data to construct a quad for each character.
295
+
296
+ Args:
297
+ font: The name of the font to use.
298
+ text: The string to process.
299
+ start_x: The initial x-coordinate for the text baseline.
300
+ start_y: The initial y-coordinate for the text baseline.
301
+
302
+ Returns:
303
+ A list of floats representing the packed vertex data for all characters.
304
+ """
305
+ inst = []
306
+ atlas = self._fonts.get(font)
307
+ if atlas:
308
+ x, y = float(start_x), float(start_y) # Use floats for positioning
309
+
310
+ for ch in text:
311
+ if ch not in atlas.glyphs:
312
+ continue
313
+ g = atlas.glyphs[ch]
314
+ w, h = g["size"]
315
+ adv = g["advance"]
316
+ bearing_x, bearing_y = g["bearing"]
317
+
318
+ # UV coordinates from atlas (in pixels)
319
+ u0_px, v0_px, u1_px, v1_px = g["uv"]
320
+
321
+ # Normalize UVs to the range [0, 1]
322
+ u0 = u0_px / atlas.atlas_w
323
+ v0 = v0_px / atlas.atlas_h
324
+ u1 = u1_px / atlas.atlas_w
325
+ v1 = v1_px / atlas.atlas_h
326
+
327
+ # Calculate the screen position for the top-left corner of the quad.
328
+ # FreeType's origin is at the baseline, with +y going up.
329
+ # Screen coordinates usually have +y going down, so we adjust.
330
+ pos_x = x + bearing_x
331
+ pos_y = y - bearing_y
332
+
333
+ # Each character is defined by 8 floats:
334
+ # pos_x, pos_y, u0, v0, u1, v1, w, h
335
+ inst.extend([pos_x, pos_y, u0, v0, u1, v1, float(w), float(h)])
336
+ # Advance the cursor for the next character
337
+ x += adv
338
+ return inst
339
+
340
+
341
+ # Create a singleton instance of the Text class for global use.
342
+ Text = _Text()