Another procedural terrain question

Okay, so I’m working on a better version of the code I posted for creating a mesh from a heightmap, I need some code that I can seriously use to create a third-person game for my research.

I have a question that relates to Panda’s rendering internals.

What I thought was a major issue in the posted code was that it simply translated the entire height map, regardless of its size, to one big mesh. All of the vertices were packed into a single GeomVertexData, all the rendering primitives belonged to a single GeomTriangles, and there was a single Geom and a single GeomNode and a single NodePath for the whole thing.

In the posted code the entire mesh is displayed on-screen at once. In the new code, I want to scale it up to be a large section of terrain which an Actor could walk around on, so it will not all be on-screen at one time.

So it seems obvious that at some point the data from the heightmap must be split up into different ‘cells’, but my question is, at what point in the mesh creation should this happen?

Will it be most efficient to split it up first thing, so that multiple GeomVertexData’s and corresponding multiple everything else are created? Or should I split it up later, creating just one GeomVertexData but multiple GeomPrimitive’s?

I guess there must be some size limit on a GeomVertexData, but should I go right up to this limit or have many much smaller GeomVertexData representing ‘cells’ of the terrain?

This page (http://panda3d.org/manual/index.php/Creating_Geometry_from_Scratch) in the manual certainly suggests that I would want multiple GeomPrimitive’s representing ‘cells’ of the terrain for efficient culling (exactly how big each cell should be is a different question), because I don’t want parts of the terrain that are far away and not on-screen to be rendered, but it doesn’t clarify whether they should all share one GeomVertexData or should have one each.

Aside from the GeomVertexData question, I think what the manual suggests is that every cell of the terrain should have its own GeomPrimitive and (for fast culling) its own GeomNode (and therefore its own Geom and NodePath as well), which seems to suggest that culling in Panda3D occurs at the GeomNode level. After that, all the NodePaths could be attached to a single dummy node allowing them to be manipulated at once.

The short answer is, Panda does culling at the Geom level, so you should split your terrain into multiple different Geoms. Splitting into multiple different GeomPrimitives within the same Geom doesn’t help culling, and only increases your overall render cost.

You could go further, and have multiple different Geoms share the same GeomVertexData, but it’s not clear if there’s much benefit to that. There might be a slight benefit if several Geoms that share the same GeomVertexData happen to be onscreen at the same time.

The tricky part is deciding how many triangles to put in each Geom. There is a balance point between splitting your geometry up into lots of tiny pieces for efficient culling, and collecting it together into as few, large pieces as possible for optimal rendering.

Here’s how it works: Panda sends each GeomPrimitives object to the card in a single call. Because of the limitations on the PC bus, you can only issue so many calls to the graphics card in a frame, and this limit is the same no matter how sweet your graphics card is. So you want to minimize the number of individual GeomPrimitives (and, therefore, the Geoms) in the scene.

If you have a very high-end card, the cost per-vertex is relatively low, and you can afford to send lots of vertices to the card even if they happen to be offscreen, because it can quickly process them all and reject them. So with a high-end card, you don’t mind if the Geoms are very large, because the cost of having many GeomPrimitives far outweighs the cost of rendering offscreen vertices.

On the other hand, if you have a low-end card, the cost per-vertex cost is relatively high, and it is very important not to waste time trying to render a lot of vertices that are offscreen. So with a low-end card, you want the Geoms to be small, because the cost of rendering offscreen vertices outweighs the cost of having many GeomPrimitives.

So the right size depends on the quality of the hardware you are running. It’s impossible to find a balance point that suits everyone. Most people just choose a particular target hardware, and then try experimentally to find the right size piece that suits that hardware.

David

Oh, let me follow up on this: you could, in fact, put all of your Geoms into a single GeomNode. But then Panda would have to test the bounding volume of each and every Geom, every frame. You could help optimizing culling a bit by building it out into a hierarchy, collecting neighboring Geoms together.

It would look something like this: collect a 4x4 grid of neighboring Geoms into a single GeomNode. Then collect a 4x4 grid of neighboring GeomNodes under a single PandaNode. Then collect a 4x4 grid of those neighboring PandaNodes under another, higher PandaNode. And so on until you have just one PandaNode that covers the whole graph.

That way, Panda can test the bounding volume hierarchically, and quickly reject large numbers of Geoms by testing a single bounding volume.

David

Hi David,

question about your hierarchy idea for optimised culling:

Is there any particular reason why you specified that Geom’s should first be grouped into GeomNode’s, then GeomNode’s into PandaNode’s, then PandaNode’s into more PandaNode’s as necessary?

I ask because, when combining this optimisation with LOD, I have found that you cannot attach a Geom to a LODNode, you must attach a PandaNode (or subclass).

So if I have say 3 Geom’s for every patch, representing 3 levels of detail, then I need to have 3 GeomNode’s for each patch, one for each Geom, and then I collect the 3 GeomNode’s for a Patch into one LODNode.

At this point I cannot build your pyramid exactly as you have specified, now that the patches are LODNode’s I cannot group them into GeomNode’s, but I can group them into PandaNode’s, then group those PandaNode’s into PandaNode’s and so on, so that the pyramidal part of the scene graph hierarchy has the same shape as you suggested but is made entirely of PandaNode’s, instead of being GeomNode’s at the base level and PandaNode’s at higher levels.

However if I do this it seems that every patch switches LOD at the same time, each patch is not switching based on the camera’s distance from that patch but (I believe) based on the camera’s distance from the single NodePath that encompasses the entire terrain at the top of the scene graph hierarchy.

So now what I think I need to do is give every patch it’s own NodePath, that is attach the LODNode for each patch to a unique NodePath, so that every patch has its own position. Then I would need to build up the scene graph pyramid, I think, with more NodePath’s instead of with PandaNode’s (because I don’t think I can attach a NodePath to a PandaNode as a child).

First, remember that a NodePath is really a handle to a PandaNode. For most purposes, you can think of a NodePath and a PandaNode as interchangeable–you can go back and forth. If you have a NodePath, you can get its PandaNode via nodePath.node(); if you have a PandaNode, you can make a NodePath for it via NodePath(pandaNode).

What you’re trying to do is establish the center of each LODNode. By default, the LODNode centers on (0, 0, 0). Since you don’t have a local transform on each LODNode, they all exist in the same transform space, and therefore they all have the same center.

It is true that you could solve this by putting each LODNode into its own transform space, generating all of the vertices around (0, 0, 0) and then using nodePath.setPos() to move the LODNode into place. But that would be silly, because you’d be introducing a lot of unnecessary transforms into the scene graph. The better solution is simply to do:


lodNode.setCenter(Point3(x, y, z))

for each LODNode.

Oh, and I wouldn’t worry about grouping four neighboring Geoms into one GeomNode. I just suggested that because it’s the obvious lowest level of grouping. But if you want to have an LODNode for each Geom, it’s OK to put one Geom per GeomNode.

David