reduce number of baches

Hi, im fairly new to Panda3d and got some problems understanding the differences of GeomNode, PandaNode and Nodepath.
I adapted the Procedural-Cube example that comes with the Panda installation to display a minecraft-like fractal landscape.

the base idea is to repeat the following lines with the appropriate square coordinates.

square0=makeSquare( 1,-1,-1, 1, 1, 1)

Now comes the problem. After creating some thousand squares the frame rate drops drastically. I think the snode consists of to many batches and i tried to apply the flattenstrong() command to remove all the batches but flattenstrong is a function of nodepath. Is there a way to reparent the snode from addGeom to nodepath? (i only found the function to create a PandaNode as child of nodepath but that didn´t seem to be very helpfull)

Some screen shots of the project and the render analyzer:

A cup of coffee just rebooted my brain an i found the solution.
In the example that i modified the geomnode was attached to a render object:
and i can call flattenStrong with the cube object.

Welcome to the forums! :slight_smile:

Let me start off by explaining the fundamental differences between these classes. PandaNode is the base object for every single node in the scene. GeomNode is also a PandaNode. It inherits from PandaNode, and is a specific type of PandaNode; in this case, it represents a node that can hold renderable geometry in the form of Geom objects. But as it inherits from PandaNode, it’s still a PandaNode like every other node in the scene graph and can do everything that a PandaNode can.

The PandaNode class only implements basic fundamental scene graph operations, such as managing child and parent nodes, managing the render state of a node (colour, texture, etc), and so on. Features like flattening nodes together are implemented in separate classes, such as SceneGraphReducer. But applying many operation on nodes from a high level using the PandaNode interface is a bit clumsy.

Enter NodePath. This is a class that is very light in terms of memory storage, because all it holds is a pointer to a node, not an actual node itself. It also comes with a whole range of high-level operations that provide easy access to a whole range of low-level functionality relating to the node and low-level operations that can be applied to the node. One of those operations is flattenStrong(), which creates a SceneGraphReducer object, initialises it with the node that the NodePath points to, and invokes the unification process on the underlying nodes.

Now back to your problem. Your analysis of the problem was spot-on: even modern GPUs have trouble handling more than a couple hundred batches per frame. (Every geom that is partially or completely in view of the camera roughly represents one batch- you can view the exact information using PStats). This is not a limitation of the GPU or CPU, but of the bus that is used to transfer data to the graphics processor.

So, assuming that the cubes are rigid (not moving), flattening the cubes into groups of nodes is the obvious way to go. I say “groups of nodes” because you probably don’t want to put your scene entirely in the same Geom, as this will void the advantages of frustum culling (as geometry behind the camera would still be rendered). Therefore, you’ll want to group bunches of cubes that are close together and flatten them together (or create them as a single Geom in the first place, which may be a better idea if you start running into performance issues due to flattening thousands of geoms), until you have only a few hundred bunches left.

So, with all that in mind, the easiest way to flatten the nodes is simply to create a new NodePath object to encapsulate your node, and call flattenStrong. NodePath objects are disposable and cheap, you can create one to perform operations on a PandaNode at any time.

nPath = NodePath(node)

The alternative solution (in case you want to avoid using NodePath for some reason) is to invoke the lower-level SceneGraphReducer yourself. If you want to do this, then I advise looking at the source code of NodePath::flatten_strong, in panda/src/pgraph/nodePath.cxx of the Panda3D source code.

A big thanks for this very detailed and informative answer :slight_smile: .
I already thought that the GeomNode is a specific type of Panda node but how exactly do I use the functions of it.
Is it just: somenode.function or
if somenode is created as GeomNode?

Back to my main problem. I solved the framerate drop with flattenstrong and it really made a good job in boosting the performance. But the disadvantage you already mentioned comes along with it. It takes over 500 seconds to complete the flatten command.
I´m now trying to create the Landscape with larger batches from the beginning

I did a little performance test on flatten and got the following results:


Before flattening each geonode consisted of 2 tris.

That are´t really my expected results and I´m curios why the 30k Tri test is slower than the 110k Tri.
The conclusion I came up with after looking at the first 3 results was that the flattenstrong command tests every geomnode for adjacent geomnodes resulting in an exponential increase of calculation-time with increasing polygons.

The last few hours i tried to solve the Problem to create the Geom in one Bach, this is with what i came up:

