HeightFieldTesselator Example

Return to Code Snippets

HeightFieldTesselator Example

Postby pleopard » Thu Nov 09, 2006 5:59 pm

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!

Code: Select all
# 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()
User avatar
pleopard
 
Posts: 202
Joined: Tue Apr 25, 2006 1:33 pm

Postby enn0x » Fri Nov 10, 2006 11:15 am

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.

Code: Select all
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( )
enn0x
 
Posts: 1278
Joined: Wed Nov 08, 2006 1:39 am
Location: Germany, Munich

Postby pleopard » Sat Nov 11, 2006 4:23 pm

Aersome, yeah let's keep this thread rolling.
User avatar
pleopard
 
Posts: 202
Joined: Tue Apr 25, 2006 1:33 pm

Postby Cyan » Sat Nov 11, 2006 4:35 pm

Any ideas to prevent popping?
User avatar
Cyan
 
Posts: 215
Joined: Fri Jul 14, 2006 12:26 pm
Location: Utah

Postby ThomasEgi » Sat Nov 11, 2006 6:15 pm

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.
User avatar
ThomasEgi
 
Posts: 2147
Joined: Fri Jul 28, 2006 10:43 am
Location: Germany,Koblenz

Need Help

Postby Neophoenix » Sun Nov 12, 2006 7:21 am

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
 
Posts: 1
Joined: Sun Nov 12, 2006 7:17 am

Postby enn0x » Sun Nov 12, 2006 3:37 pm

@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
enn0x
 
Posts: 1278
Joined: Wed Nov 08, 2006 1:39 am
Location: Germany, Munich

Postby ThomasEgi » Sun Nov 12, 2006 6:44 pm

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?
User avatar
ThomasEgi
 
Posts: 2147
Joined: Fri Jul 28, 2006 10:43 am
Location: Germany,Koblenz

Postby enn0x » Mon Nov 13, 2006 6:24 am

@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 (http://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.
enn0x
 
Posts: 1278
Joined: Wed Nov 08, 2006 1:39 am
Location: Germany, Munich

Postby Josh Yelon » Mon Nov 13, 2006 12:01 pm

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.
Josh Yelon, Teacher, Carnegie Mellon Entertainment Technology Center
Josh Yelon
 
Posts: 1360
Joined: Wed Mar 30, 2005 8:30 pm

Postby ThomasEgi » Mon Nov 13, 2006 12:23 pm

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.
User avatar
ThomasEgi
 
Posts: 2147
Joined: Fri Jul 28, 2006 10:43 am
Location: Germany,Koblenz

Postby chombee » Mon Feb 12, 2007 8:28 am

enn0x -- I tried your code with a 256*256 greyscale image for heightfield and a corresponding 256*256 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?
chombee
 
Posts: 244
Joined: Fri Jan 13, 2006 10:09 am

Postby enn0x » Mon Feb 12, 2007 10:22 am

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
 
Posts: 1278
Joined: Wed Nov 08, 2006 1:39 am
Location: Germany, Munich

Postby rdb » Tue May 01, 2007 7:22 am

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

You can access it here
How to use it:
Code: Select all
self.myTerrain=terrainGenerator("heightmap.png",loader.loadTexture("texture.png"))
taskMgr.add(self.myTerrain.next,"terrainUpdate")
rdb
 
Posts: 8637
Joined: Mon Dec 04, 2006 5:58 am
Location: Netherlands


Return to Code Snippets

Who is online

Users browsing this forum: No registered users and 1 guest