"""
vertex_array_object.py
This module defines the `VertexArrayObject` class, which encapsulates the creation, management,
and usage of OpenGL Vertex Array Objects (VAOs) in modern OpenGL rendering workflows.
The `VertexArrayObject` class inherits from `VertexBuffer` and provides
a clean, object-oriented interface for managing VAO handles and vertex
attribute configurations.
It supports binding/unbinding operations,
attribute registration, and rendering via `glDrawArrays`.
Features:
- Automatic VAO generation if none is provided
- Storage and enabling of vertex attribute definitions
- Integration with VBOs via `ModernVBO` (used as context managers)
- Simplified draw calls for points or other primitive modes
- Graceful deletion and handle management
Dependencies:
- numpy
- PyOpenGL (OpenGL.GL and OpenGL.raw.GL)
Example usage:
==============
>>>vao = VertexArrayObject()
...vao.add_attribute(index=0, vbo=vbo, size=3)
...vao.add_attribute(index=1, vbo=colors, size=3)
...vao.update(index_count=100)
Intended for OpenGL 3.0+ with VAO support.
"""
import ctypes
from typing import Optional
import numpy as np
from OpenGL.GL import glDeleteVertexArrays, glGenVertexArrays
from OpenGL.raw.GL._types import GL_FLOAT, GL_UNSIGNED_INT
from OpenGL.raw.GL.ARB.vertex_array_object import glBindVertexArray
from OpenGL.raw.GL.VERSION.GL_1_0 import GL_POINTS
from OpenGL.raw.GL.VERSION.GL_1_1 import glDrawArrays, glDrawElements
from OpenGL.raw.GL.VERSION.GL_1_5 import (
GL_ARRAY_BUFFER,
GL_ELEMENT_ARRAY_BUFFER,
GL_STATIC_DRAW,
glBindBuffer,
)
from OpenGL.raw.GL.VERSION.GL_2_0 import (
glEnableVertexAttribArray,
glVertexAttribPointer,
)
from picogl.backend.modern.core.vertex.array.helpers import (
enable_points_rendering_state,
)
from picogl.backend.modern.core.vertex.base import VertexBuffer
from picogl.backend.modern.core.vertex.buffer.element import ModernEBO
from picogl.backend.modern.core.vertex.buffer.object import ModernVBO
from picogl.buffers.attributes import LayoutDescriptor
from picogl.buffers.base import VertexBase
from picogl.buffers.glcleanup import delete_buffer
from picogl.buffers.vertex.aliases import NAME_ALIASES
from picogl.logger import Logger as log
from picogl.safe import gl_gen_safe
[docs]
class VertexArrayObject(VertexBase):
"""
OpenGL Vertex Array Objects (VAO) class
"""
[docs]
def __init__(self, handle: int = None):
"""
VertexArrayObject
:param handle: int Handle (ID) of the OpenGL Vertex Array Object (VAO).
"""
if not handle or handle is None:
if glGenVertexArrays:
handle = gl_gen_safe(glGenVertexArrays)
else:
raise RuntimeError(
"glGenVertexArrays not available — OpenGL context not ready"
)
super().__init__(handle)
[docs]
self.named_vbos: dict[str, VertexBuffer] = {}
[docs]
self.vao = None # Bonds Vertex Array Object. Does absolutely nothing
[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
# self.named_vbos: dict[str, LegacyVBO] = {} # store by semantic name
self.ebo = None
self.ebo = None
[docs]
self.layout: Optional[LayoutDescriptor] = None
# self.named_vbos: dict[str, ModernVBO] = {} # store by semantic name
self.bind()
[docs]
def bind(self):
"""
Bind the VAO for use in rendering.
"""
glBindVertexArray(self.handle)
[docs]
def unbind(self):
"""
Unbind the VAO by binding to zero.
"""
glBindVertexArray(0)
[docs]
def delete(self):
"""
Delete the VAO from GPU memory.
"""
glDeleteVertexArrays(1, [self.handle])
[docs]
def set_layout(self, layout: LayoutDescriptor) -> None:
"""
set_layout
:param layout: LayoutDescriptor: The layout descriptor to define the vertex attribute format.
:raises: None
Sets the layout for the rendering setup by binding the buffers and configuring the attributes.
The state is stored in the Vertex Array Object (VAO). This method assumes a single Vertex Buffer
Object (VBO) holds all position data but can be adapted as required. Handles optional usage of
Normal Buffer Object (NBO) and Element Buffer Object (EBO) if present.
"""
try:
self.layout = layout
if self.vao is None:
return
glBindVertexArray(self.handle)
if self.vbo is not None:
glBindBuffer(GL_ARRAY_BUFFER, getattr(self.vbo, "_id", self.vbo))
if self.nbo is not None:
# If you have multiple buffers, bind as needed per attribute
pass # adapt as needed
if self.layout:
for attr in self.layout.attributes:
log.parameter("attr", attr)
glEnableVertexAttribArray(attr.index)
glVertexAttribPointer(
attr.index,
attr.size,
attr.type,
attr.normalized,
attr.stride,
ctypes.c_void_p(attr.offset),
)
if self.ebo is not None:
glBindBuffer(
GL_ELEMENT_ARRAY_BUFFER, getattr(self.ebo, "_id", self.ebo)
)
glBindVertexArray(0)
self._configured = True
except Exception as ex:
log.error(f"error {ex} occurred setting layout")
[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 add_vbo(
self,
index: int,
data: np.ndarray,
size: int,
dtype: int = GL_FLOAT,
name: str = None,
handle: int = None,
) -> ModernVBO:
"""
Add a Vertex Buffer Object (VBO) to the VAO and set its attributes.
:param handle:
:param index: VAO attribute index
:param data: Vertex data
:param size: Size per vertex (e.g., 3 for vec3)
:param dtype: OpenGL data type (e.g., GL_FLOAT)
:param name: Optional semantic name (e.g., "position", "colour")
:return: OpenGL buffer handle (GLuint)
"""
vbo = ModernVBO(handle=handle)
vbo.bind()
vbo.set_data(data)
vbo.set_vertex_attributes(index=index, data=data, size=size, dtype=dtype)
vbo.configure()
self.attributes.append((index, vbo.handle, size, dtype, False, 0, 0))
self.vbos.append(vbo)
if name:
self.named_vbos[name] = vbo
return vbo
[docs]
def delete_buffers(self):
"""
delete_buffers
:return: None
"""
for vbo in self.vbos:
delete_buffer(vbo)
self.vbos.clear()
if self.ebo:
delete_buffer(self.ebo)
self.ebo = None
self.named_vbos.clear()
[docs]
def add_attribute(
self,
index: int,
vbo: int,
size: int = 3,
dtype: int = GL_FLOAT,
normalized: bool = False,
stride: int = 0,
offset: int = 0,
):
"""
add_attribute
:param index: int Index of the vertex attribute.
:param vbo: int Vertex Buffer Object (VBO) associated with this attribute.
:param size: int Size of the vertex attribute (e.g., 3 for a 3D vector).
:param dtype: int Data type of the vertex attribute (default is GL_FLOAT).
:param normalized: bool Whether the data is normalized (default is False).
:param stride: int Byte offset between consecutive vertex attributes (default is 0).
:param offset: int Byte offset to the first component of the
vertex attribute (default is 0).
Add a vertex attribute to the VAO.
"""
self.attributes.append((index, vbo, size, dtype, normalized, stride, offset))
[docs]
def add_ebo(self, data: np.ndarray) -> ModernEBO:
"""
add_ebo
:param data: np.ndarray
:return: int
"""
ebo = ModernEBO(data=data)
ebo.bind()
ebo.set_element_attributes(data=data, size=data.nbytes, dtype=GL_STATIC_DRAW)
ebo.configure()
self.ebo = ebo
return ebo
[docs]
def set_ebo(self, ebo: int) -> int:
"""
add_ebo
:param ebo: int
:return: int
"""
self.ebo = ModernEBO(handle=ebo)
self.ebo.bind()
return ebo
@property
[docs]
def index_count(self) -> str | int | None:
"""
Return the number of indices in the EBO.
:return: int
"""
try:
if self.ebo:
if hasattr(self.ebo, "data"):
return len(self.ebo.data)
return 0
except Exception as ex:
log.error(f"error {ex} occurred")
[docs]
def draw(
self,
index_count: int = None,
dtype: int = GL_UNSIGNED_INT,
mode: int = GL_POINTS,
pointer: int = ctypes.c_void_p(0),
):
"""
draw
:param pointer: ctypes.c_void_p(0)
:param dtype: GL_UNSIGNED_INT
:param index_count: int Number of vertices to draw.
:param mode: int e.g. GL_POINT
:return: None
"""
atom_count = index_count or self.index_count
if mode == GL_POINTS:
enable_points_rendering_state()
if self.ebo:
glDrawElements(mode, atom_count, dtype, pointer)
else:
glDrawArrays(mode, 0, atom_count)