HeightFieldTesselator Example

Yes, this uses some classes that are not included here but you should still be able to see how to use HeightFieldTesselator by perusing this code. Just know that SceneNavigator is my own class that moves the camera around in the scene FPS style. At any time you can still get the camera position and orientation using base.camera.getPos() and base.camera.getHpr().

This example renders a texture mapped, lit terrain model. Instead of re-tesselating each frame, I re-tesselate every ‘self.mTerrainUpdateInterval’ seconds.

Next update will include multitexturing.

Enjoy!

# Standard imports

import sys
import math

# Panda imports

from direct.gui.OnscreenText import OnscreenText 
from direct.showbase import DirectObject
from direct.showbase.DirectObject import DirectObject
import direct.directbase.DirectStart
from direct.task import Task
from pandac.PandaModules import *
from pandac.PandaModules import HeightfieldTesselator

# PHL imports

from PHL.Core.Properties import *
from PHL.P3D.Utilities import CreateTextLabel
from PHL.P3D.Shapes import P3DCreateGridXY,P3DCreateGridYZ,P3DCreateGridXZ
from PHL.P3D.TerraViz02.SceneNavigator import SceneNavigator
from PHL.Math.Bounds import BoundingVolume
from PHL.Math.Utilities import *

# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# 

class World(DirectObject):
	def getBoundingVolume(self):
		return BoundingVolume(\
				0,0,0,\
				self.mTerrainWidth,\
				self.mTerrainWidth,\
				self.mTerrainHeight
			)

	def postStatusMessage(self,msg):
		self.mTextDisplay2.setText(msg)

	def toggleWireframe(self):
		base.toggleWireframe()

	def toggleTexture(self):
		base.toggleTexture()

	def joystick(self):
		return None

	def environmentModel(self):
		return self

	def __init__(self):

		text = 'Panda3D HeightfieldTesselator Class Test Harness'
		color = (1,1,0,1)
		self.mTextDisplay1 = CreateTextLabel(text,color,1)
		self.mTextDisplay2 = CreateTextLabel(' ',(0,1,0,1),2)

		base.setBackgroundColor(0,0.1,0.6,1)

		# Setup world size
		self.mTerrainWidth = 5000
		self.mTerrainHeight = 100

		# Setup navigator
		base.camLens.setFar(10000.0)
		base.camLens.setFov(75, 95)
		self.__Nav = SceneNavigator(self)
		self.__Nav.setPitchGain(0.4)
		self.__Nav.setYawGain(0.4)
		self.__Nav.setRollGain(0.2)
		self.__Nav.setMotionGain(self.mTerrainWidth*0.0005)

		# Setup lighting
		lightAttribute = LightAttrib.makeAllOff()
		dirLight = DirectionalLight('DirLight')
		dirLight.setColor(Vec4(0.6,0.6,0.6,1.0))
		dirLightNP = render.attachNewNode(dirLight.upcastToPandaNode()) # crashes without upcast
		dirLightNP.setPos(Vec3(0.0,-10.0,10.0))
		dirLightNP.setHpr(Vec3(0.0,-26.0,0.0))
		lightAttribute = lightAttribute.addLight(dirLight) # add to attribute
		ambientLight = AmbientLight('ambientLight')
		ambientLight.setColor(Vec4(0.25,0.25,0.25,1.0))
		ambientLightNP = render.attachNewNode(ambientLight.upcastToPandaNode())
		lightAttribute = lightAttribute.addLight(ambientLight)
		render.node().setAttrib(lightAttribute)

		# Prep terrain textures
		coverTextureFile = "Dirt/Ground1.png"
		self.mCoverTexture = loader.loadTexture(coverTextureFile)
		Assert(\
			self.mCoverTexture != None,\
			"Failed loading terrain cover texture "+coverTextureFile)

		# Setup heightfield
		self.mHeightFieldTesselator = HeightfieldTesselator("Heightfield")
		fName = "HeightField.png"
		fileObj = Filename(fName)
		self.mHorizontalScale = self.mTerrainWidth/2048.0
		self.mVerticalScale = self.mTerrainHeight
		self.mTerrainUpdateInterval = 0.1
		self.mTerrainUScale = 0.001
		self.mTerrainVScale = self.mTerrainUScale
		self.mHeightFieldTesselator.setHeightfield(fileObj)
		self.mHeightFieldTesselator.setVerticalScale(self.mVerticalScale)
		self.mHeightFieldTesselator.setHorizontalScale(self.mHorizontalScale)
		self.mLastTesselateTime = -1
		self.mHeightFieldNode = None
		self.updateHeightField()

		# Setup keyboard events
		self.setupKeyBindings()

		# Done, setup a periodic update task
		taskMgr.add(self.timeUpdate,'TimeUpdate')

	# Re-tesselate the heightfield
	def updateHeightField(self):

		if self.mHeightFieldNode != None:
			self.mHeightFieldNode.removeNode()
		self.mHeightFieldNode = self.mHeightFieldTesselator.generate()

		self.mHeightFieldNode.setTexGen(\
			TextureStage.getDefault(),
			TexGenAttrib.MWorldPosition
		)
		self.mHeightFieldNode.setTexture(\
			TextureStage.getDefault(),
			self.mCoverTexture,
			1
		)
		self.mTerrainUScale = 0.001
		self.mTerrainVScale = 0.001
		self.mHeightFieldNode.setTexScale(\
			TextureStage.getDefault(),
			self.mTerrainUScale,
			self.mTerrainVScale
		); 

		self.mHeightFieldNode.reparentTo(render)

	# Periodic update
	def timeUpdate(self,task):
		if (self.mLastTesselateTime == -1) or \
				(task.time-self.mLastTesselateTime>self.mTerrainUpdateInterval):
			self.mLastTesselateTime = task.time
			cPos = base.camera.getPos()
			ix = int(round(cPos[0]/self.mHorizontalScale)) 
			iy = int(round(-cPos[1]/self.mHorizontalScale))
			print task.time,ix,iy
			self.mHeightFieldTesselator.setFocalPoint(ix,iy)
			self.updateHeightField()

		self.__Nav.tickUpdate()
		return Task.cont

	# Take a snapshot
	def snapShot(self):
		base.screenshot('Snap')

	# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
	#
	# Method name : setupKeyBindings
	#
	# Description:
	#
	#	Load and register key bindings
	#
	# Input(s):
	#
	#	None
	#
	# Output(s):
	#
	#	None
	#
	def setupKeyBindings(self) :
		self.accept('h',self.__Nav.orientGodMode)
		self.accept('p',self.snapShot)
		self.accept('w',self.toggleWireframe)
		self.accept('t',self.toggleTexture)
		self.accept('m',self.__Nav.toggleAutoCentering)
		self.accept('escape',sys.exit)
		self.accept('.',self.__Nav.toggleDriving)
		self.accept('n',self.__Nav.nextNavigationMode)
		self.accept('+',self.__Nav.speedUp)
		self.accept('-',self.__Nav.slowDown)
		self.accept('home',self.__Nav.reset)
		self.accept('space',self.__Nav.turboBoostOn)
		self.accept('space-up',self.__Nav.turboBoostOff)
		self.accept('f11',self.__Nav.toggleSuperTurboBoost)

		self.accept('d',self.__Nav.drivingForwardOn)
		self.accept('d-up',self.__Nav.drivingForwardOff)
		self.accept('e',self.__Nav.slidingUpOn)
		self.accept('e-up',self.__Nav.slidingUpOff)
		self.accept('v',self.__Nav.slidingDownOn)
		self.accept('v-up',self.__Nav.slidingDownOff)
		self.accept('f',self.__Nav.slidingRightOn)
		self.accept('f-up',self.__Nav.slidingRightOff)
		self.accept('s',self.__Nav.slidingLeftOn)
		self.accept('s-up',self.__Nav.slidingLeftOff)
		self.accept('c',self.__Nav.drivingBackwardOn)
		self.accept('c-up',self.__Nav.drivingBackwardOff)


# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# 

world = World()

# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Run the program

run()

Another way to update the terrain could be the following demo.

It uses a python generator construct, which checks every fifth frame if the camera position is more than threshhold away from the current focal point, and only if it is then it updates the focal points, and in the next frame generates a new mesh. This way re-generating the terrain is done only when needed.

The demo runs without additional code, but a greyscale heightfield (in my case 256x256 pixel) and a texture is required.

I hope this thread will go on for some time, and additional code is added. Perhaps someone knows how to write a terrain shader instead of a simple texture, which blends several tileable textures (e.g. grass, mud, gravel, rock) depending on an alpha-map or terrain normals.

import direct.directbase.DirectStart

from direct.showbase.DirectObject import DirectObject
from pandac.PandaModules import HeightfieldTesselator
from pandac.PandaModules import Filename
from pandac.PandaModules import TextureStage
from pandac.PandaModules import TexGenAttrib
from direct.task.Task import Task

import sys


def terrainGenerator( ):
    filename = Filename( 'models/elevation.png' ) # 256 x 256 pixel
    tex = loader.loadTexture( 'models/grass.png' )

    x0 = -128        # Origin of terrain mesh, panda units
    y0 = 128         # Origin of terrain mesh, panda units

    fx = -999        # Focal point, pixel units
    fy = -999        # Focal point, pixel units
    threshhold = 8   # Threshhold for moving the focal point, pixel units
    hscale = 1.0
    vscale = 20.0

    # Create the tesselator
    tesselator = HeightfieldTesselator( 'Terrain' )
    tesselator.setHeightfield( filename )
    tesselator.setHorizontalScale( hscale )
    tesselator.setVerticalScale( vscale )
    tesselator.setFocalPoint( fx, fy )

    # Dummy node for first loop
    node = tesselator.generate( )

    while 1:
        x = int( ( x0 - camera.getX( ) ) / -hscale )
        y = int( ( y0 - camera.getY( ) ) /  hscale )
        if abs( x - fx ) > threshhold or abs( y - fy ) > threshhold:
            fx = x
            fy = y
            tesselator.setFocalPoint( fx, fy )
            yield( )

            node.removeNode( )

            node = tesselator.generate( )
            node.setTexGen( TextureStage.getDefault( ), TexGenAttrib.MWorldPosition )
            node.setTexScale( TextureStage.getDefault( ), 1.0, 1.0 )
            node.setTexture( tex )
            node.reparentTo( render )
            node.setPos( x0, y0, -50 )
            yield( )

        yield( )
        yield( )
        yield( )
        yield( )


