Custom debug drawer

I solved a problem for cuboids:

box-edges-debug-drawer-bullet-physics-opengl-pysdl3

assets/shaders/color.frag
uniform vec3 uColor;

void main()
{
    gl_FragColor = vec4(uColor, 1.0);
}
assets/shaders/color.vert
attribute vec3 aPosition;
uniform mat4 uMvpMatrix;

void main()
{
    gl_Position = uMvpMatrix * vec4(aPosition, 1.0);
}
assets/shaders/texture.frag
uniform sampler2D uSampler;
varying vec2 vTexCoord;

void main()
{
    // gl_FragColor = texture2D(uSampler, vTexCoord);
    // For Y-flip
    gl_FragColor = texture2D(uSampler, vec2(vTexCoord.s, 1.0 - vTexCoord.t));
}
assets/shaders/texture.vert
attribute vec3 aPosition;
attribute vec2 aTexCoord;
uniform mat4 uMvpMatrix;
varying vec2 vTexCoord;

void main()
{
    gl_Position = uMvpMatrix * vec4(aPosition, 1.0);
    vTexCoord = aTexCoord;
}
box_drawer.py
import glm
import numpy as np
from OpenGL.GL import *


class BoxDrawer():

    def __init__(self, program):

        self.program = program

        glUseProgram(self.program)
        self.aPositionLocation = glGetAttribLocation(self.program, "aPosition")
        self.aTexCoordLocation = glGetAttribLocation(self.program, "aTexCoord")
        self.uMvpMatrixLocation = glGetUniformLocation(self.program, "uMvpMatrix")
        uSamplerLocation = glGetUniformLocation(self.program, "uSampler")
        glUniform1i(uSamplerLocation, 0)

        self.initVertexBuffers()

    def initVertexBuffers(self):

        vertPositions = np.array([
            -0.5, -0.5, 0.5,
            -0.5, 0.5, 0.5,
            -0.5, 0.5, -0.5,
            -0.5, 0.5, 0.5,
            0.5, 0.5, 0.5,
            0.5, 0.5, -0.5,
            0.5, 0.5, 0.5,
            0.5, -0.5, 0.5,
            0.5, -0.5, -0.5,
            0.5, -0.5, 0.5,
            -0.5, -0.5, 0.5,
            -0.5, -0.5, -0.5,
            -0.5, -0.5, -0.5,
            -0.5, 0.5, -0.5,
            0.5, 0.5, -0.5,
            0.5, -0.5, 0.5,
            0.5, 0.5, 0.5,
            -0.5, 0.5, 0.5,
            -0.5, -0.5, -0.5,
            -0.5, -0.5, 0.5,
            -0.5, 0.5, -0.5,
            -0.5, 0.5, -0.5,
            -0.5, 0.5, 0.5,
            0.5, 0.5, -0.5,
            0.5, 0.5, -0.5,
            0.5, 0.5, 0.5,
            0.5, -0.5, -0.5,
            0.5, -0.5, -0.5,
            0.5, -0.5, 0.5,
            -0.5, -0.5, -0.5,
            0.5, -0.5, -0.5,
            -0.5, -0.5, -0.5,
            0.5, 0.5, -0.5,
            -0.5, -0.5, 0.5,
            0.5, -0.5, 0.5,
            -0.5, 0.5, 0.5
        ], dtype=np.float32)
        self.vertPosBuffer = glGenBuffers(1)
        glBindBuffer(GL_ARRAY_BUFFER, self.vertPosBuffer)
        glBufferData(GL_ARRAY_BUFFER, len(vertPositions) * 4,
            vertPositions, GL_STATIC_DRAW)

        texCoords = np.array([
            1, 1,
            0, 1,
            0, 0,
            1, 1,
            0, 1,
            0, 0,
            1, 1,
            0, 1,
            0, 0,
            1, 1,
            0, 1,
            0, 0,
            0, 1,
            0, 0,
            1, 0,
            1, 0,
            1, 1,
            0, 1,
            1, 0,
            1, 1,
            0, 0,
            1, 0,
            1, 1,
            0, 0,
            1, 0,
            1, 1,
            0, 0,
            1, 0,
            1, 1,
            0, 0,
            1, 1,
            0, 1,
            1, 0,
            0, 0,
            1, 0,
            0, 1
        ], dtype=np.float32)
        self.texCoordBuffer = glGenBuffers(1)
        glBindBuffer(GL_ARRAY_BUFFER, self.texCoordBuffer)
        glBufferData(GL_ARRAY_BUFFER, len(texCoords) * 4,
            texCoords, GL_STATIC_DRAW)

    def bind(self):
        glUseProgram(self.program)

        glBindBuffer(GL_ARRAY_BUFFER, self.vertPosBuffer)
        glVertexAttribPointer(self.aPositionLocation, 3, GL_FLOAT, GL_FALSE, 0, ctypes.c_void_p(0))
        glEnableVertexAttribArray(self.aPositionLocation)

        glBindBuffer(GL_ARRAY_BUFFER, self.texCoordBuffer)
        glVertexAttribPointer(self.aTexCoordLocation, 2, GL_FLOAT, GL_FALSE, 0, ctypes.c_void_p(0))
        glEnableVertexAttribArray(self.aTexCoordLocation)

    def draw(self, projViewMatrix, position, size, rotation, texture):
        self.bind()
        glBindTexture(GL_TEXTURE_2D, texture)

        # Create a model matrix, that is, a matrix combining the
        # translation matrix, rotation matrix, and the scale matrix
        modelMatrix = glm.translate(glm.mat4(1), position)
        rotationMatrix = glm.mat4_cast(rotation)
        modelMatrix = modelMatrix * rotationMatrix
        modelMatrix = glm.scale(modelMatrix, size)

        # Combine projView matrix and model matrix into one MVP matrix
        mvpMatrix = projViewMatrix * modelMatrix

        # Send MVP matrix to the vertex shader
        glUniformMatrix4fv(self.uMvpMatrixLocation, 1, GL_FALSE, glm.value_ptr(mvpMatrix))

        # Draw a rectangle
        glDrawArrays(GL_TRIANGLES, 0, 36)
