Custom debug drawer

Hello,

AI wrote that the custom debug drawer can be implemented like this but it is wrong:

from panda3d.bullet import BulletDebugDrawer

class CustomDebugDrawer(BulletDebugDrawer):
    def __init__(self):
        BulletDebugDrawer.__init__(self)
        self.vertices = []
    
    def draw_line(self, p1, p2, color):
        self.vertices.extend([p1, p2])
    
    def draw_contact_point(self, point, normal, distance, life_time, color):
        pass
    
    def report_error_warning(self, message):
        print("Bullet Warning:", message)
    
    def draw3d_text(self, location, text):
        pass
    
    def set_debug_mode(self, mode):
        self.mode = mode
    
    def get_debug_mode(self):
        return self.mode

# Usage:
debug_drawer = CustomDebugDrawer()
world.setDebugDrawer(debug_drawer)
world.debugDrawWorld()

# Now access collected vertices
vertices = debug_drawer.vertices

I need this function:

    def draw_line(self, p1, p2, color):
        self.vertices.extend([p1, p2])

I want to draw lines by myself. Is is possible?

If you prefer to communicate with the AI ​​first, why ask secondary questions to the community? Ask them to the AI ​​or consult the documentation or forum first - here.

The Gemini and DeepSeek wrote that it is possible and suggested a few way but that ways don’t work. I have published an example above to show what I want. I need to get the start and the end of the collider edge like this:

    def draw_line(self, p1, p2, color):
        self.vertices.extend([p1, p2])

The Python interpreter said that there is no class with the BulletDebugDrawer name:

from panda3d.bullet import BulletDebugDrawer

Another solution from DeepSeek:

Method 2: Parsing BulletDebugNode Geometry

You can extract the debug visualization geometry:

def get_debug_vertices(debug_node):
    vertices = []
    geom_node = debug_node.getChild(0)
    
    for i in range(geom_node.getNumGeoms()):
        geom = geom_node.getGeom(i)
        vdata = geom.getVertexData()
        reader = GeomVertexReader(vdata, 'vertex')
        
        while not reader.isAtEnd():
            vertex = reader.getData3()
            vertices.append(vertex)

I tried to apply it to my code but there is no children elements in the debug_node:

debug_node.getChild(0)

I tried Method 2 like this:

from panda3d.bullet import BulletWorld, BulletDebugNode

debugNode = BulletDebugNode('Debug')

world = BulletWorld()
world.setGravity(Vec3(0, -9.81, 0))
world.setDebugNode(debugNode)
debugNode.showWireframe(True)

I call the following method In the simulation loop:

world.doPhysics(0.016)

It the debug_node.getNumChildren() method returns 0:

def get_debug_vertices(debug_node):
    vertices = []
    # geom_node = debug_node.getChild(0)
    print(debug_node.getNumChildren())

Why would you listen to AI? They’re not reliable sources of information. :/

(Or, as you’ve seen, reliable sources of code.)

All right, let’s forget what the AI generated: what are you actually trying to do, specifically?

I gather that you want to make your own debugging visualiser: what features do you want, and how do you want it to differ from the debug visualisers that the engine already provides?

There is only one relevant physics engine in the Python world, and that is the Panda3D community port of Bullet Physics. For example, the last activity on PyODE was March 22, 2010 PyODE. I can’t use PyBullet because its API is very far from the C++ version of Bullet Physics. The Panda3D port of Bullet Physics is very close to the C++ version of Bullet Physics.

I make demos in C++, SDL3, OpenGL and Bullet Physics. And in JavaScript, WebGL and Ammo.js. I want to do them in parallel in Python, PySDL3 and Bullet Physics. In C++ it was easy to get a method that gives the start and end of a collider boundary segment:

class DebugDrawer : public btIDebugDraw
{
public:
    virtual void drawLine(const btVector3 &from, const btVector3 &to,
        const btVector3 &color) override;

I need to get the from, to and color values to draw a line segment for colliders. It was possible to draw cubes and spheres instead of colliders, but I would like to draw segments, as in the btIDebugDraw method.

move-box-bullet-physics-opengl-pysdl3.zip (110.0 KB)

pip Panda3D PySDL3 numpy PyOpenGL Pillow PyGLM
py main.py

move-box-bullet-physics-opengl-pysdl3

Ah, I see! Thank you for the explanation! :slight_smile:

Hmm… I’m not clear on this: what is “a collider boundary segment”? That is, if you have, say, a spherical collider, what is/are its “boundary segment(s)”?

(I’m reading your description to indicate some property or element of a collider; if that’s mistaken, please correct me!)

Likewise, I’m not clear on this property: what colour is to be used here…?

To determine the outer boundaries of an object, the overall dimensions are usually used in the case of simple shapes. If it is a mesh, then here you need to iterate over the data vertices.

Bullet Physics has a helper class called btIDebugDraw. You can inherit from this class and override the drawLine() method:

class DebugDrawer : public btIDebugDraw
{
public:
    virtual void drawLine(const btVector3 &from, const btVector3 &to,
        const btVector3 &color) override;

The drawLine() method will be automatically called when the world->debugDrawWorld() method is called. The start and end points of the segment, as well as the color of the segment, are passed through the parameters of the drawLine() method, so you can display a line:

void DebugDrawer::drawLine(const btVector3 &from, const btVector3 &to,
    const btVector3 &color)
{
    m_lineDrawer->draw(glm::vec3(from.x(), from.y(), from.z()),
        glm::vec3(to.x(), to.y(), to.z()), glm::vec3(color.x(), color.y(), color.z()), m_thickness);
}

Panda3D also uses the btIDebugDraw class in this file on line 76: panda3d/panda/src/bullet/bulletDebugNode.h at master · panda3d/panda3d · GitHub

Panda3D uses the drawLine() method:

I understand that much, but that’s not quite what I’m asking:

You keep saying “the segment”, but you haven’t specified what segment it is that you want to draw. That’s what I’m asking.

For example, let’s consider a spherical collider. Do you want to generate segments approximating the sphere? That’s essentially a maths question: determine how you want to depict your sphere (e.g. as an icosohedron, or via latitude/longitude, etc.), then use the relevant maths to generate points on the sphere, and pass those to your line-drawing method.

But you’ve said that you don’t want to draw a sphere. It’s thus not clear to me what depiction of (again, for example) a spherical collider you would want…

Once I have a better idea of what visualisation you’re trying to make with your segments, I might be better able to suggest how to generate start- and end- points for those segments.

Bullet Physics calls the drawLine method to draw a sphere. My demo in C++ and WASM: Mountain skybox using C++, SDL3, Emscripten, OpenGL ES 2.0, and Bullet Physics

Bullet Physics gives me the from, to, and color. I draw a line segment between the from and to points with the color that Bullet Physics gives me. I wrote the LineDrawer class in C++ to draw a line segment between two points:

class LineDrawer
{
public:
    LineDrawer(Lightless *lightless);

    void draw(const glm::vec3 &from, const glm::vec3 &to,
        const glm::vec3 &color, float thickness = 0.05f);
    void updateProjViewMatrix(const glm::mat4 &projViewMatrix);

private:
    void bind();

private:
    Lightless *m_lightless;
    glm::vec3 m_tempVec;
    glm::mat4 m_projViewMatrix;
};

I draw a line segment using this class:

void DebugDrawer::drawLine(const btVector3 &from, const btVector3 &to,
    const btVector3 &color)
{
    m_lineDrawer->draw(glm::vec3(from.x(), from.y(), from.z()),
        glm::vec3(to.x(), to.y(), to.z()), glm::vec3(color.x(), color.y(), color.z()), m_thickness);
}

I want to make the same in Python. The problem is that Panda3D has DebugDraw class in the private section: panda3d/panda/src/bullet/bulletDebugNode.h at master · panda3d/panda3d · GitHub

I think you should leave the internal debugging mode method alone. You need to know enough about the shape type of a physical object and its dimensions to draw its contours yourself using simple mathematics. Here’s an example to start with.

from panda3d.core import LineSegs, NodePath
from direct.showbase.ShowBase import ShowBase

class MyApp(ShowBase):