#you cant normalize in-place so this is a helper function
def myNormalize(myVec):
	return myVec
#helper function to make a geom mesh given a Vertexlist (2 vertices in line resume in one rectangle, for example: Vertexlist = [[0,0,0],[0,1,1]]), adapted from panda3d example "Procedural-Cube" 
def create_chunk(vertexlist):

	vdata=GeomVertexData('square', format, Geom.UHDynamic)

	vertex=GeomVertexWriter(vdata, 'vertex')
	normal=GeomVertexWriter(vdata, 'normal')
	color=GeomVertexWriter(vdata, 'color')
	texcoord=GeomVertexWriter(vdata, 'texcoord')
	#loop through the vertex list and create 1 rectangle for every 2 vertices
	TriBuffer = []
	maxindex = len(vertexlist)
	for index in range(0,maxindex,2):
		pointa = vertexlist[index]
		pointb = vertexlist[index+1]
		#split the vertices to their coordinates for later usage
		x1 = pointa[0]
		y1 = pointa[1]
		z1 = pointa[2]

		x2 = pointb[0]
		y2 = pointb[1]
		z2 = pointb[2]
		#make sure we draw the sqaure 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(myNormalize(Vec3(2*x1-1, 2*y1-1, 2*z1-1)))
			normal.addData3f(myNormalize(Vec3(2*x2-1, 2*y1-1, 2*z1-1)))
			normal.addData3f(myNormalize(Vec3(2*x2-1, 2*y2-1, 2*z2-1)))
			normal.addData3f(myNormalize(Vec3(2*x1-1, 2*y2-1, 2*z2-1)))
			vertex.addData3f(x1, y1, z1)
			vertex.addData3f(x2, y2, z1)
			vertex.addData3f(x2, y2, z2)
			vertex.addData3f(x1, y1, z2)

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

		#adding different colors to the vertex for visibility

		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 arent directly supported by the Geom interface
		#you might be interested in the CardMaker class if you are
		#interested in rectangle though




	maxindex = len(TriBuffer)
	for index in range(maxindex):
		primitive = TriBuffer[index]

	#print(x1, y1, z1)
	#print(x2, y2, z1)
	#print(x2, y2, z2)
	#print(x1, y1, z2)
	return square	

Unfortunately all the time i try to run the program i get the following error message:

AssertionError: primitive->check_valid(cdata->_data.get_read_pointer()) at line
370 of c:\buildslave\release_sdk_win32\build\panda3d\panda\src\gobj\geom.cxx

I think that this has something to do with how python adds the GeomTriangles to my TriBuffer array. Probably python just stores a pointer to the GeomTriangle instead of a real copy.
I would really appreciate it if someone knows a solution.

flattenStrong() does a lot of work, and it’s not really related to the number of triangles. The number of Geoms is involved too, as well as the number of GeomTriangles objects, and the number of GeomNodes. I don’t know that I would expect it to be linear with all of these objects, either; it has to copy and recopy data to coalesce Geoms together, and the might be parts of it that tend to be O(N*N) or worse.

As to your error, that assertion generally indicates that your GeomTriangles object references index numbers that are not found in your GeomVertexData. So you should simply check your math. You can print out the GeomVertexData and/or the GeomTriangles to verify you’re getting what you think you are.

Also, note that you should not create a new GeomTriangles object for each triangle. Re-use the same one for all of your triangles. A GeomTriangles object can (and should) contain many triangles; each GeomTriangles object has to be sent as its own separate batch.


You´re right, I just looked over the math of my program and found the error that caused the assertion error.
I accidentally called a non existing vertex index.
(counting 1234 insted of 0123 :blush: )
It now works like a charm but i´ll have a look at the re-use of the GeomTriangles as soon as possible.

You can just keep adding vertices to the same GeomTriangles object. Every set of three vertices will represent a single triangle.

That sounds easy… and it really is :smiley:

I totally love how Panda3D manages the coding… It´s as easy as breathing.
I tried to create something like this in several different engines now but some hadn’t the capabilities to create new stuff run time (like Blenders Game engine) and others like python ogre are just unlearn-able for me.

The tutorials, examples, api reference and of course the community of Panda3D are nothing short of awesome.

This are some screenshots of the current result:
The geometry is generated in ~1.9 seconds and contains 100k+ tris but i will decrease the the batch-size and increase they´r number for better culling and manipulation purposes.