collider.py
from panda3d.bullet import BulletBoxShape, BulletRigidBodyNode
from panda3d.core import Point3, Quat, TransformState, Vec3


class Collider():

    def __init__(self, name, position, rotation, size, mass, world):

        self.position = position
        self.rotation = rotation
        self.size = size

        shape = BulletBoxShape(Vec3(size.x / 2, size.y / 2, size.z / 2))
        self.node = BulletRigidBodyNode(name)
        self.node.addShape(shape)
        self.node.setMass(mass)

        p = Point3(self.position.x, self.position.y, self.position.z)
        q = Quat.identQuat()
        s = Vec3(1, 1, 1)

        self.transform = TransformState.make_pos_quat_scale(p, q, s)
        self.node.setTransform(self.transform)

        world.attachRigidBody(self.node)

    def update(self):
        self.position.x = self.node.getTransform().pos.x
        self.position.y = self.node.getTransform().pos.y
        self.position.z = self.node.getTransform().pos.z

        hpr = self.node.getTransform().getHpr()
        quat = Quat()
        quat.setHpr(hpr)

        self.rotation.w = quat.getX()
        self.rotation.x = quat.getY()
        self.rotation.y = quat.getZ()
        self.rotation.z = quat.getW()
line_drawer.py
import math

import glm
import numpy as np
from OpenGL.GL import *

from math_helper import MathHelper


