I'm working on a Python project using the moderngl library for OpenGL rendering. I have a class that is responsible for rendering a textured cube, and I'm facing an issue where the textures are not correctly applied to the cube faces. The rendered result does not resemble a cube with distinct textures on each face.
import moderngl
from PIL import Image
import numpy as np
from pathlib import Path
from typing import Dict, Tuple, Optional
class BlockRender:
class BlockDataType:
def __init__(self, textures: Dict[str, str]) -> None:
if not isinstance(textures, dict):
raise ValueError("Textures must be a dictionary.")
required_keys = {'top', 'bottom', 'back', 'front', 'left', 'right'}
if not set(textures.keys()).issuperset(required_keys):
raise ValueError("Textures dictionary must have keys: top, left, bottom, right, back, front.")
self.textures = textures
def read_block_data(self, ctx) -> Dict[str, moderngl.Texture]:
image_data = {}
for key, path in self.textures.items():
try:
image = Image.open(path)
data = np.array(image.convert('RGBA'))
texture = ctx.texture(image.size, 4, data.tobytes())
texture.build_mipmaps()
image_data[key] = texture
except Exception as e:
raise ValueError(f"Error reading '{key}' texture: {e}")
return image_data
def verify_path_and_extension(self) -> bool:
for key, path in self.textures.items():
try:
if not (Path(path).is_file() and Path(path).suffix.lower() in {'.jpg', '.jpeg', '.png', '.gimp'}):
raise ValueError(f"Invalid or unsupported file for '{key}' texture: {path}")
except Exception as e:
raise ValueError(f"Error verifying path for '{key}' texture: {e}")
return True
def __init__(self, blockdata: BlockDataType, position: Optional[Tuple[float, float, float]] = (0, 0, 0)) -> None:
if not isinstance(blockdata, self.BlockDataType):
raise ValueError("blockdata must be an instance of BlockDataType.")
self.blockdata = blockdata
self.position = position
def render_matrices_and_vertices(self, ctx: moderngl.Context) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
vertices = np.array([
# Front face
[-0.5, -0.5, 0.5], # 0
[-0.5, 0.5, 0.5], # 1
[0.5, 0.5, 0.5], # 2
[0.5, -0.5, 0.5], # 3
# Back face
[-0.5, -0.5, -0.5], # 4
[-0.5, 0.5, -0.5], # 5
[0.5, 0.5, -0.5], # 6
[0.5, -0.5, -0.5], # 7
# Top face
[-0.5, 0.5, 0.5], # 8
[0.5, 0.5, 0.5], # 9
[0.5, 0.5, -0.5], # 10
[-0.5, 0.5, -0.5], # 11
# Bottom face
[-0.5, -0.5, 0.5], # 12
[0.5, -0.5, 0.5], # 13
[0.5, -0.5, -0.5], # 14
[-0.5, -0.5, -0.5], # 15
# Left face
[-0.5, -0.5, 0.5], # 16
[-0.5, 0.5, 0.5], # 17
[-0.5, 0.5, -0.5], # 18
[-0.5, -0.5, -0.5], # 19
# Right face
[0.5, -0.5, 0.5], # 20
[0.5, 0.5, 0.5], # 21
[0.5, 0.5, -0.5], # 22
[0.5, -0.5, -0.5], # 23
], dtype=np.float32)
# Read textures from BlockDataType
textures = self.blockdata.read_block_data(ctx)
# Define texture coordinates for each face based on loaded textures
texture_coords = np.zeros((vertices.shape[0], 2), dtype=np.float32)
for key in ['top', 'bottom', 'back', 'front', 'left', 'right']:
texture = textures[key]
# Calculate texture coordinates based on texture size
width, height = texture.width, texture.height
texture_coords[:, 0] = np.interp(vertices[:, 0], [-0.5, 0.5], [0, width])
texture_coords[:, 1] = np.interp(vertices[:, 1], [-0.5, 0.5], [height, 0])
texture_coords /= np.array([width, height], dtype=np.float32)
# Define the indices of the triangles
indices = np.array([
# Front face
0, 1, 2, 0, 2, 3,
# Back face
4, 5, 6, 4, 6, 7,
# Top face
8, 9, 10, 8, 10, 11,
# Bottom face
12, 13, 14, 12, 14, 15,
# Left face
16, 17, 18, 16, 18, 19,
# Right face
20, 21, 22, 20, 22, 23,
], dtype=np.uint32)
return vertices, texture_coords, indices
class Block:
def __init__(self, textures: Dict[str, str], position: Optional[Tuple[float, float, float]] = (0, 0, 0)) -> None:
block_data_type = BlockRender.BlockDataType(textures=textures)
self.block_render = BlockRender(blockdata=block_data_type, position=position)
def set_position(self, position: Tuple[float, float, float]) -> None:
self.block_render.position = position
def render(self, ctx: moderngl.Context) -> None:
vertices, texture_coords, indices = self.block_render.render_matrices_and_vertices(ctx)
# Create a shader program
program = ctx.program(
vertex_shader="""
#version 330
uniform mat4 model;
in vec3 in_position;
in vec2 in_texture;
out vec2 frag_texture;
void main() {
gl_Position = model * vec4(in_position, 1.0);
frag_texture = in_texture;
}
""",
fragment_shader="""
#version 330
uniform sampler2D texture_sampler;
in vec2 frag_texture;
out vec4 color;
void main() {
color = texture(texture_sampler, frag_texture);
}
"""
)
# Create vertex buffer
vbo = ctx.buffer(vertices.tobytes())
vao = ctx.simple_vertex_array(program, vbo, 'in_position', 'in_texture')
# Set model matrix
model_matrix = np.identity(4, dtype=np.float32)
model_matrix[:3, 3] = self.block_render.position
program['model'].write(model_matrix)
# Render
for key in ['top', 'bottom', 'back', 'front', 'left', 'right']:
texture = self.block_render.blockdata.read_block_data(ctx)[key]
texture.build_mipmaps()
texture.use()
vao.render(moderngl.TRIANGLES)
I suspect that there might be an issue with how I'm calculating the texture coordinates for each face, but I'm not sure where the problem lies. I've tried a few different approaches, including adjusting the texture coordinates based on the vertices' x and y coordinates, but none seem to produce the correct result.