class World( DirectObject ):

    def __init__( self ):

        # Input
        self.accept( 'escape', self.exit )
        self.accept( '1', self.toggleWireframe )
        self.accept( '2', self.toggleTexture )
        self.accept( '3', self.screenshot )

        # Base setup
        base.setBackgroundColor( 0, 0, 0 )

        # Terrain
        self.terrain = terrainGenerator( )
        self.terrain.next( )

        # Tasks
        taskMgr.add( self.move, 'moveTask' )

    def move( self, task ):
        self.terrain.next( )
        return Task.cont

    def exit( self ):
        sys.exit( )

    def toggleWireframe( self ):
        base.toggleWireframe( )

    def toggleTexture( self ):
        base.toggleTexture( )

    def screenshot( self ):
        base.screenshot( 'screenshot' )


w = World( )
run( )

Aersome, yeah let’s keep this thread rolling.

Any ideas to prevent popping?

most elegant way would be to use a vertex shader for it. fewest cpu usage. but i’m not shure if it works if the vertex have different horizontal resolutions (well it might but i’m pretty shure it requires quite some work).
except of updating and resolution you dont have much choices (at least if i understood how this algorithm works).
well other algorithm may have fewer screen-space errors but this would require to change the terrain-algorithm.

I tried both code snipplets but each time panda gives me the same message which is Terrain mech is 0 triangles. What should I do?

@Neophoenix

Hello,

one thing you could do is check that the path to your grayscale heightfield image is valid. I have checked, there is no error message if the file can not be found. Unlike when loading a mesh you can’t leave away the file extension.

There is no heightfield image included in the distribution, so you have to create your own.

Hope this helps,
enn0x

ah for the preventing the popping stuff and terrain in general. i’d like to point once more to the ranger mkII (plz david dont cut off my head)
http://web.interware.hu/bandi/ranger.html
and dont be deceived by the nice bumpmap. just switch over to wireframe mode and you’ll see the truth. but even there is still room for improvement, first one: write it as vertex shader so the cpu wont be loaded with it. and second. a second vertex shader (or include it all into one) to smooth the vertex-movement.

the whole algorithm seems to be not all too different from the current one in panda (except that it is a real circle) and i think some more stuff.

just wanted to remind the people to not foreget about this one =)

anyway. a small vertex shader which keeps the “old” and the “updated” terrain-mesh in memory and smoothes the new-placed vertices along the zaxis to their new position (the movement should only be done when the camera is moveing) it should be able to prevent popping quite nice while keeping the current algorithm=) so any shaderspecialists volunteering?