class LineDrawer:

    def __init__(self, program, projViewMatrix):
        # Save the program and projView matrix
        self.program = program
        self.projViewMatrix = projViewMatrix

        # Activate the current shader program to access shader variables
        glUseProgram(self.program)

        # Access the uniform variables in the shaders
        self.aPositionLocation = glGetAttribLocation(self.program, "aPosition")
        self.uColorLocation = glGetUniformLocation(self.program, "uColor")
        self.uMvpMatrixLocation = glGetUniformLocation(self.program, "uMvpMatrix")

        # Create a buffer in the video card's RAM
        self.vertPosBuffer = glGenBuffers(1)
        self.initVertexBuffers()

    def initVertexBuffers(self):
        # Set the vertices of the square
        vertPositions = np.array([
            -0.5, -0.5, 0.5,
            -0.5, 0.5, 0.5,
            -0.5, 0.5, -0.5,
            -0.5, 0.5, 0.5,
            0.5, 0.5, 0.5,
            0.5, 0.5, -0.5,
            0.5, 0.5, 0.5,
            0.5, -0.5, 0.5,
            0.5, -0.5, -0.5,
            0.5, -0.5, 0.5,
            -0.5, -0.5, 0.5,
            -0.5, -0.5, -0.5,
            -0.5, -0.5, -0.5,
            -0.5, 0.5, -0.5,
            0.5, 0.5, -0.5,
            0.5, -0.5, 0.5,
            0.5, 0.5, 0.5,
            -0.5, 0.5, 0.5,
            -0.5, -0.5, -0.5,
            -0.5, -0.5, 0.5,
            -0.5, 0.5, -0.5,
            -0.5, 0.5, -0.5,
            -0.5, 0.5, 0.5,
            0.5, 0.5, -0.5,
            0.5, 0.5, -0.5,
            0.5, 0.5, 0.5,
            0.5, -0.5, -0.5,
            0.5, -0.5, -0.5,
            0.5, -0.5, 0.5,
            -0.5, -0.5, -0.5,
            0.5, -0.5, -0.5,
            -0.5, -0.5, -0.5,
            0.5, 0.5, -0.5,
            -0.5, -0.5, 0.5,
            0.5, -0.5, 0.5,
            -0.5, 0.5, 0.5
        ], dtype=np.float32)
        # Bind to the created buffer
        glBindBuffer(GL_ARRAY_BUFFER, self.vertPosBuffer)
        # Copy vertex array to buffer
        glBufferData(GL_ARRAY_BUFFER, len(vertPositions) * 4,
            vertPositions, GL_STATIC_DRAW)

    # This method should be called if the window
    # size changes or the camera position changes.
    def setProjViewMatrix(self, projViewMatrix):
        self.projViewMatrix = projViewMatrix

    def bind(self):
        # Activate Shader Program Object
        glUseProgram(self.program)

        # Bind to the buffer
        glBindBuffer(GL_ARRAY_BUFFER, self.vertPosBuffer)

        # Set up buffer
        glVertexAttribPointer(self.aPositionLocation, 3, GL_FLOAT, GL_FALSE,
            0, ctypes.c_void_p(0))
        glEnableVertexAttribArray(self.aPositionLocation)

    def draw(self, start, end, color, thickness):
        # Find the center of the segment
        v = end - start
        centerPosition = start + v / 2

        # Find the length of the segment
        length = glm.length(v)

        # Normalize the segment vector
        norm = glm.normalize(v);

        # Calculate the angle of a segment
        rotation = MathHelper.rotationTo(glm.vec3(1, 0, 0), norm);

        # Create a model matrix, that is, a matrix combining the
        # translation matrix, rotation matrix, and the scale matrix
        # Create a translation matrix
        modelMatrix = glm.translate(glm.mat4(1), centerPosition)
        # Create a rotation matrix
        rotationMatrix = glm.mat4_cast(rotation)
        modelMatrix = modelMatrix * rotationMatrix
        # Create a scale matrix
        modelMatrix = glm.scale(modelMatrix, glm.vec3(length, thickness, thickness))

        # Combine projView matrix and model matrix into one MVP matrix
        mvpMatrix = self.projViewMatrix * modelMatrix

        self.bind()

        # Send MVP matrix to the vertex shader
        glUniformMatrix4fv(self.uMvpMatrixLocation, 1, GL_FALSE,
            glm.value_ptr(mvpMatrix))

        # Send color value to fragment shader
        glUniform3fv(self.uColorLocation, 1, glm.value_ptr(color))

        # Call a draw command that will cause the vertex shader
        # to be called 4 times - once for each vertex of the square
        glDrawArrays(GL_TRIANGLE_STRIP, 0, 36)
main.py
import ctypes
import os

import glm
from OpenGL.GL import *
from OpenGL.GL.shaders import *

os.environ["SDL_MAIN_USE_CALLBACKS"] = "1"
os.environ["SDL_RENDER_DRIVER"] = "opengl"

import sdl3
from panda3d.bullet import BulletWorld
from panda3d.core import Vec3

from box_drawer import BoxDrawer
from collider import Collider
from line_drawer import LineDrawer
from texture_loader import loadTexture

glContext = None
window = None

boxDrawer = None
crateTexture = None
green3Texture = None

lineDrawer = None

showCollider = True

canvasWidth = 400
canvasHeight = 400

# Create a perspective projection matrix and a view matrix
fovy = 45
near = 0.1
far = 100
projMatrix = glm.perspective(glm.radians(fovy),
    canvasWidth / canvasHeight, near, far)
viewMatrix = glm.lookAt(
    glm.vec3(10, 20, 25), # Camera position
    glm.vec3(0, 0, 0), # Target position
    glm.vec3(0, 1, 0)) # Camera up vector
# Combine them to one projView matrix
projViewMatrix = projMatrix * viewMatrix

world = BulletWorld()
world.setGravity(Vec3(0, -9.81, 0))

boxCollider = None
box2Collider = None
floorCollider = None

