Help with closing primitives

So, I am making a lot of progress on my minecraft terrain clone. But I’m hitting the same performance hit everyone else is. I found a couple of examples of drawing each face, then making cubes out of each face. This gave me a much needed performance boost, but it’s still too slow.

I think what I need to do is leave the primitive open while creating all the cubes, then closing it after I’ve created them all. The problem is, I’m having a difficult time conceptualizing just how to do this. So I’ll just share my working (but horribly horribly slow code):

	def myNormalize(self, myVec):
		myVec.normalize()
		return myVec
	
	def makeCube(self,x,y,z):
		# Draw a cube with the default x,y,z plane
		# being the front left bottom point
		square0=self.makeSquare(x,y,z,x+1,y,z+1)
		square1=self.makeSquare(x,y,z,x,y+1,z+1)
		square2=self.makeSquare(x+1,y,z,x+1,y+1,z+1)
		square3=self.makeSquare(x,y+1,z,x+1,y+1,z+1)
		square4=self.makeSquare(x,y,z+1,x+1,y+1,z+1)
		square5=self.makeSquare(x,y,z,x+1,y+1,z)
		snode=GeomNode('square')
		snode.addGeom(square0)
		snode.addGeom(square1)
		snode.addGeom(square2)
		snode.addGeom(square3)
		snode.addGeom(square4)
		snode.addGeom(square5)
		cube=render.attachNewNode(snode)
	
		# By default, geomNodes face only one way.
		cube.setTwoSided(True)
		# Load up our texture and apply it to the cube...the way the UV mapping works
		# All textures should be added to this file
		cube.setTexture(loader.loadTexture("assets/models/glow-diffuse.png"))
		glowMap = loader.loadTexture("assets/models/glow-lightmap.png")
		glowTex = TextureStage('glowTex')
		glowTex.setMode(TextureStage.MGlow)
		cube.setTexture(glowTex, glowMap)
		cube.flattenStrong()
		

	def makeSquare(self, x1,y1,z1, x2,y2,z2):
		format=GeomVertexFormat.getV3n3cpt2()
		vdata=GeomVertexData('square', format, Geom.UHDynamic)

		vertex=GeomVertexWriter(vdata, 'vertex')
		normal=GeomVertexWriter(vdata, 'normal')
		color=GeomVertexWriter(vdata, 'color')
		texcoord=GeomVertexWriter(vdata, 'texcoord')
	
		#make sure we draw the square in the right plane
		if x1!=x2:
			vertex.addData3f(x1, y1, z1)
			vertex.addData3f(x2, y1, z1)
			vertex.addData3f(x2, y2, z2)
			vertex.addData3f(x1, y2, z2)

			normal.addData3f(self.myNormalize(Vec3(2*x1-1, 2*y1-1, 2*z1-1)))
			normal.addData3f(self.myNormalize(Vec3(2*x2-1, 2*y1-1, 2*z1-1)))
			normal.addData3f(self.myNormalize(Vec3(2*x2-1, 2*y2-1, 2*z2-1)))
			normal.addData3f(self.myNormalize(Vec3(2*x1-1, 2*y2-1, 2*z2-1)))
		
		else:
			vertex.addData3f(x1, y1, z1)
			vertex.addData3f(x2, y2, z1)
			vertex.addData3f(x2, y2, z2)
			vertex.addData3f(x1, y1, z2)

			normal.addData3f(self.myNormalize(Vec3(2*x1-1, 2*y1-1, 2*z1-1)))
			normal.addData3f(self.myNormalize(Vec3(2*x2-1, 2*y2-1, 2*z1-1)))
			normal.addData3f(self.myNormalize(Vec3(2*x2-1, 2*y2-1, 2*z2-1)))
			normal.addData3f(self.myNormalize(Vec3(2*x1-1, 2*y1-1, 2*z2-1)))

		texcoord.addData2f(0.0, 1.0)
		texcoord.addData2f(0.0, 0.0)
		texcoord.addData2f(1.0, 0.0)
		texcoord.addData2f(1.0, 1.0)

		# quads aren't directly supported by the Geom interface
		# So we have to combine two triangles to make a square
		tri1=GeomTriangles(Geom.UHDynamic)
		tri2=GeomTriangles(Geom.UHDynamic)

		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
		
	def buildChunk(self, x1, x2, y1, y2, z1, z2, world,level,chunk):
		for x in range (x1, x2):
			self.blocks[x] = {}
			for y in range (y1, y2):
				self.blocks[x][y] = {}
				for z in range (z1, z2):
					self.blocks[x][y][z] = self.getPerVal(x,y,z,world,level,chunk)
					if z == 1:
						self.makeCube(x,y,z)
					elif self.blocks[x][y][z] != 0:
						self.makeCube(x,y,z)
					
	
					
	def getPerVal(self,x,y,z,world,level,chunk):
		self.gpv = world(level,chunk,x,y,z)
		return self.gpv