    def __init__(self):
        ShowBase.__init__(self)

        lines = LineSegs()
        lines.set_color(0, 1, 0, 1)
        lines.set_thickness(0.1)

        lines.move_to(0, 0, 0)
        lines.draw_to(0, 0, 1)

        np = NodePath(lines.create())
        np.reparent_to(render)

app = MyApp()
app.run()
1 Like

[edit]
Wait–I just thought to look at the Bullet sample programs, and… does the extant Bullet debug visualisation not already do what you want…?

It already allows its collision shapes to be displayed as wireframes–i.e., essentially, a set of line-segments.

See this screenshot from the “02_Shapes.py” sample-program:

Or is there something more-specific that you want from your visualisation…?
[/edit]

Okay, so you do want to draw a sphere–just in line-segments rather than faces. Fair enough!

Hmm… Why not expose your C++ implementation to Python? That seems like it might be simpler than re-implementing it, or trying to get around the DebugDraw class being private.

Of course, you could always just fork Panda itself, alter the source code such that DebugDraw isn’t private, and then compile your own build to work with.

But ultimately, I think that Serega’s advice, above, is good:

Instead of messing with the engine, or trying to work around elements that seem to be not intended for developer use, why not just make your own debug visualiser in Python?

I create a window using PySDL3. I draw objects using pure OpenGL with VBO, glDrawArrays(), Pillow, PyGLM and so on. I cannot use self.render. I need Bullet Physics only from Panda3D. It works well except the Bullet debug visualisation. I saw the example here: Bullet Debug Renderer — Panda3D Manual This example uses this code:

from panda3d.bullet import BulletDebugNode

debugNode = BulletDebugNode('Debug')
debugNode.showWireframe(True)
debugNode.showConstraints(True)
debugNode.showBoundingBoxes(False)
debugNode.showNormals(False)
debugNP = render.attachNewNode(debugNode)
debugNP.show()

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

I cannot use this line:

debugNP = render.attachNewNode(debugNode)

But I can to import BulletDebugNode:

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

I can create a BulletDebugNode object:

debugNode = BulletDebugNode('Debug')

And I can to set it as debug node with world.setDebugNode(debugNode):

world = BulletWorld()
world.setGravity(Vec3(0, -9.81, 0))
world.setDebugNode(debugNode)
debugNode.showWireframe(True)

But I cannot use debugNP.show() like in the official example: Bullet Debug Renderer — Panda3D Manual

from direct.showbase.DirectObject import DirectObject

o = DirectObject()
o.accept('f1', toggleDebug)

def toggleDebug():
    if debugNP.isHidden():
        debugNP.show()
    else:
        debugNP.hide()

As a result this method (debug_node.getNumChildren() returns 0:

def get_debug_vertices(debug_node):
    vertices = []
    # geom_node = debug_node.getChild(0)
    print(debug_node.getNumChildren())

This is my example. It shows a static cube (a big one like a floor) and a dynamic cube. It print 0 to the console because of this line debug_node.getNumChildren():
custom-debug-drawer-bullet-physics-opengl-pysdl3.zip (110.5 KB)

I call get_debug_vertices(debugNode) in the render loop:

@sdl3.SDL_AppIterate_func
def SDL_AppIterate(appstate):

    keyboard()

    world.doPhysics(0.016)

    boxCollider.update()

    get_debug_vertices(debugNode)

    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)

    # 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)

    sdl3.SDL_GL_SwapWindow(window)
    return sdl3.SDL_APP_CONTINUE
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.x = quat.getX()
        # self.rotation.y = quat.getY()
        # self.rotation.z = quat.getZ()
        # self.rotation.w = quat.getW()

        self.rotation.w = quat.getX()
        self.rotation.x = quat.getY()
        self.rotation.y = quat.getZ()
        self.rotation.z = quat.getW()
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, BulletDebugNode
from panda3d.core import Vec3

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

debugNode = BulletDebugNode('Debug')

glContext = None
window = None

boxDrawer = None
crateTexture = None
green3Texture = None

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(0, 5, 7), # 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))
world.setDebugNode(debugNode)
debugNode.showWireframe(True)

def get_debug_vertices(debug_node):
    vertices = []
    # geom_node = debug_node.getChild(0)
    print(debug_node.getNumChildren())
    