keys = { "left": False, "right": False, "up": False, "down": False }

def getCubeVertices(position, rotationQuat, halfSize):

    # Create a cube
    #    v5----- v1
    #   /|      /|
    #  v4------v0|
    #  | |     | |
    #  | |v7---|-|v3
    #  |/      |/
    #  v6------v2

    # Local vertices (untransformed, centered at origin)
    localVertices = [
        glm.vec3( halfSize,  halfSize,  halfSize),  # v0
        glm.vec3( halfSize,  halfSize, -halfSize),  # v1
        glm.vec3( halfSize, -halfSize,  halfSize),  # v2
        glm.vec3( halfSize, -halfSize, -halfSize),  # v3
        glm.vec3(-halfSize,  halfSize,  halfSize),  # v4
        glm.vec3(-halfSize,  halfSize, -halfSize),  # v5
        glm.vec3(-halfSize, -halfSize,  halfSize),  # v6
        glm.vec3(-halfSize, -halfSize, -halfSize)   # v7
    ]

    # Rotate and translate each vertex
    worldVertices = []
    for vertex in localVertices:
        # Apply rotation (quaternion * vertex)
        rotated = rotationQuat * vertex
        # Apply translation
        worldVertex = rotated + position
        worldVertices.append(worldVertex)

    return worldVertices

@sdl3.SDL_AppInit_func
def SDL_AppInit(appstate, argc, argv):
    global glContext
    global window
    global boxDrawer
    global crateTexture
    global green3Texture
    global boxCollider
    global box2Collider
    global floorCollider
    global lineDrawer

    if not sdl3.SDL_Init(sdl3.SDL_INIT_VIDEO):
        sdl3.SDL_Log("Couldn't initialize SDL: %s".encode() % sdl3.SDL_GetError())
        return sdl3.SDL_APP_FAILURE

    sdl3.SDL_GL_SetAttribute(sdl3.SDL_GL_MULTISAMPLEBUFFERS, 1) # Enable MULTISAMPLE
    sdl3.SDL_GL_SetAttribute(sdl3.SDL_GL_MULTISAMPLESAMPLES, 2) # Can be 2, 4, 8 or 16

    windowTitle = "PySDL3, PyGLM, Pillow, OpenGL, Python".encode()
    window = sdl3.SDL_CreateWindow(windowTitle, canvasWidth, canvasHeight,
        sdl3.SDL_WINDOW_OPENGL | sdl3.SDL_WINDOW_RESIZABLE)
    if not window:
        sdl3.SDL_Log("Couldn't create a window: %s".encode() % sdl3.SDL_GetError())
        return sdl3.SDL_APP_FAILURE

    # Create an OpenGL context
    glContext = sdl3.SDL_GL_CreateContext(window)
    if not glContext:
        sdl3.SDL_Log("Couldn't create a glContext: %s".encode() % sdl3.SDL_GetError())
        return sdl3.SDL_APP_FAILURE

    sdl3.SDL_GL_SetSwapInterval(1) # Turn on vertical sync

    glClearColor(0.15, 0.15, 0.15, 1)
    glEnable(GL_DEPTH_TEST)

    glEnable(GL_BLEND)
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)

    vertexShaderSource = None
    fragmentShaderSource = None

    with open("./assets/shaders/texture.vert") as file:
        vertexShaderSource = file.read()
    with open("./assets/shaders/texture.frag") as file:
        fragmentShaderSource = file.read()
    textureProgram = compileProgram(
       compileShader(vertexShaderSource, GL_VERTEX_SHADER),
       compileShader(fragmentShaderSource, GL_FRAGMENT_SHADER))

    with open("./assets/shaders/color.vert") as file:
        vertexShaderSource = file.read()
    with open("./assets/shaders/color.frag") as file:
        fragmentShaderSource = file.read()
    colorProgram = compileProgram(
       compileShader(vertexShaderSource, GL_VERTEX_SHADER),
       compileShader(fragmentShaderSource, GL_FRAGMENT_SHADER))

    boxDrawer = BoxDrawer(textureProgram)

    crateTexture = loadTexture("./assets/images/crate.png")
    green3Texture = loadTexture("./assets/images/green-3.png")

    floorName = "floor"
    floorPosition = glm.vec3(0, -10, 0)
    floorRotation = glm.quat(1, 0, 0, 0)
    floorSize = glm.vec3(20, 20, 20)
    floorCollider = Collider(floorName, floorPosition,
        floorRotation, floorSize, 0, world)
    floorCollider.node.setFriction(1)

    boxName = "box"
    boxPosition = glm.vec3(7, 2, 0)
    boxRotation = glm.quat(1, 0, 0, 0)
    boxSize = glm.vec3(3, 3, 3)
    boxCollider = Collider(boxName, boxPosition,
        boxRotation, boxSize, 10, world)
    boxCollider.node.setAngularFactor(Vec3(0, 0, 0))
    boxCollider.node.setFriction(1)
    boxCollider.node.setLinearSleepThreshold(False)

    box2Name = "box2"
    # box2Position = glm.vec3(2, 10, 0)
    box2Position = glm.vec3(0, 3, 0)
    box2Rotation = glm.quat(1, 0, 0, 0)
    box2Size = glm.vec3(3, 3, 3)
    box2Collider = Collider(box2Name, box2Position,
        box2Rotation, box2Size, 10, world)
    box2Collider.node.setFriction(1)
    box2Collider.node.setLinearSleepThreshold(False)

    # Create an object for drawing segments
    lineDrawer = LineDrawer(colorProgram, projViewMatrix)

    return sdl3.SDL_APP_CONTINUE