My interest is in figuring out how to move:

		tri1.closePrimitive()
		tri2.closePrimitive()


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

Into the portion after I’ve created my “chunk” so that each chunk is it’s own primitive. Is that possible? How might I go about doing that?

Thanks!
ZM

(the example code from the procedurally create cube was very helpful, as you can probably tell!)

Edit to add:
I think I need to create a dictionary that stores everything in it, then iterate through the dictionary to close it. But that seems to be easier said then implemented.

closePrimitive() only means that you are marking the end of the triangle; there’s not actually a difference between an “open” and a “closed” primitive. In fact, in the case of GeomTriangles, closePrimitive() is a no-op, because every triangle has exactly three vertices and Panda already knows that and doesn’t need you to tell it when you’ve reached the end of a triangle. (It’s still a good idea to call it anyway out of robustness, but it’s true that you can completely omit it and it will still work exactly the same way.)

But why are you creating each triangle in its own GeomTriangles object? A GeomTriangles can hold multiple triangles (thus the name, it’s a GeomTriangles, not a GeomTriangle).

You could do it like this:

      tri=GeomTriangles(Geom.UHDynamic)

      tri.addVertex(0)
      tri.addVertex(1)
      tri.addVertex(3)
      tri.closePrimitive()
   
      tri.addConsecutiveVertices(1,3)
      tri.closePrimitive()

      square=Geom(vdata)
      square.addPrimitive(tri) 

But you need to go even further than that. You will have peformance issues if you have more than a few hundred GeomTriangles (or more than a few hundred Geoms). So instead of creating a different Geom and a different GeomTriangles for each face of the cube, you should be creating just one Geom and one GeomTriangles that stores all of the cubes at once.

Of course that makes it hard to manipulate the cubes individually. So this is where you have to be clever and figure out how you want to combine geometry enough to get good performance, but not so much that you can’t use it.

David

Sorry if this is a dumb question…so, I do want to take it a step further and combine all of my vertex data into one Geom and one GeomTriangles.

So, conceptually, what I would be doing is moving

      tri=GeomTriangles(Geom.UHDynamic)

      tri.addVertex(0)
      tri.addVertex(1)
      tri.addVertex(3)
      tri.closePrimitive()
   
      tri.addConsecutiveVertices(1,3)
      tri.closePrimitive()

      square=Geom(vdata)
      square.addPrimitive(tri)

Out of my makeSquare method, and putting it at the end of my “buildChunk” method instead? Basically have one Geom and one GeomTriangles object per 4x4x4 chunk of blocks? I assume I would then need to move this:

		square0=self.makeSquare(x,y,z,x+1,y,z+1)
		square1=self.makeSquare(x,y,z,x,y+1,z+1)
		square2=self.makeSquare(x+1,y,z,x+1,y+1,z+1)
		square3=self.makeSquare(x,y+1,z,x+1,y+1,z+1)
		square4=self.makeSquare(x,y,z+1,x+1,y+1,z+1)
		square5=self.makeSquare(x,y,z,x+1,y+1,z)
		snode=GeomNode('square')
		snode.addGeom(square0)
		snode.addGeom(square1)
		snode.addGeom(square2)
		snode.addGeom(square3)
		snode.addGeom(square4)
		snode.addGeom(square5)
		cube=render.attachNewNode(snode)

There as well? Since I should be adding all of the ‘squares’ from all of my ‘cubes’ into the addGeom node after I’ve collected all of the vertex data I need?

Again, sorry for how dumb these questions are, but I grabbed most of this from examples and the procedurally generate cube example so my understanding of what I’m doing is a bit weaker in this regard then some of the more traditional and familiar things in Panda :slight_smile:

That sounds like the right idea.

