Source code for picogl.renderer.legacy_glmesh

"""
Module for creating and managing GPU-resident meshes in a legacy OpenGL
compatibility profile.

This module defines the LegacyGLMesh class, which represents an indexed
triangle mesh. It provides functionality for uploading mesh data to GPU
buffers, managing their lifecycle, and rendering the mesh.

Classes
-------
LegacyGLMesh
    Represents a GPU-resident mesh with VAO/VBO/EBO/CBO/NBO structures
    for an indexed triangle mesh.
"""

from typing import Optional

import numpy as np

from picogl.backend.gl.enums import GLDrawMode, GLNumeric
from picogl.backend.gl.state.client import GLClientState
from picogl.backend.gl.wrappers import gl_draw_elements
from picogl.backend.gl.wrappers.glcleanup import gl_delete_buffer_object
from picogl.backend.legacy.core.vertex.buffer.client_states import legacy_client_states
from picogl.gpu.buffers.attributes import (
    AttributeSpec,
    CanonicalVertexAttrs,
    legacy_attribute_spec,
)
from picogl.gpu.buffers.factory import create_layout
from picogl.gpu.buffers.helper import as_vec3_array
from picogl.gpu.buffers.vertex.aliases import VertexBufferRole
from picogl.gpu.buffers.vertex.legacy import VertexBufferGroup
from picogl.gpu.buffers.vertex.vbo.vbo_class import MeshDataAttrs, VBOType


[docs] class LegacyGLMesh: """ gl Mesh fir Compatibility Profile GPU‐resident mesh: owns VAO/VBO/EBO/CBO/NBO for an indexed triangle mesh. It does not know anything about shaders or matrices. """ def __init__( self, vertices: np.ndarray, faces: np.ndarray, colors: Optional[np.ndarray] = None, normals: Optional[np.ndarray] = None, uvs: Optional[np.ndarray] = None, ): # strict (N, 3)
[docs] self.vertices = as_vec3_array(vertices)
if faces is not None: self.indices = np.asarray(faces, dtype=np.uint32).reshape(-1) else: self.indices = np.array([], dtype=np.uint32) nverts = self.vertices.shape[0] if self.indices.size == 0: raise ValueError("GLMesh requires non-empty faces")
[docs] self.colors = ( as_vec3_array(colors) if colors is not None else np.tile((0.0, 0.0, 1.0), (nverts, 1)).astype(np.float32) )
[docs] self.normals = ( as_vec3_array(normals) if normals is not None else np.zeros_like(self.vertices) )
[docs] self.uvs = ( np.asarray(uvs, dtype=np.float32).reshape(-1, 2) if uvs is not None else np.zeros((nverts, 2), dtype=np.float32) )
[docs] self.vao: Optional[VertexBufferGroup] = None
[docs] self.index_count: int = 0
@classmethod
[docs] def from_mesh_data(cls, mesh: "MeshData") -> "LegacyGLMesh": """ Construct a GLMesh from a MeshData container. Parameters ---------- mesh : MeshData Must have .vertices (Nx3), .ebo (Mx1), optional .cbo (Nx3), .nbo (Nx3), uvs (Nx2) Returns ------- LegacyGLMesh Ready-to-upload mesh (GPU buffers are allocated only when `upload()` is called). """ return cls( vertices=mesh.vertices, faces=mesh.indices, colors=mesh.colors, normals=mesh.normals, uvs=getattr(mesh, MeshDataAttrs.TEXCOORDS, None), )
[docs] def upload(self) -> None: """Allocate & fill GPU buffers.""" if self.vao: return # already uploaded attributes = self.generate_dynamic_attributes() vao_layout = create_layout(attributes) self.vao = vao = VertexBufferGroup() vao.add_vbo(data=self.vertices, name=VBOType.VBO, size=3) vao.add_vbo(data=self.colors, name=VBOType.CBO, size=3) vao.add_vbo(data=self.normals, name=VBOType.NBO, size=3) if self.uvs is not None: vao.add_vbo(data=self.uvs, name=VBOType.UVS, size=2) vao.add_ebo(data=self.indices) vao.set_layout(vao_layout) self.index_count = self.indices.size
[docs] def generate_dynamic_attributes(self): """ generate_dynamic_attributes Create layout that matches the VBOs being added """ attributes = [ legacy_attribute_spec( VertexBufferRole.VBO, 0, name=CanonicalVertexAttrs.POSITIONS, type=GLNumeric.FLOAT, ), legacy_attribute_spec( VertexBufferRole.CBO, 1, name=CanonicalVertexAttrs.COLORS, type=GLNumeric.FLOAT, ), legacy_attribute_spec( VertexBufferRole.NBO, 2, name=CanonicalVertexAttrs.NORMALS, type=GLNumeric.FLOAT, ), ] # Add UVs attribute if UVs are provided if self.uvs is not None: attributes.append( AttributeSpec( name=VBOType.UVS, index=3, size=2, type=GLNumeric.FLOAT, normalized=False, stride=0, offset=0, ) ) return attributes
[docs] def bind(self): if not self.vao: self.upload() if not self.vao: raise RuntimeError("GLMesh not uploaded") self.vao.__enter__() # context protocol
[docs] def unbind(self): if self.vao: self.vao.__exit__(None, None, None)
[docs] def delete(self): """Free GPU resources.""" if self.vao: gl_delete_buffer_object(self.vao) self.vao = None self.index_count = 0
[docs] def __enter__(self): self.bind() return self
[docs] def __exit__(self, exc_type, exc, tb): self.unbind()
[docs] def draw(self, mode=GLDrawMode.TRIANGLES) -> None: """Draw the mesh.""" try: if not self.vao: raise RuntimeError("GLMesh not uploaded. Call upload() first.") # Use legacy client states and individual VBOs to bypass the problematic bind() method with legacy_client_states(GLClientState.VERTEX, GLClientState.COLOR): with self.vao.vbo, self.vao.cbo, self.vao.ebo: gl_draw_elements( int(self.index_count), GLNumeric.UNSIGNED_INT, mode, pointer=None, offset=0, ) except Exception as ex: print(f"Error drawing mesh: {ex}")