@sdl3.SDL_AppEvent_func
def SDL_AppEvent(appstate, event):
    global projViewMatrix

    if sdl3.SDL_DEREFERENCE(event).type == sdl3.SDL_EVENT_QUIT:
        return sdl3.SDL_APP_SUCCESS
    elif sdl3.SDL_DEREFERENCE(event).type == sdl3.SDL_EVENT_WINDOW_RESIZED:
        w = sdl3.SDL_GetWindowSurface(window).contents.w
        h = sdl3.SDL_GetWindowSurface(window).contents.h
        glViewport(0, 0, w, h)
        projMatrix = glm.perspective(glm.radians(fovy), w / h, near, far)
        projViewMatrix = projMatrix * viewMatrix
        lineDrawer.setProjViewMatrix(projViewMatrix)
    elif sdl3.SDL_DEREFERENCE(event).type == sdl3.SDL_EVENT_KEY_DOWN:
        if (
                sdl3.SDL_DEREFERENCE(event).key.scancode == sdl3.SDL_SCANCODE_W or
                sdl3.SDL_DEREFERENCE(event).button.button == sdl3.SDL_SCANCODE_UP
            ):
            keys["up"] = True
        if (
                sdl3.SDL_DEREFERENCE(event).key.scancode == sdl3.SDL_SCANCODE_S or
                sdl3.SDL_DEREFERENCE(event).button.button == sdl3.SDL_SCANCODE_DOWN
            ):
            keys["down"] = True
        if (
                sdl3.SDL_DEREFERENCE(event).key.scancode == sdl3.SDL_SCANCODE_A or
                sdl3.SDL_DEREFERENCE(event).button.button == sdl3.SDL_SCANCODE_LEFT
            ):
            keys["left"] = True
        if (
                sdl3.SDL_DEREFERENCE(event).key.scancode == sdl3.SDL_SCANCODE_D or
                sdl3.SDL_DEREFERENCE(event).button.button == sdl3.SDL_SCANCODE_RIGHT
            ):
            keys["right"] = True
    elif sdl3.SDL_DEREFERENCE(event).type == sdl3.SDL_EVENT_KEY_UP:
        if (
                sdl3.SDL_DEREFERENCE(event).key.scancode == sdl3.SDL_SCANCODE_W or
                sdl3.SDL_DEREFERENCE(event).button.button == sdl3.SDL_SCANCODE_UP
            ):
            keys["up"] = False
        if (
                sdl3.SDL_DEREFERENCE(event).key.scancode == sdl3.SDL_SCANCODE_S or
                sdl3.SDL_DEREFERENCE(event).button.button == sdl3.SDL_SCANCODE_DOWN
            ):
            keys["down"] = False
        if (
                sdl3.SDL_DEREFERENCE(event).key.scancode == sdl3.SDL_SCANCODE_A or
                sdl3.SDL_DEREFERENCE(event).button.button == sdl3.SDL_SCANCODE_LEFT
            ):
            keys["left"] = False
        if (
                sdl3.SDL_DEREFERENCE(event).key.scancode == sdl3.SDL_SCANCODE_D or
                sdl3.SDL_DEREFERENCE(event).button.button == sdl3.SDL_SCANCODE_RIGHT
            ):
            keys["right"] = False

    return sdl3.SDL_APP_CONTINUE