Another approach, of course, is to generate everything individually as you are doing now, and then call root.flattenStrong() at the end. flattenStrong() is a function that walks the scene graph hierarchy beginning at the node you call it on, and finds all the geometry at and below that node, and moves it into a single Geom and GeomTriangles objects. (Or, more accurately, into as few as possible. Sometimes multiple Geoms are still required because, for instance, you have different textures applied to some geometry, which means they can’t be combined together.)

Of course, flattenStrong() isn’t free, but it’s not that slow either (it’s much faster than doing all that processing in Python). But if you’re going to be creating geometry, it might be a little bit faster to create it the right way first, rather than create it the wrong way and then massage it into the right way afterwards.

Or, maybe it gets too complicated to create it the right way first, and in that case it might make sense to create everything individually and flattenStrong() it. It also might be the easiest way to manipulate your cubes (store a copy of all of your individual cubes, and then just copy it and flattenStrong() it whenever you make changes for rendering).

It’s all up to you. :slight_smile:

David

So, when I call flatten strong on each cube in my makeCube method, I get a performance boost. However, if I call flattenStrong() at the end of my buildChunk method, my performance absolutely tanks, e.g. like this:

	def makeCube(self,x,y,z):
		# Draw a cube with the default x,y,z plane
		# being the front left bottom point
		square0=self.makeSquare(x,y,z,x+1,y,z+1)
		square1=self.makeSquare(x,y,z,x,y+1,z+1)
		square2=self.makeSquare(x+1,y,z,x+1,y+1,z+1)
		square3=self.makeSquare(x,y+1,z,x+1,y+1,z+1)
		square4=self.makeSquare(x,y,z+1,x+1,y+1,z+1)
		square5=self.makeSquare(x,y,z,x+1,y+1,z)
		snode=GeomNode('square')
		snode.addGeom(square0)
		snode.addGeom(square1)
		snode.addGeom(square2)
		snode.addGeom(square3)
		snode.addGeom(square4)
		snode.addGeom(square5)
		self.cube=render.attachNewNode(snode)
	
		# By default, geomNodes face only one way.
		self.cube.setTwoSided(True)
		# Load up our texture and apply it to the cube...the way the UV mapping works
		# All textures should be added to this file
		self.cube.setTexture(loader.loadTexture("assets/models/glow-diffuse.png"))
		glowMap = loader.loadTexture("assets/models/glow-lightmap.png")
		glowTex = TextureStage('glowTex')
		glowTex.setMode(TextureStage.MGlow)
		self.cube.setTexture(glowTex, glowMap)
...

	def buildChunk(self, x1, x2, y1, y2, z1, z2, world,level,chunk):
		for x in range (x1, x2):
			self.blocks[x] = {}
			for y in range (y1, y2):
				self.blocks[x][y] = {}
				for z in range (z1, z2):
					self.blocks[x][y][z] = self.getPerVal(x,y,z,world,level,chunk)
					if z == 1:
						self.makeCube(x,y,z)
					elif self.blocks[x][y][z] != 0:
						self.makeCube(x,y,z)
		self.cube.flattenStrong()

Am I doing it wrong?

I don’t see anything wrong, but the other side of flattening is that once a node has been flattened, Panda must then render either all of the vertices in the node, or none of them. (Before flattening, Panda can eliminate those parts of the node that are outside the viewing frustum, but not after flattening.) So, if you flatten the whole world, suddenly Panda has to render the whole world every frame.

If your scene is simple enough that your graphics card can handle rendering the entire scene every frame, this isn’t a problem; but if you overwhelm your graphics card, you might need to reconsider your level of flattening.

You can use root.analyze() to get a report of how much geometry you’ve got beneath the root node, and how it is distributed. Try looking at it before and after flattening. It shouldn’t change (much) in the number of vertices; if it does, something’s wrong.

You can also see some of this in PStats–in PStats, you can see the number of vertices (and the number of Geoms) that are actually being sent to the graphics card. Before flattenStrong(), the number of vertices will be relatively low, and the number of Geoms will be relatively high. After flattenStrong(), the number of Geoms will be lower (because they have been combined together), while the number of vertices will increase (because Panda can no longer cull out individual pieces).

The trick is to find the balance point between Geoms and triangles that is appropriate for your graphics card.

David

You’ve given me a lot of avenues to explore. Thanks David!