@ThomasEgi

From what I see Ranger Mk2 is based on the SOAR algorithm, which is indeed a great way to render huge terrain with great detail. With SOAR there is no need to care for “popping”, since SOAR creates a new mesh from scratch every frame (for example see http://web.interware.hu/bandi/ranger.html, the section about technology).

SOAR is quite different from the current Panda3d approach. These two screenshots show why:

http://www-static.cc.gatech.edu/~lindstro/papers/visualization2001a/fig11b.jpg
http://www-static.cc.gatech.edu/~lindstro/papers/visualization2001a/fig11c.jpg
(both from “Visualization of Large Terrains Made Easy” by Peter Lindstrom, Valerio Pascucci)

First, SOAR has high detail inside a clipping area, and not around a (x,y) position, and second, SOAR increases detail where the terrain is “uneven”, and uses few polygons where the terrain is “flat”. In short: Given a camera position and orientation SOAR optimizes the terrain mesh for polygon count, and in a very effective way.

The terrain tesselator that comes with Panda3d is much simpler (sorry, no offence meant). High detail when close to the focal point, and low detail when far way, even is the terrain is only a plane.

There are numerous ways to do terrain, and each way has it’s own advantages and disadvantages. SOAR is in my opinion good for rendering large terrain from bird’s eye view, for example when creating a flight simulator. When running around FPS style one usually doesn’t cares about what is behind the next hill, since this part of the terrain it is not visible.

For now I am happy that Panda3d comes with a builtin way to do terrain, even though it may not be suited for every kind of application. For those who really need SOAR have a look at Delta3d (delta3d.org/), an open source game engine backed by the US armed forces, duh. It aims at simulations software mainly, has python bindings, but is Windows.

For the last game that I was a developer on, I actually wrote a terrain system that creates a new mesh every frame. It prevented popping by gradually morphing new detail in. That looked okay, but a little weird — in some ways, it’s better for the terrain to pop than for it to look like it’s moving.

However, I bet I can help you reduce the appearance of popping. If you’re using vertex lighting to light the terrain, then every time the terrain changes, the lighting changes too. That magnifies the appearance of popping. My suggestion would be to experiment with either a fixed lightmap, or a shader that computes per-pixel lighting based not on the current shape of the terrain, but rather, on the “true” shape of the terrain as defined by the original raw heightfield.

shure they work different. but if you you look at the demo and at the current panda terrain. it is very simmilar. especialy if you look from the top down onto the terrain. so my guess is that either the demo is not really what was described in the paper or just looks like it isn’t.
because popping even occurs in the ranger demo =)

either way. a vertex shader should be able to prevent popping in the current panda approach.

enn0x – I tried your code with a 256256 greyscale image for heightfield and a corresponding 256256 colour image for texture. Problem is that the texture is not drawn once but many times over the terrain model. Is this what you intended, use of a small repeating texture?

A problem is that if you repeat a small texture that many times over a big model it looks all psychedelic when the whole model is viewed from far away. What you really need is a big texture that repeats just once over the entire model, and a smaller detail texture that repeats many times but is only shown on parts of the terrain that are close to the camera. This is what I was doing with the terrain code I was writing some time ago. Auto-generating or varying either of these textures based on properties of the model would be good also.

A good texturing example would be much appreciated! pleopard maybe? Or the Panda dev that added this heightfield code?

It’s been some time since I did this example, and to be honest I didn’t pay much attention to texturing here, since the goal was only to show how HeightfieldTesselator could be used.

But you are right, applying the same small grass tile again and again over a large terrain doesn’t look good.

Using a colormap and a detailmap is certainly one way to do a good looking terrain texture, and a cheap one, since not much memory is required for textures. Since I am a fan of RPG’s I would prefer to use splattering techniques. That is, have a few base textures tiles, e.g. grass, dirt, cobbles, sand, rock. And then blend them into each other with alpha-maps of the same resolution as the heightfield.

I would also appreciate examples of how to do state-of-art terrain texturing & lighting with Panda3D.

enn0x

enn0x, if you don’t mind, i made a big update to your code.

You can access it [color=red]here
How to use it:

self.myTerrain=terrainGenerator("heightmap.png",loader.loadTexture("texture.png"))
taskMgr.add(self.myTerrain.next,"terrainUpdate")