def keyboard():
    if keys["up"]:
        boxCollider.node.setLinearVelocity(Vec3(0, 0, -5))
    if keys["down"]:
        boxCollider.node.setLinearVelocity(Vec3(0, 0, 5))
    if keys["left"]:
        boxCollider.node.setLinearVelocity(Vec3(-5, 0, 0))
    if keys["right"]:
        boxCollider.node.setLinearVelocity(Vec3(5, 0, 0))

@sdl3.SDL_AppIterate_func
def SDL_AppIterate(appstate):

    keyboard()

    world.doPhysics(0.016)

    boxCollider.update()
    box2Collider.update()

    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)

    if showCollider:
        vertices = getCubeVertices(box2Collider.position, box2Collider.rotation, 1.5)

        # Create a cube
        #    v5----- v1
        #   /|      /|
        #  v4------v0|
        #  | |     | |
        #  | |v7---|-|v3
        #  |/      |/
        #  v6------v2

        # v0 - v1
        lineDrawer.draw(start = vertices[0], end = vertices[1],
            color = glm.vec3(0, 1, 0), thickness = 0.2)
        # v0 - v2
        lineDrawer.draw(start = vertices[0], end = vertices[2],
            color = glm.vec3(0, 1, 0), thickness = 0.2)
        # v2 - v3
        lineDrawer.draw(start = vertices[2], end = vertices[3],
            color = glm.vec3(0, 1, 0), thickness = 0.2)
        # v3 - v1
        lineDrawer.draw(start = vertices[3], end = vertices[1],
            color = glm.vec3(0, 1, 0), thickness = 0.2)
        # v1 - v0
        lineDrawer.draw(start = vertices[1], end = vertices[0],
            color = glm.vec3(0, 1, 0), thickness = 0.2)

        # v4 - v5
        lineDrawer.draw(start = vertices[4], end = vertices[5],
            color = glm.vec3(0, 1, 0), thickness = 0.2)
        # v5 - v7
        lineDrawer.draw(start = vertices[5], end = vertices[7],
            color = glm.vec3(0, 1, 0), thickness = 0.2)
        # v7 - v6
        lineDrawer.draw(start = vertices[7], end = vertices[6],
            color = glm.vec3(0, 1, 0), thickness = 0.2)
        # v6 - v4
        lineDrawer.draw(start = vertices[6], end = vertices[4],
            color = glm.vec3(0, 1, 0), thickness = 0.2)

        # v0 - v4
        lineDrawer.draw(start = vertices[0], end = vertices[4],
            color = glm.vec3(0, 1, 0), thickness = 0.2)
        # v2 - v6
        lineDrawer.draw(start = vertices[2], end = vertices[6],
            color = glm.vec3(0, 1, 0), thickness = 0.2)
        # v3 - v7
        lineDrawer.draw(start = vertices[3], end = vertices[7],
            color = glm.vec3(0, 1, 0), thickness = 0.2)
        # v1 - v5
        lineDrawer.draw(start = vertices[1], end = vertices[5],
            color = glm.vec3(0, 1, 0), thickness = 0.2)

    axesSize = 50
    # X
    lineDrawer.draw(start = glm.vec3(-axesSize, 0, 0), end = glm.vec3(axesSize, 0, 0),
        color = glm.vec3(1, 0, 0), thickness = 0.1)
    # Y
    lineDrawer.draw(start = glm.vec3(0, -axesSize, 0), end = glm.vec3(0, axesSize, 0),
        color = glm.vec3(0, 1, 0), thickness = 0.1)
    # Z
    lineDrawer.draw(start = glm.vec3(0, 0, -axesSize), end = glm.vec3(0, 0, axesSize),
        color = glm.vec3(0, 0, 1), thickness = 0.1)

    # Static collider
    position = floorCollider.position
    size = floorCollider.size
    rotation = floorCollider.rotation
    boxDrawer.draw(projViewMatrix, position, size, rotation, green3Texture)

    # Box
    position = boxCollider.position
    size = boxCollider.size
    rotation = boxCollider.rotation
    boxDrawer.draw(projViewMatrix, position, size, rotation, crateTexture)

    # Box2
    position = box2Collider.position
    size = box2Collider.size
    rotation = box2Collider.rotation
    boxDrawer.draw(projViewMatrix, position, size, rotation, crateTexture)

    sdl3.SDL_GL_SwapWindow(window)
    return sdl3.SDL_APP_CONTINUE

@sdl3.SDL_AppQuit_func
def SDL_AppQuit(appstate, result):
    global glContext
    sdl3.SDL_GL_DestroyContext(glContext)
    # SDL will clean up the window/renderer for us
