"""
VertexArrayGroup
Legacy backend (no real GL VAO support)
"""
import ctypes
from typing import Optional, Any
import numpy as np
from OpenGL.GL import glVertexAttribPointer
from OpenGL.raw.GL.VERSION.GL_1_0 import GL_POINTS, GL_TRIANGLES, GL_UNSIGNED_INT, GL_FLOAT, GL_LINE, GL_LINE_STRIP
from OpenGL.raw.GL.VERSION.GL_1_1 import (
GL_COLOR_ARRAY,
GL_NORMAL_ARRAY,
GL_VERTEX_ARRAY,
glDrawArrays,
glDrawElements,
)
from OpenGL.raw.GL.VERSION.GL_1_5 import (
GL_ARRAY_BUFFER,
GL_ELEMENT_ARRAY_BUFFER,
glBindBuffer,
)
from OpenGL.raw.GL.VERSION.GL_2_0 import (
glDisableVertexAttribArray,
glEnableVertexAttribArray,
)
from picogl.backend.legacy.core.vertex.buffer.client_states import legacy_client_states
from picogl.backend.legacy.core.vertex.buffer.color import LegacyColorVBO
from picogl.backend.legacy.core.vertex.buffer.element import LegacyEBO
from picogl.backend.legacy.core.vertex.buffer.normal import LegacyNormalVBO
from picogl.backend.legacy.core.vertex.buffer.position import LegacyPositionVBO
from picogl.backend.legacy.core.vertex.buffer.vertex import LegacyVBO
from picogl.buffers.attributes import LayoutDescriptor
from picogl.buffers.base import VertexBase
from picogl.buffers.glcleanup import delete_buffer_object
from picogl.buffers.vertex.aliases import NAME_ALIASES
from picogl.logger import Logger as log
[docs]
class VertexBufferGroup(VertexBase):
"""Container for legacy VBOs, mimicking VAO interface."""
def __init__(self, draw_mode: int = GL_LINE_STRIP):
super().__init__()
# self.index_count = 0
[docs]
self._index_count = None
[docs]
self.handle = 0 # Does absolutely nothing
[docs]
self.vao = (
None # Bonds Vertex Array Object. Does absolutely nothing, but is needed
)
[docs]
self.vbo = None # Atom Vertex Buffer Object
[docs]
self.cbo = None # Color Vertex Buffer Object
[docs]
self.nbo = None # Normal Vertex Buffer Object
[docs]
self.ebo = None # Bond Index Buffer Object
[docs]
self.layout: Optional[LayoutDescriptor] = None
[docs]
self.named_vbos: dict[str, LegacyVBO] = {} # store by semantic name
[docs]
self.draw_mode: int = draw_mode
[docs]
self.vbo_classes = {
"vbo": LegacyPositionVBO,
"cbo": LegacyColorVBO,
"ebo": LegacyEBO,
"nbo": LegacyNormalVBO,
}
[docs]
def __del__(self):
# Don't auto-delete OpenGL resources here unless you are certain the GL context is current.
# Logging here can help detect premature GC.
# print("VertexBufferGroup.__del__", self)
pass
[docs]
def add_vbo_object(self, name: str, vbo: "LegacyVBO") -> "LegacyVBO":
"""Register a VBO by semantic name or shorthand alias."""
# normalize to canonical key
canonical = NAME_ALIASES.get(name, name)
# store consistently
self.named_vbos[canonical] = vbo
# and assign to attribute if it exists
if hasattr(self, canonical):
setattr(self, canonical, vbo)
return vbo
[docs]
def get_vbo_object(self, name: str) -> "LegacyVBO":
"""Retrieve a VBO by its semantic or shorthand name."""
canonical = NAME_ALIASES.get(name, name)
return self.named_vbos.get(canonical)
[docs]
def delete(self) -> None:
for buf in (self.nbo, self.cbo, self.vbo, self.ebo):
if buf:
delete_buffer_object(buf)
self.nbo = self.cbo = self.vbo = self.ebo = None
self.layout = None
@property
[docs]
def index_count(self) -> int:
"""
Return the number of indices in the EBO.
:return: int
"""
if self._index_count is not None:
return self._index_count
return len(self.ebo.data) if self.ebo and hasattr(self.ebo, "data") else 0
@index_count.setter
def index_count(self, value):
"""Setter for index_count with basic validation."""
if not isinstance(value, int):
raise TypeError(f"index_count must be an int, got {type(value).__name__}")
if value < 0:
raise ValueError("index_count must be non-negative")
self._index_count = value
[docs]
def draw(self, index_count: int = 0, mode: int = GL_POINTS):
"""
draw
:param index_count:
:param count: int
:param mode: int
Enable legacy client states, bind VBOs, draw, and clean up.
"""
if not index_count:
index_count = self.index_count
if not mode:
mode = self.draw_mode
# Use the layout-based binding approach
with self:
with legacy_client_states(GL_VERTEX_ARRAY, GL_COLOR_ARRAY, GL_NORMAL_ARRAY):
# Issue draw call
glDrawArrays(mode, 0, index_count)
[docs]
def add_vbo(
self,
name: str,
data: np.ndarray,
size: int = 3,
dtype: int = GL_FLOAT,
handle: int | None = None,
) -> Any:
"""Create and register a VBO with explicit parameters."""
vbo_class = self.get_buffer_class(name)
if data is None or size <= 0:
raise ValueError("data must be a numpy array, size > 0")
vbo = vbo_class(data=data, size=size, handle=handle, dtype=dtype)
self.add_vbo_object(name, vbo)
return self
[docs]
def get_buffer_class(self, name: str = "vbo") -> type[LegacyVBO]:
"""
get_buffer_class
:param name: str
:return: LegacyVBO
"""
vbo_class = self.vbo_classes.get(name, LegacyPositionVBO)
return vbo_class
[docs]
def add_ebo(self, name: str = "ebo", data: np.ndarray = None):
"""
add_ebo
:param name: str
:param data: np.ndarray
"""
ebo_class = self.vbo_classes.get(name, LegacyEBO)
self.add_vbo_object(name, ebo_class(data=data))
[docs]
def draw_elements(
self,
count: int = 0,
mode: int = GL_TRIANGLES,
dtype: int = GL_UNSIGNED_INT,
offset: int = 0,
):
"""
Draw using an element buffer (EBO) with legacy client states.
:param count: Number of indices to draw. Defaults to `self.index_count`.
:param mode: OpenGL primitive type (GL_TRIANGLES, GL_LINES, etc.).
:param dtype: Data type of indices (GL_UNSIGNED_BYTE, GL_UNSIGNED_SHORT, GL_UNSIGNED_INT).
:param offset: Byte offset into the EBO.
"""
if not self.ebo:
raise RuntimeError("No element buffer (EBO) bound for draw_elements()")
if not count:
count = self.index_count
# Bind buffers and set up attribute pointers
with self:
# Legacy client states still required
with legacy_client_states(GL_VERTEX_ARRAY, GL_COLOR_ARRAY, GL_NORMAL_ARRAY):
# Bind each VBO (legacy-style)
for vbo in self.named_vbos.values():
vbo.bind()
# Bind EBO for indexed drawing
glBindBuffer(
GL_ELEMENT_ARRAY_BUFFER, getattr(self.ebo, "_id", self.ebo)
)
glDrawElements(mode, count, dtype, ctypes.c_void_p(offset))
# Unbind EBO afterwards to prevent accidental reuse
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0)
[docs]
def set_layout(self, layout: LayoutDescriptor) -> None:
self.layout = layout
[docs]
def bind(self) -> None:
"""Bind buffers and upload attribute pointers per stored layout."""
if not self.layout:
return
try:
# For legacy rendering, use the old glVertexPointer approach
# instead of modern glVertexAttribPointer
for attr in self.layout.attributes:
canonical = NAME_ALIASES.get(attr.name, attr.name)
vbo = self.named_vbos.get(canonical)
if not vbo:
continue
# Bind the VBO
buffer_handle = getattr(vbo, "handle", vbo)
glBindBuffer(GL_ARRAY_BUFFER, buffer_handle)
# Use legacy pointer functions based on attribute type
if canonical in ["vbo", "position"]:
from OpenGL.raw.GL.VERSION.GL_1_1 import glVertexPointer
glVertexPointer(3, GL_FLOAT, 0, None)
elif canonical in ["nbo", "normal"]:
from OpenGL.raw.GL.VERSION.GL_1_1 import glNormalPointer
glNormalPointer(GL_FLOAT, 0, None)
elif canonical in ["cbo", "color"]:
from OpenGL.raw.GL.VERSION.GL_1_1 import glColorPointer
glColorPointer(3, GL_FLOAT, 0, None)
except Exception as ex:
log.error(f"error {ex} occurred in VertexBufferGroup")
for attr in self.layout.attributes:
log.parameter("attr", attr)
log.parameter("attr.index", attr.index)
log.parameter("attr.size", attr.size)
log.parameter("attr.type", int(attr.type))
log.parameter("attr.normalized", attr.normalized)
log.parameter("attr.stride", attr.stride)
[docs]
def unbind(self) -> None:
"""Disable attribute arrays and unbind the array buffer."""
if not self.layout:
return
# For legacy rendering, we don't need to disable vertex attrib arrays
# since we're using the old glVertexPointer approach
glBindBuffer(GL_ARRAY_BUFFER, 0)
[docs]
def __enter__(self):
"""Context manager entry - bind the VBO."""
self.bind()
return self
[docs]
def __exit__(self, exc_type, exc_val, exc_tb):
"""Context manager exit - unbind the VBO."""
self.unbind()