Performance issues with Rigid Body Combiner

Hey All,

I’m a newcomer to panda3d, but I’m loving it so far. I’m learning my way through it by slowly recreating (and piecing together tutorials/code snippets) into one of my all-time favorite games, Minecraft. So far I can procedurally generate a giant chunk of blocks. My goal is to be able to render this fairly fast, and also get a decent frame-rate while tumbling around it, assuming I’ll eventually have many more chunks in the scene. I heard that a great way to improve frame rate performance is to reduce the # of meshes in the scene by using the Rigid Body Combiner’s collect() function. This definitely helps in-game performance, but that call seems extremely costly, and it’s taking upwards of 2 minutes to collect all the meshes. Is there anything I can change about my code to make collect run faster? Or, maybe some other method to achieve similar results?

Below is my code for reference:

from panda3d.core import Geom, GeomTriangles, GeomVertexWriter
from panda3d.core import GeomVertexFormat, GeomVertexData
from panda3d.core import Texture, GeomNode
from direct.showbase.ShowBase import ShowBase
from panda3d.core import RigidBodyCombiner, NodePath
from panda3d.core import Texture

import sys, copy

class MyApp(ShowBase):
    def __init__(self):
        ShowBase.__init__(self)
        self.setFrameRateMeter(True)
        self.accept( "escape" , sys.exit)

        c = Chunk()
        for x in range(Chunk.MAX_CHUNK_X):
            for y in range(Chunk.MAX_CHUNK_Y):
                for z in range(Chunk.MAX_CHUNK_Z):
                    c.setElement(x, y, z, 1)
        c.generate()

def makeSquare(x1,y1,z1, x2,y2,z2):
	format=GeomVertexFormat.getV3t2()
	vdata=GeomVertexData('square', format, Geom.UHStatic)
	vertex=GeomVertexWriter(vdata, 'vertex')
	texcoord=GeomVertexWriter(vdata, 'texcoord')
	
	if x1!=x2:
		vertex.addData3f(x1, y1, z1)
		vertex.addData3f(x2, y1, z1)
		vertex.addData3f(x2, y2, z2)
		vertex.addData3f(x1, y2, z2)
	else:
		vertex.addData3f(x1, y1, z1)
		vertex.addData3f(x2, y2, z1)
		vertex.addData3f(x2, y2, z2)
		vertex.addData3f(x1, y1, z2)
	texcoord.addData2f(0.0, 1.0)
	texcoord.addData2f(0.0, 0.0)
	texcoord.addData2f(1.0, 0.0)
	texcoord.addData2f(1.0, 1.0)

	tri1=GeomTriangles(Geom.UHStatic)
	tri2=GeomTriangles(Geom.UHStatic)

	tri1.addVertex(0)
	tri1.addVertex(1)
	tri1.addVertex(3)

	tri2.addConsecutiveVertices(1,3)

	tri1.closePrimitive()
	tri2.closePrimitive()

	square=Geom(vdata)
	square.addPrimitive(tri1)
	square.addPrimitive(tri2)
	return square

class Chunk:
    MAX_CHUNK_X = 16
    MAX_CHUNK_Y = 16
    MAX_CHUNK_Z = 256
    INITIAL_CHUNK_DATA = bytearray(MAX_CHUNK_X*MAX_CHUNK_Y*MAX_CHUNK_Z)

    def __init__(self):
        self.data = copy.copy(Chunk.INITIAL_CHUNK_DATA)
        self.rbc = RigidBodyCombiner('rbc')
        self.rbcnp = NodePath(self.rbc)
        self.rbcnp.reparentTo(render)

    def setElement(self, x, y, z, element):
        self.data[x + y*Chunk.MAX_CHUNK_X + z*Chunk.MAX_CHUNK_X*Chunk.MAX_CHUNK_Y] = element

    def getElement(self, x, y, z):
        if x < 0 or y < 0 or z < 0 \
           or x >= Chunk.MAX_CHUNK_X or y >= Chunk.MAX_CHUNK_Y or z >= Chunk.MAX_CHUNK_Z:
            return 0 # Dummy air voxel data, change once we
                     # have cross-chunk access
        return self.data[x + y*Chunk.MAX_CHUNK_X + z*Chunk.MAX_CHUNK_X*Chunk.MAX_CHUNK_Y]

    def generate(self):
        self.node = GeomNode('chunk')
        self.nodePath = self.rbcnp.attachNewNode(self.node)

        for x in range(Chunk.MAX_CHUNK_X):
            for y in range(Chunk.MAX_CHUNK_Y):
                for z in range(Chunk.MAX_CHUNK_Z):
                    squares = []
                    if self.getElement(x-1, y, z) == 0:
                        squares.append(makeSquare(x,y+1,z, x,y,z+1))
                    if self.getElement(x, y-1, z) == 0:
                        squares.append(makeSquare(x,y,z, x+1,y,z+1))
                    if self.getElement(x, y, z-1) == 0:
                        squares.append(makeSquare(x+1,y,z, x,y+1,z))
                    if self.getElement(x+1, y, z) == 0:
                        squares.append(makeSquare(x+1,y,z, x+1,y+1,z+1))
                    if self.getElement(x, y+1, z) == 0:
                        squares.append(makeSquare(x+1,y+1,z, x,y+1,z+1))
                    if self.getElement(x, y, z+1) == 0:
                        squares.append(makeSquare(x,y,z+1, x+1,y+1,z+1))
                    if len(squares) > 0:
                        blockNode = GeomNode('block')
                        for sq in squares:
                            blockNode.addGeom(sq)
                        blockNodePath = self.nodePath.attachNewNode(blockNode)
                        #tex = loader.loadTexture('assets/grass.png')
                        #blockNodePath.setTexture(tex, 1)

        self.rbc.collect()

app = MyApp()
app.run()

How much game coding experience do you have? Voxels are a fairly advanced topic, and game engines are not really geared towards rendering them quickly (as you are finding out). They require different optimisations and data layouts to most other kinds of games, so it might be hard to get that performance in Panda3d.

The only way I could think of to do this would be something like this:

  1. break the world up into sizeable chunks (you will need to test to see which size is the best, I think)
  2. for each chunk:
    Find out which faces to draw to show the whole chunk
    Write them all into a single static geom with the GeomVertexWriter
    3)render these chunks
  3. whenever you change the environment data, just trash the associated chunk and remake it

This way, you never need to worry about being able to move individual blocks, because they don’t actually exist.

edit: A bit of research reveals that this is what Minecraft does. Each of minecraft’s “chunks” is divided into 16x16x16 big render chunks, which are simple geometry that is replaced each time it’s changed. I’m not sure how it manages to render so many render chunks, though. If I had to guess wildly, I would say it combines far-away chunks into fewer renderchunks.