math_helper.py
import math

import glm


class MathHelper:

    # Sets a quat from the given angle and rotation axis, then returns it
    @staticmethod
    def setAxisAngle(axis, rad):
        rad = rad * 0.5
        s = math.sin(rad)
        out = glm.quat()
        out.x = s * axis[0]
        out.y = s * axis[1]
        out.z = s * axis[2]
        out.w = math.cos(rad)
        return out

    # This is port of https://glmatrix.net/docs/quat.js.html#line652
    # Sets a quaternion to represent the shortest rotation from one vector to another
    # Both vectors are assumed to be unit length
    @staticmethod
    def rotationTo(initialVector, destinationVector):
        xUnitVec3 = glm.vec3(1, 0, 0)
        yUnitVec3 = glm.vec3(0, 1, 0)
        out = glm.quat()

        dot = glm.dot(destinationVector, initialVector)

        if dot < -0.999999:
            tmpvec3 = glm.cross(initialVector, xUnitVec3)
            if glm.length(tmpvec3) < 0.000001:
                tmpvec3 = glm.cross(initialVector, yUnitVec3)
            tmpvec3 = glm.normalize(tmpvec3)
            out = MathHelper.setAxisAngle(tmpvec3, math.pi)
            return out
        elif dot > 0.999999:
            out.x = 0
            out.y = 0
            out.z = 0
            out.w = 1
            return out
        else:
            tmpvec3 = glm.cross(initialVector, destinationVector)
            out.x = tmpvec3[0]
            out.y = tmpvec3[1]
            out.z = tmpvec3[2]
            out.w = 1 + dot
            return glm.normalize(out)
texture_loader.py
from OpenGL.GL import *
from PIL import Image


def loadTexture(path):
    image = Image.open(path)
    data = image.convert("RGBA").tobytes()
 
    glEnable(GL_TEXTURE_2D)
 
    texture = glGenTextures(1)
    glBindTexture(GL_TEXTURE_2D, texture)
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE)
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE)
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, image.width, image.height, 0,
        GL_RGBA, GL_UNSIGNED_BYTE, data)
    glBindTexture(GL_TEXTURE_2D, 0)
 
    return texture

To run this example you should:

pip Panda3D PySDL3 numpy PyOpenGL Pillow PyGLM
  • Run the example:
py main.py
  • Move a cube with WASD or arrow-keys
  • You can disable drawing of colliders by setting the showColliders variable to False in the beginning of the main.cpp

I will try to write a sphere with lines later. I will explain a little how I solve it below.

I can get the position and quaternion of object:

    def update(self):
        self.position.x = self.node.getTransform().pos.x
        self.position.y = self.node.getTransform().pos.y
        self.position.z = self.node.getTransform().pos.z

        hpr = self.node.getTransform().getHpr()
        quat = Quat()
        quat.setHpr(hpr)

        self.rotation.w = quat.getX()
        self.rotation.x = quat.getY()
        self.rotation.y = quat.getZ()
        self.rotation.z = quat.getW()

I asked the following question to Deep Seek: I know the position, size and quaternion of the cube. How do I find out the coordinates of the vertices? I asked to show examples in three languages: Python (PyGLM), C++ (GLM), JavaScript (glMatrix):

Python Implementation with PyGLM
import glm  # Make sure PyGLM is installed: `pip install PyGLM`

def get_cube_vertices(position, size, rotation_quat):
    # Half-size for local vertices
    s = size * 0.5

    # Local vertices (untransformed, centered at origin)
    local_vertices = [
        glm.vec3( s,  s,  s),  # v0
        glm.vec3( s,  s, -s),  # v1
        glm.vec3( s, -s,  s),  # v2
        glm.vec3( s, -s, -s),  # v3
        glm.vec3(-s,  s,  s),  # v4
        glm.vec3(-s,  s, -s),  # v5
        glm.vec3(-s, -s,  s),  # v6
        glm.vec3(-s, -s, -s)   # v7
    ]

    # Rotate and translate each vertex
    world_vertices = []
    for vertex in local_vertices:
        # Apply rotation (quaternion * vertex)
        rotated = rotation_quat * vertex
        # Apply translation
        world_vertex = rotated + position
        world_vertices.append(world_vertex)

    return world_vertices

# Example usage
position = glm.vec3(1.0, 2.0, 3.0)  # Cube center
size = 2.0                           # Side length
rotation_quat = glm.quat(glm.vec3(0, glm.radians(45), 0))  # 45° around Y-axis

