Source code for picogl.renderer.legacy_glmesh

import ctypes
from typing import Optional

import numpy as np
from OpenGL.GL import glDrawElements
from OpenGL.raw.GL._types import GL_FLOAT
from OpenGL.raw.GL.VERSION.GL_1_0 import GL_TRIANGLES, GL_UNSIGNED_INT
from OpenGL.raw.GL.VERSION.GL_1_1 import (GL_COLOR_ARRAY, GL_VERTEX_ARRAY,
                                          glDrawArrays)

from picogl.attrs.vertex import CanonicalVertexAttrs
from picogl.backend.legacy.core.vertex.buffer.client_states import \
    legacy_client_states
from picogl.buffers.attributes import AttributeSpec
from picogl.buffers.factory.layout import create_layout
from picogl.buffers.glcleanup import delete_buffer_object
from picogl.buffers.vertex.legacy import VertexBufferGroup
from picogl.buffers.vertex.vbo.vbo_class import VBOType, MeshDataAttrs


[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 = np.asarray(vertices, dtype=np.float32).reshape(-1, 3)
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 = ( np.asarray(colors, dtype=np.float32).reshape(-1, 3) if colors is not None else np.tile((0.0, 0.0, 1.0), (nverts, 1)).astype(np.float32) )
[docs] self.normals = ( np.asarray(normals, dtype=np.float32).reshape(-1, 3) 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 = [ AttributeSpec( name=CanonicalVertexAttrs.POSITIONS, index=0, size=3, type=GL_FLOAT, normalized=False, stride=0, offset=0, ), AttributeSpec( name=CanonicalVertexAttrs.COLORS, index=1, size=3, type=GL_FLOAT, normalized=False, stride=0, offset=0, ), AttributeSpec( name=CanonicalVertexAttrs.NORMALS, index=2, size=3, type=GL_FLOAT, normalized=False, stride=0, offset=0, ), ] # Add UVs attribute if UVs are provided if self.uvs is not None: attributes.append( AttributeSpec( name=VBOType.UVS, index=3, size=2, type=GL_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: 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=GL_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(GL_VERTEX_ARRAY, GL_COLOR_ARRAY): with self.vao.vbo, self.vao.cbo, self.vao.ebo: glDrawElements( mode, int(self.index_count), GL_UNSIGNED_INT, ctypes.c_void_p(0) ) except Exception as ex: print(f"Error drawing mesh: {ex}")