    # for i in range(geom_node.getNumGeoms()):
    #     geom = geom_node.getGeom(i)
    #     vdata = geom.getVertexData()
    #     reader = GeomVertexReader(vdata, 'vertex')
        
    #     while not reader.isAtEnd():
    #         vertex = reader.getData3()
    #         vertices.append(vertex)
    
    return vertices

boxCollider = None
floorCollider = None

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

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

    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()

    program = compileProgram(
       compileShader(vertexShaderSource, GL_VERTEX_SHADER),
       compileShader(fragmentShaderSource, GL_FRAGMENT_SHADER))
    glUseProgram(program)

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

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

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

    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
    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()

    get_debug_vertices(debugNode)

    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)

    # 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)

    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
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

I see that I cannot use BulletDebugNode to create a custom debug drawer class. But I can draw cubes for BulletBoxShape and I can load a sphere from Collada using pugixml to draw BulletSphereShape. I thought that I can get vertices from the BulletDebugNode but I see that I cannot make it without Panda3D’s render.

Ah, I see! Thank you for clarifying!

Hmm, okay, that is trickier.

In that case–at the least for colliders that use built-in mathematical shapes–I’d suggest not trying to work around Panda’s standard debug-node, but instead taking the mathematical approach.

The specifics would of course depend on the shape in question:

For spheres, generate the points for three circles–one aligned to each plane of the 3D space–and pass those to your line-drawing code.

This can be done, if I’m not much mistaken, by:

  • Determining an angular interval/step
    • The smaller, the more segments, and the less angled the result appears to be for a given viewing-distance
  • Looping for a number of iterations equal to 360 degrees or 2 * pi (as appropriate) divided by the angular interval. In that loop:
    • The current angle is given by: iteration index * angular interval
    • The next angle is given by: current angle + angular interval
    • For the XY-plane:
      • The current point is given by: centrePoint + (sin(currentAngle), cos(currentAngle), 0) * sphereRadius
      • The next point is given by: centrePoint + (sin(nextAngle), cos(nextAngle), 0) * sphereRadius
    • Similarly for the XZ and YZ planes
    • Pass these “current” and “next” points into your line-drawing method
  • At the end of the loop, draw a line between the last “next” point and the first “current” point. (Or iterate one more time than calculated above.)

(Naturally, this can be optimised a bit by storing the next angle and the next points, as the current -angle and -points of the next iteration should be equal to the next -angle and -points of the current iteration.)

For boxes, generate points based on the corners of the collider–i.e. at (pos.x - width, pos.y - length, pos.z - height), etc.–and pass those as appropriate to your line-drawing code.

And so on.

For polygonal objects, you’d have to get the vertices of each face, I imagine, and pass those to your line-drawing code.

I’m not sure of whether the colours used by the standard Bullet debug-node can be retrieved in code anywhere. If not, you may want to just reverse engineer them from whatever state the standard debug-node uses for them.

from direct.showbase.ShowBase import ShowBase
from panda3d.core import LineSegs, NodePath
import math

class MyApp(ShowBase):
    def __init__(self):
        ShowBase.__init__(self)

        radius = 5
        step = 12

        seg = LineSegs()
        seg.set_color(0, 1, 0, 1)
        seg.set_thickness(2)

        seg.move_to(radius, 0, 0)

        for angle in range(0, 361, step):
            x = radius*math.cos(math.radians(angle))
            y = radius*math.sin(math.radians(angle))
            seg.draw_to(x, y, 0)

        debugging_sphere = NodePath("debugging_sphere")
        debugging_sphere.reparent_to(render)

        line1 = NodePath(seg.create())
        line1.reparent_to(debugging_sphere)

        line2 = line1.copy_to(debugging_sphere)
        line2.set_hpr(0, 0, 90)
        