vertices = get_cube_vertices(position, size, rotation_quat)

# Print vertices
for i, vertex in enumerate(vertices):
    print(f"v{i}: ({vertex.x:.2f}, {vertex.y:.2f}, {vertex.z:.2f})")

Output:

The program prints the world-space coordinates of all 8 vertices, e.g.:

v0: (2.41, 3.00, 3.00)
v1: (2.41, 3.00, 1.00)
v2: (0.00, 1.00, 3.00)
...
C++ Implementation with GLM
#include <glm/glm.hpp>
#include <glm/gtc/quaternion.hpp>
#include <glm/gtx/quaternion.hpp>
#include <vector>
#include <iostream>

std::vector<glm::vec3> getCubeVertices(
    const glm::vec3& position,
    float size,
    const glm::quat& rotation
) {
    // Half-size for local vertices
    float s = size * 0.5f;

    // Local vertices (untransformed, centered at origin)
    std::vector<glm::vec3> localVertices = {
        { s,  s,  s},  // v0
        { s,  s, -s},  // v1
        { s, -s,  s},  // v2
        { s, -s, -s},  // v3
        {-s,  s,  s},  // v4
        {-s,  s, -s},  // v5
        {-s, -s,  s},  // v6
        {-s, -s, -s}   // v7
    };

    // Rotate and translate each vertex
    std::vector<glm::vec3> worldVertices;
    for (const auto& vertex : localVertices) {
        // Apply rotation (quaternion * vertex)
        glm::vec3 rotated = rotation * vertex;
        // Apply translation
        glm::vec3 worldVertex = rotated + position;
        worldVertices.push_back(worldVertex);
    }

    return worldVertices;
}

int main() {
    // Example usage
    glm::vec3 position(1.0f, 2.0f, 3.0f);  // Cube center
    float size = 2.0f;                      // Side length
    glm::quat rotation = glm::angleAxis(glm::radians(45.0f), glm::vec3(0.0f, 1.0f, 0.0f));  // 45° around Y-axis

    std::vector<glm::vec3> vertices = getCubeVertices(position, size, rotation);

    // Print vertices
    for (size_t i = 0; i < vertices.size(); ++i) {
        std::cout << "v" << i << ": (" 
                  << vertices[i].x << ", " 
                  << vertices[i].y << ", " 
                  << vertices[i].z << ")\n";
    }

    return 0;
}

Output:

The program prints the world-space coordinates of all 8 vertices, e.g.:

v0: (2.41421, 3, 3.0)
v1: (2.41421, 3, 1.0)
v2: (0.0, 1, 3.0)
...
JavaScript Implementation with glMatrix
import { vec3, quat } from 'gl-matrix';

function getCubeVertices(position, size, rotationQuat) {
    // Half-size for local vertices
    const s = size * 0.5;

    // Local vertices (untransformed, centered at origin)
    const localVertices = [
        [ s,  s,  s],  // v0
        [ s,  s, -s],  // v1
        [ s, -s,  s],  // v2
        [ s, -s, -s],  // v3
        [-s,  s,  s],  // v4
        [-s,  s, -s],  // v5
        [-s, -s,  s],  // v6
        [-s, -s, -s]   // v7
    ];

    // Rotate and translate each vertex
    const worldVertices = [];
    const rotated = vec3.create();
    const worldVertex = vec3.create();

    for (const vertex of localVertices) {
        // Apply rotation (quaternion * vertex)
        vec3.transformQuat(rotated, vertex, rotationQuat);
        // Apply translation
        vec3.add(worldVertex, rotated, position);
        worldVertices.push([...worldVertex]);  // Convert to plain array
    }

    return worldVertices;
}

// Example usage
const position = [1.0, 2.0, 3.0];  // Cube center
const size = 2.0;                   // Side length
const rotationQuat = quat.create();
quat.setAxisAngle(rotationQuat, [0, 1, 0], Math.PI / 4);  // 45° around Y-axis

const vertices = getCubeVertices(position, size, rotationQuat);

// Print vertices
vertices.forEach((vertex, i) => {
    console.log(`v${i}: (${vertex[0].toFixed(2)}, ${vertex[1].toFixed(2)}, ${vertex[2].toFixed(2)})`);
});

Output:

The program logs the world-space coordinates of all 8 vertices, e.g.:

v0: (2.41, 3.00, 3.00)
v1: (2.41, 3.00, 1.00)
v2: (0.00, 1.00, 3.00)
...
1 Like