Source code for picogl.renderer.legacy_glmesh

import ctypes
from typing import Optional

import numpy as np
from picogl.buffers.factory.layout import create_layout
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.backend.legacy.core.vertex.buffer.client_states import legacy_client_states
from picogl.buffers.attributes import AttributeSpec
from picogl.buffers.glcleanup import delete_buffer_object
from picogl.buffers.vertex.legacy import VertexBufferGroup


[docs] class LegacyGLMeshNew: """ GL Mesh for Compatibility Profile (Legacy OpenGL) Keeps a similar interface to theIndexed version, but uses client-side arrays / legacy calls instead of VAOs/VBOs/EBOs. """ def __init__( self, vertices: np.ndarray, faces: Optional[np.ndarray], colors: Optional[np.ndarray] = None, normals: Optional[np.ndarray] = None, uvs: Optional[np.ndarray] = None, use_indices: bool = True, ): # 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] # Validation: must have non-empty faces when using indexed path if self.indices.size == 0: raise ValueError("LegacyGLMesh requires non-empty faces") # If indices are used, ensure they form complete triangles if self.indices.size % 3 != 0: raise ValueError("LegacyGLMesh: faces must define a multiple of 3 indices (triangles)")
[docs] self.use_indices = use_indices # allow an easy switch to non-indexed path
[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 = None # keep name for compatibility
[docs] self.index_count: int = self.indices.size
# Optional expanded data for non-indexed path
[docs] self._expanded_vertices = None
[docs] self._expanded_colors = None
[docs] self._expanded_normals = None
[docs] self._expanded_uvs = None
if not self.use_indices: self._expand_to_non_indexed() @classmethod
[docs] def from_mesh_data(cls, mesh: "MeshData") -> "LegacyGLMesh": return cls( vertices=mesh.vbo, faces=mesh.ebo, colors=mesh.cbo, normals=mesh.nbo, uvs=getattr(mesh, "uvs", None), )
[docs] def _expand_to_non_indexed(self) -> None: """ Expand indexed data to per-triangle vertex data for glDrawArrays. """ if self.indices is None or self.indices.size == 0: raise ValueError("Cannot expand: no indices to expand from") v = self.vertices c = self.colors n = self.normals t = self.uvs tri_count = self.indices.size // 3 expanded_v = np.empty((tri_count * 3, 3), dtype=np.float32) expanded_c = np.empty((tri_count * 3, 3), dtype=np.float32) expanded_n = np.empty((tri_count * 3, 3), dtype=np.float32) expanded_t = np.empty((tri_count * 3, 2), dtype=np.float32) for i in range(tri_count): a = self.indices[3 * i + 0] b = self.indices[3 * i + 1] cidx = self.indices[3 * i + 2] expanded_v[3 * i + 0] = v[a] expanded_v[3 * i + 1] = v[b] expanded_v[3 * i + 2] = v[cidx] expanded_c[3 * i + 0] = c[a] expanded_c[3 * i + 1] = c[b] expanded_c[3 * i + 2] = c[cidx] expanded_n[3 * i + 0] = n[a] expanded_n[3 * i + 1] = n[b] expanded_n[3 * i + 2] = n[cidx] expanded_t[3 * i + 0] = t[a] expanded_t[3 * i + 1] = t[b] expanded_t[3 * i + 2] = t[cidx] self._expanded_vertices = expanded_v self._expanded_colors = expanded_c self._expanded_normals = expanded_n self._expanded_uvs = expanded_t self.vertices = expanded_v self.colors = expanded_c self.normals = expanded_n self.uvs = expanded_t self.indices = np.array([], dtype=np.uint32) self.index_count = expanded_v.shape[0]
[docs] def upload(self) -> None: """Legacy GL uses client-side arrays; minimal setup is preserved for compatibility.""" # In legacy GL, upload typically isn't necessary for client arrays, # but we keep a placeholder to maintain API compatibility. if self.vao: return # If you want to precompute a single contiguous block, you could store lists here. self.vao = None # no VAO in legacy immediate mode # index_count already set during __init__ or _expand_to_non_indexed # For clarity, ensure it's correct: self.index_count = self.indices.size if self.use_indices else self.vertices.shape[0]
[docs] def bind(self): # Legacy GL uses client arrays; no VAO binding. We might enable client states here. pass
[docs] def unbind(self): pass
[docs] def delete(self): """Legacy GL doesn’t hold GPU resources in VAOs/VBOs; keep API for symmetry.""" 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 using Legacy GL calls.""" try: # In legacy GL, ensure data pointers are set before drawing. if self.use_indices and self.index_count > 0: # If you are using client-side element arrays, you can call: # glDrawElements(mode, index_count, GL_UNSIGNED_INT, self.indices.ctypes.data) # But most modern Python OpenGL wrappers expect numpy buffers; adapt as needed. glDrawElements(mode, int(self.index_count), GL_UNSIGNED_INT, ctypes.c_void_p(0)) else: # Non-indexed path: draw as arrays. We'll draw as triangles from vertex data. glDrawArrays(mode, 0, int(self.index_count)) except Exception as ex: raise
[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 .vbo (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.vbo, faces=mesh.ebo, colors=mesh.cbo, normals=mesh.nbo, uvs=getattr(mesh, "uvs", 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="vbo", size=3) vao.add_vbo(data=self.colors, name="cbo", size=3) vao.add_vbo(data=self.normals, name="nbo", size=3) if self.uvs is not None: vao.add_vbo(data=self.uvs, name="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="positions", index=0, size=3, type=GL_FLOAT, normalized=False, stride=0, offset=0, ), AttributeSpec( name="colors", index=1, size=3, type=GL_FLOAT, normalized=False, stride=0, offset=0, ), AttributeSpec( name="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="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}")