        line3 = line1.copy_to(debugging_sphere)
        line3.set_hpr(0, 90, 0)

app = MyApp()
app.run()

It looks simpler in the code, of course, you need to make allowances for the fact that pure OpenGL is used.

1 Like

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

This is a simple example showing how to use the LineDrawer class. This class draws a line between two points. I deform a 1x1 cube to draw a line in 3D space with a thickness of. For example, in draws XYZ axes like this:

axesSize = 200
# X
lineDrawer.draw(start=glm.vec3(-axesSize, 0, 0), end=glm.vec3(axesSize, 0, 0),
    color=glm.vec3(1, 0, 0), thickness=2)
# Y
lineDrawer.draw(start=glm.vec3(0, -axesSize, 0), end=glm.vec3(0, axesSize, 0),
    color=glm.vec3(0, 1, 0), thickness=2)
# Z
lineDrawer.draw(start=glm.vec3(0, 0, -axesSize), end=glm.vec3(0, 0, axesSize),
    color=glm.vec3(0, 0, 1), thickness=2)
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 math
import os

import glm
import numpy as np
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 line_drawer import LineDrawer

glContext = None
window = None

lineDrawer = None

canvasWidth = 400
canvasHeight = 400

# Create a perspective projection matrix and a view matrix
fovy = 45
near = 0.1
far = 500
projMatrix = glm.perspective(glm.radians(fovy),
    canvasWidth / canvasHeight, near, far)
viewMatrix = glm.lookAt(
    glm.vec3(50, 100, 300), # 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

vertexShaderSource = """
    attribute vec3 aPosition;
    uniform mat4 uMvpMatrix;

    void main()
    {
        gl_Position = uMvpMatrix * vec4(aPosition, 1.0);
    }
"""

fragmentShaderSource = """
    uniform vec3 uColor;

    void main()
    {
        gl_FragColor = vec4(uColor, 1.0);
    }
"""

@sdl3.SDL_AppInit_func
def SDL_AppInit(appstate, argc, argv):
    global glContext
    global window
    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, PyOpenGL".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.2, 0.2, 0.2, 1)
    glEnable(GL_DEPTH_TEST)

    program = compileProgram(
        compileShader(vertexShaderSource, GL_VERTEX_SHADER),
        compileShader(fragmentShaderSource, GL_FRAGMENT_SHADER))
    glUseProgram(program)

    # Create an object for drawing segments
    lineDrawer = LineDrawer(program, 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)
    return sdl3.SDL_APP_CONTINUE

@sdl3.SDL_AppIterate_func
def SDL_AppIterate(appstate):

    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)

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

    # First line
    lineDrawer.draw(start = glm.vec3(-60, 50, 0), end = glm.vec3(40, 80, 0),
        color = glm.vec3(1, 0.5, 0.5), thickness = 1)

    # Second line
    lineDrawer.draw(start = glm.vec3(-28, 37, 0), end = glm.vec3(80, 25, 0),
        color = glm.vec3(0.5, 0.5, 1), thickness = 3)

    # Polygonal chain
    color = glm.vec3(0.5, 1, 0.5)
    thickness = 5
    lineDrawer.draw(start = glm.vec3(-81, -73, 0), end = glm.vec3(-43, -20, 0),
        color = color, thickness = thickness)
    lineDrawer.draw(start = glm.vec3(-43, -20, 0), end = glm.vec3(0, -5, 0),
        color = color, thickness = thickness)
    lineDrawer.draw(start = glm.vec3(0, -5, 0), end = glm.vec3(25, -10, 0),
        color = color, thickness = thickness)
    lineDrawer.draw(start = glm.vec3(25, -10, 0), end = glm.vec3(52, -70, 0),
        color = color, thickness = thickness)
    lineDrawer.draw(start = glm.vec3(52, -70, 0), end = glm.vec3(77, -5, 0),
        color = color, thickness = thickness)

    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)

line-drawer-3d-opengl-pysdl3.zip (4.0 KB)

pip install PySDL3 PyOpenGL numpy
py main.py

1 Like