Simple Panda3D tutorial - terrain from height map

Download:

http://www.homepages.inf.ed.ac.uk/s0094060/terrain.tar.gz

Screenshot:

Author: Sean Hammond

Contact: s0094060@sms.ed.ac.uk

terrain.py, version 0.001, the first ever version.

I release this code to the public. You can do whatever you want with it.

I hope that others will take this simple and inefficient example and build on

it, improve it, fix my mistakes, etc.

If you do make improvements to this code, I’d appreciate if you would let me

know, and send me a copy of your new version.

This tutorial demonstrates how to construct a mesh from a height map and

apply a texture to it in Panda3D. Various other basic functions of Panda3D

are present also, such as using keyboard events to move the terrain around

and displaying a text overlay on the Panda3D rendering window. This code

does not do procedural terrain generation, it uses a set of height maps and

textures that are packed along with this code (five height maps and

corresponding textures are provided). However it should be easy to write

height map and texture generation algorithms and plug them into this code.

The height maps and textures

----------------------------

The provided height maps and textures were produced using terraform:

terraform.sourceforge.net/

All of the height maps are of size 100x100, resulting in a comfortable frame

rate on my (less than impressive) system, except terrain 1 which is 250x250

and slows down my system significantly.

All of the textures are of size 1000x1000 (actually the texture for terrain 2

is only 100x100).

The code should be able to handle height maps and textures of any

size. They shouldn’t even need to be square (although I have only tried it

with squares). However note that there are no efficiency considerations for

high-poly terrains in this code, and see Known issues below.

The main shortcoming of this demo so far as a terrain system, aside from its

overall lack of sophistication (it’s my first Panda3D program) is that the

terrain it generates doesn’t look great because it’s not that detailed. This

is due to the height maps and textures provided - if larger height maps and

much larger textures could be provided I think things would look much better.

However, see Known issues below.

Known issues

------------

This code can only handle height maps (and therefore terrain) up to a certain

size. The largest height map I have rendered successfully is 250x250 (terrain

1 provided). With a height map of 300x300 or above the code gives the

following error:

Assertion failed: *(PN_uint16 *)pointer == a at line 1073 of panda/src/gobj/geom VertexColumn.cxx

Traceback (most recent call last):

File “terrain.py”, line 72, in ?

triangles.addVertex(x+heightMap.getXSize()+1)

AssertionError: *(PN_uint16 *)pointer == a at line 1073 of panda/src/gobj/geomVe rtexColumn.cxx

I don’t know what the exact cut-off point at which this error occurs is, but

it’s somewhere between 250x250 and 300x300.

I think this error may be fixed by improving the code to split the terrain up

into multiple GeomTriangles objects. At the moment the entire terrain mesh is

created in a single GeomTriangles object regardless of its size. I don’t know

if doing that is good or bad. Just a guess.

In any case, a height map of size 250*250 results in (I think) a terrain mesh

of 125,000 triangles (each pixel in the height map corresponds to a single

square of the mesh, each square of the mesh is constructed as two triangles)

which is enough to slow Panda3D down too much on my system. It’s obvious that

to create larger or higher detail terrains some form of LOD (controlling the

detail level of terrain depending on how far from the camera it is) or other

efficiency improvement is needed.[/img]

Yay for height maps!

All right, so the reason why you can’t go passed 250x250 is a bit of a doozy (I may be way off on this too, but it seems right):

So if you use pdb.pm() at the crash, you’ll see it dying around vertex index 65535, which is in line with the assertion error (in this case, its asking if the newest index is indexArray[65536],
and since uint16(65536)=0x0000, its triggering the assertion error)

Okay, so the heart of this lies in the GeometryPrimitive’s cycleDataWriter. Its based off of CycleData, which is a generic object used by many panda classes to store data. Each class usually implements their own version, and the GeomPrimitive unfortunately uses uint16s to store the vertex indices (check
panda/src/gobj/geomPrimitive.i line 367. CDWriter and Reader are built off of CDATA).

I’m curious if this is a hardware limitation?
(by the way, geomPrimitive.cxx line 247 is the add_vertex function)

Anywho, there’s a funny solution that may be nigh impossible (other than
splitting into multiple geomPrimitives to avoid the 65k vertex limit):

So the GeomPrimitive only builds an index off of the vertex list if it perceives the need to. If your vertex indices in the primitive are strictly ordered (1,2,3,4,5, etc). then it keeps the primitive unindexed. As a result, it avoids using the cycle data at all, preventing the assertion. You can test this with the following:

myGeom=GeomTriangles(Geom.UHStatic)


for i in range(100000):
   myGeom.addVertex(3*i)
   myGeom.addVertex(3*i+1)
   myGeom.addVertex(3*i+2)
   myGeom.closePrimitive()

will make 1000000 polygons. You can kill the whole thing by following up with:


myGeom.addVertex(1)  #BEWAAAAAAARE

This will force indexing, and subsequently cause roughly 3000000 assertion errors to trigger. whee. So yeah, if you find a way to order the triangles to the same order as the verticess, you can do larger than 250x250 height maps.

Err, yeah, so the fact that GeomPrimitive doesn’t actually create the index until it’s needed allows you to “work around” this hardware limitation–in OpenGL, for certain drivers, in certain rendering modes. But for the most part, you’re not supposed to do that. You can’t really go beyond 16-bit indexes on current-generation hardware (although some OpenGL drivers will automatically remap this for you). DirectX simply doesn’t allow it.

Really, you’re just not supposed to stuff more than 65536 vertices into a single GeomPrimitive. Even though it works in some limited cases, it’s not optimal. One day this will be supported transparently, especially when there is hardware that can handle it.

The CycleData bit, by the way, is just the standard way that Panda stores data in its classes to implement pipelining–different versions of an object for the different stages of the graphics pipeline visible in different threads (not yet fully implemented). For the most part, you can treat things within a CycleData object as if they are things within the class itself.

David

Actually, you know what? I’m mistaken. DirectX does support more than 16-bit indexes.

So it might be safe to use this feature of Panda. But to do this, you have to explicitly tell your GeomPrimitive that you intend to use 32-bit indexes, like this:


primitive.setIndexType(GeomPrimitive.NTUint32)

Still, strictly speaking, you should not create a GeomPrimitive with more than base.win.getGsg().getMaxVerticesPerPrimitive() different vertices in it; and you should not create a GeomVertexArray with more than base.win.getGsg().getMaxVerticesPerArray() vertices in it. These limits are imposed by your hardware, and they may be different for different cards (or different drivers).

David

Great, thanks guys.

I actually started working on some improvements to the code, mainly for efficiency (in terms of framerate) which happen to include building the mesh out of multiple GeomPrimitive’s anyway. The mesh is so big (I’m now scaling it to 10 or 100 times the size it was in the currently released code, that’s scaling, not increasing the no. of triangles) that it can’t all fit on screen at once, so it is not the most efficient thing to have the whole mesh as one GeomPrimitive, even regardless of this index error.

So, it may be that the current development version I have can handle much larger height maps, I’ll try it sometime.