Source code for picogl.utils.loader.texture

"""
Texture Loader
"""
import os
import struct
from pathlib import Path
from typing import Optional

from OpenGL.GL import (glCompressedTexImage2D, glDeleteTextures, glGenTextures,
                       glTexImage2D)
from OpenGL.GL.framebufferobjects import glGenerateMipmap
from OpenGL.raw.GL.EXT.texture_compression_s3tc import (
    GL_COMPRESSED_RGBA_S3TC_DXT1_EXT, GL_COMPRESSED_RGBA_S3TC_DXT3_EXT,
    GL_COMPRESSED_RGBA_S3TC_DXT5_EXT)
from OpenGL.raw.GL.VERSION.GL_1_0 import (GL_LINEAR, GL_LINEAR_MIPMAP_LINEAR,
                                          GL_REPEAT, GL_RGB, GL_RGBA,
                                          GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER,
                                          GL_TEXTURE_MIN_FILTER,
                                          GL_TEXTURE_WRAP_S, GL_TEXTURE_WRAP_T,
                                          GL_UNSIGNED_BYTE, glTexParameteri)
from OpenGL.raw.GL.VERSION.GL_1_1 import glBindTexture
from PIL import Image


[docs] class TextureLoader: """ Loads a 2D texture from a DDS file or a standard image file using PIL. Automatically creates an OpenGL texture ID. """
[docs] def __init__(self, file_name: str, mode: str = "RGB") -> None:
[docs] self.texture_gl_id: Optional[int] = None
[docs] self.width: int = 0
[docs] self.height: int = 0
[docs] self.format: str = mode
[docs] self.buffer: Optional[bytes] = None
[docs] self.inversed_v_coords: bool = False
if not os.path.isabs(file_name): file_name = os.path.abspath( os.path.join(os.path.dirname(__file__), "..", file_name) ) if file_name.lower().endswith(".dds"): self.load_dds(file_name) else: self.load_by_pil(file_name, mode)
[docs] def load_dds(self, file_name: str) -> None: """ Load a DDS texture from file. Supports DXT1, DXT3, DXT5 compressed textures. Falls back to PIL loading if compressed texture loading fails. """ try: with open(file_name, "rb") as dds_file: dfs_tag = dds_file.read(4) if dfs_tag != b"DDS ": raise ValueError(f"Invalid DDS file: {file_name}") head = dds_file.read(124) self.height = struct.unpack("<I", head[8:12])[0] self.width = struct.unpack("<I", head[12:16])[0] linear_size = struct.unpack("<I", head[16:20])[0] mip_map_count = struct.unpack("<I", head[24:28])[0] four_cc = head[80:84].decode("ascii") supported_dds = ["DXT1", "DXT3", "DXT5"] if four_cc not in supported_dds: raise ValueError(f"Not supported DDS file: {four_cc}") self.format = four_cc block_size = 8 if four_cc == "DXT1" else 16 gl_format = { "DXT1": GL_COMPRESSED_RGBA_S3TC_DXT1_EXT, "DXT3": GL_COMPRESSED_RGBA_S3TC_DXT3_EXT, "DXT5": GL_COMPRESSED_RGBA_S3TC_DXT5_EXT, }[four_cc] buffer_size = linear_size * 2 if mip_map_count > 1 else linear_size with open(file_name, "rb") as dds_file: dds_file.seek(128) # skip DDS header dds_buffer = dds_file.read(buffer_size) self.texture_gl_id = glGenTextures(1) glBindTexture(GL_TEXTURE_2D, self.texture_gl_id) offset = 0 w, h = self.width, self.height for level in range(mip_map_count): size = ((w + 3) // 4) * ((h + 3) // 4) * block_size try: glCompressedTexImage2D( GL_TEXTURE_2D, level, gl_format, w, h, 0, size, dds_buffer[offset: offset + size], ) except Exception as e: # Fallback: try with explicit byte array conversion try: import array byte_array = array.array('B', dds_buffer[offset: offset + size]) glCompressedTexImage2D( GL_TEXTURE_2D, level, gl_format, w, h, 0, size, byte_array, ) except Exception as e2: raise RuntimeError(f"Failed to load DDS compressed texture: {e2}") offset += size w //= 2 h //= 2 if w == 0 or h == 0: break # Set texture parameters for DDS glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT) glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT) glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR) glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR) self.inversed_v_coords = True except Exception as e: # Fallback: try to load as regular image with PIL print(f"⚠️ DDS compressed loading failed: {e}") print(f"🔄 Falling back to PIL loading for: {file_name}") try: self.load_by_pil(file_name, "RGBA") except Exception as e2: raise RuntimeError(f"Failed to load DDS file with both compressed and PIL methods: {e2}")
[docs] def load_by_pil(self, file_name: str, mode: str) -> None: """ Load a standard image using PIL and upload as OpenGL texture. """ with Image.open(file_name) as image: converted = image.convert(mode) self.buffer = converted.transpose(Image.FLIP_TOP_BOTTOM).tobytes() self.width, self.height = image.size self.format = mode # Map PIL mode to OpenGL format gl_format_map = {"RGB": GL_RGB, "RGBA": GL_RGBA} gl_format = gl_format_map.get(mode.upper(), GL_RGB) self.texture_gl_id = glGenTextures(1) glBindTexture(GL_TEXTURE_2D, self.texture_gl_id) glTexImage2D( GL_TEXTURE_2D, 0, gl_format, self.width, self.height, 0, gl_format, GL_UNSIGNED_BYTE, self.buffer, ) # Texture parameters glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT) glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT) glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR) glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR) glGenerateMipmap(GL_TEXTURE_2D)
[docs] def delete(self) -> None: """ Deletes the OpenGL texture to free GPU memory. """ if self.texture_gl_id: glDeleteTextures([self.texture_gl_id]) self.texture_gl_id = None
[docs] def __len__(self) -> int: return len(self.buffer) if self.buffer else 0
if __name__ == "__main__":
[docs] texture = TextureLoader( file_name=os.path.join( str(Path.home()), "projects/PicoGL/examples/resources/tu02/uvtemplate.tga" ) )
print(texture.texture_gl_id) print(texture.width) print(texture.height) print(texture.format) print(texture.buffer) print(texture.inversed_v_coords) texture.delete()