from EarthSculptor to Panda

hello, everyone. i’ve done a little job to render EarthSculptor terrain in panda. EarthSculptor is a nice terrain editor, you can also export terrain texture, but just a final combined texture, terrain doesn’t look good using only single texture. we can find the original textures and using multitexture to get the same appearence as in EarthSculptor. i implement this with GeoMipTerrain and cg shader.
Screen shots:


here i take EarthSculptor’s default map for example, the same way when you have made your own terrain.
At first let’s check “EarthSculptor\Maps\defaultMap” folder. it contains output files. there are 4 png images:
default.png - the heightmap of the map
default_l.png - the lightmap of the map
default_c.png - the colormap of the map
default_d.png - the alphamap of the map, each channel represents one of the four detail textures that make up the detail textures.
and a .map file:
default.map - this is an ascii file containing all the parameters, colors, texture names used in the map. open it with a texteditor we can find the deatail textures. this map use 4 detail textures, you can find them in “EarthSculptor\Textures” folder:
detailtexture1 “[HOME]Textures\grayRock.png”
detailtexture2 “[HOME]Textures\hardDirt.png”
detailtexture3 “[HOME]Textures\bigRockFace.png”
detailtexture4 “[HOME]Textures\shortGrass.png”
now we have all textures we need. i copied them to a “data” folder.

one problem i didn’t get through is the detail texture scale. we cann’t used the scale factor directly from file “default.map”, the terrain will look different with in EarthSculptor. i did some test it seems EarthSculptor’s scale is opposite to our normal way and is not linear, i couldn’t find a formular to convert a EarthSculptor’s scale to a normal scale. i had to count how many times each detail repeats to get the approximate scale factor. Waiting for someone to figure it out.

here is my script and shader. it’s plain simple. Big thanks to pro-rsoft for GeoMipTerrain and guiding me in texture and shader.

# rendering Earthsculptor terrain with panda
# Author:weihuan, Email:kutawei@yahoo.cn, 2008/06/29

import direct.directbase.DirectStart
from pandac.PandaModules import GeoMipTerrain
from pandac.PandaModules import TextureStage
from pandac.PandaModules import TextNode
from pandac.PandaModules import Texture
from pandac.PandaModules import Filename
from direct.showbase.DirectObject import DirectObject
from direct.gui.OnscreenText import OnscreenText
import sys

class World(DirectObject):

    def __init__(self):
        
        self.title = OnscreenText(text = "Rendering Earthsculptor terrain",
                         style = 1, fg = (1,1,1,1), pos = (.4,-.95), 
                         align = TextNode.ALeft,  scale = .08)
                                
        base.camLens.setFar(2000)
        base.setFrameRateMeter(True)
        base.setBackgroundColor(0.4, 0.5, 0.7)
        
        #terrain setting
        self.terrain = GeoMipTerrain("myTerrain")
        self.terrain.setHeightfield(Filename("data/default.png"))
        self.terrain.getRoot().setSz(400)
        self.terrain.setBlockSize(32)
        self.terrain.setFactor(400)
        self.terrain.setMinLevel(3)
        self.terrain.getRoot().reparentTo(render)
        self.terrain.getRoot().setPos(-256/2, -256/2, 0)
        self.terrain.generate()
        
        #load textures
        tex0 = loader.loadTexture("data/default_d.png")
        tex0.setMinfilter(Texture.FTLinearMipmapLinear)
        tex1 = loader.loadTexture("data/grayRock.png")
        tex1.setMinfilter(Texture.FTLinearMipmapLinear)
        tex2 = loader.loadTexture("data/hardDirt.png")
        tex2.setMinfilter(Texture.FTLinearMipmapLinear)
        tex3 = loader.loadTexture("data/bigRockFace.png")
        tex3.setMinfilter(Texture.FTLinearMipmapLinear)
        tex4 = loader.loadTexture("data/shortGrass.png")
        tex4.setMinfilter(Texture.FTLinearMipmapLinear)
        tex5 = loader.loadTexture("data/default_c.png")
        tex5.setMinfilter(Texture.FTLinearMipmapLinear)
        tex6 = loader.loadTexture("data/default_l.png")
        tex6.setMinfilter(Texture.FTLinearMipmapLinear)
        #set mutiltextures
        self.terrain.getRoot().setTexture( TextureStage('tex0'),tex0 ) 
        self.terrain.getRoot().setTexture( TextureStage('tex1'),tex1 )
        self.terrain.getRoot().setTexture( TextureStage('tex2'),tex2 )
        self.terrain.getRoot().setTexture( TextureStage('tex3'),tex3 )
        self.terrain.getRoot().setTexture( TextureStage('tex4'),tex4 )
        self.terrain.getRoot().setTexture( TextureStage('tex5'),tex5 )
        self.terrain.getRoot().setTexture( TextureStage('tex6'),tex6 )
        #load shader
        self.terrain.getRoot().setShader(loader.loadShader('terraintexture.sha'))
             
        self.accept('escape', sys.exit)      
        taskMgr.add(self.update,"updateTerrain")

    def update(self, task):
        self.terrain.setFocalPoint(base.camera.getX(),base.camera.getY())
        self.terrain.update()
        return task.cont

if __name__ == '__main__':
    myworld = World()
    run( )

terraintexture.sha

//Cg
//
//Cg profile arbvp1 arbfp1

void vshader( in float4 vtx_position  : POSITION,
              in float2 vtx_texcoord0 : TEXCOORD0,
              in uniform float4x4 mat_modelproj,
              
              out float4 l_position  : POSITION,
              out float2 l_texcoord0 : TEXCOORD0,
              out float2 l_detail1   : TEXCOORD1,
    					out float2 l_detail2   : TEXCOORD2,
    					out float2 l_detail3   : TEXCOORD3,
    					out float2 l_detail4   : TEXCOORD4 )
{ 
  	l_position = mul(mat_modelproj,vtx_position);
  	l_texcoord0 = vtx_texcoord0;
  	
    //detail texture coordinates scaled, we must get the correct scale fator to make terrain look like in EarthSculptor
  	l_detail1 = vtx_texcoord0 * 9.3;   //27.365000 in EarthSculptor
  	l_detail2 = vtx_texcoord0 * 12.8;  //20.000000 in EarthSculptor
  	l_detail2 = vtx_texcoord0 * 7.9;   //32.340000 in EarthSculptor
  	l_detail4 = vtx_texcoord0 * 11.5;  //22.389999 in EarthSculptor
} 

void fshader( in float4 l_position  : POSITION,
              in float2 l_texcoord0 : TEXCOORD0,
              in float2 l_detail1   : TEXCOORD1,
    					in float2 l_detail2   : TEXCOORD2,
    					in float2 l_detail3   : TEXCOORD3,
    					in float2 l_detail4   : TEXCOORD4,
    					
              in uniform sampler2D tex_0 : TEXUNIT0,
              in uniform sampler2D tex_1 : TEXUNIT1,
              in uniform sampler2D tex_2 : TEXUNIT2,
              in uniform sampler2D tex_3 : TEXUNIT3,
              in uniform sampler2D tex_4 : TEXUNIT4,
              in uniform sampler2D tex_5 : TEXUNIT5,
              in uniform sampler2D tex_6 : TEXUNIT6,

              out float4 o_color : COLOR ) 
{
    //add 4 detail colors
    float4 alpha = tex2D(tex_0, l_texcoord0.xy);
    o_color = tex2D(tex_1, l_detail1.xy) * alpha.x
            + tex2D(tex_2, l_detail2.xy) * alpha.y
            + tex2D(tex_3, l_detail3.xy) * alpha.z
            + tex2D(tex_4, l_detail4.xy) * alpha.w;
            
    //color map, there are 3 Color Modes in EarthSculptor
    o_color = o_color + tex2D(tex_5, l_texcoord0.xy) - 0.5;
    //o_color = o_color + tex2D(tex_5, l_texcoord0.xy);
    //o_color = o_color * tex2D(tex_5, l_texcoord0.xy);
    
    //light map 
    o_color = o_color * tex2D(tex_6, l_texcoord0.xy);
    
    o_color.a = 1.0;
}

definetly a very usefull thing, you should post it in the code-snippsets section. maybe some moderator can move the topic.

greetings
Thomas E.

PS: screenshots dont work for me.

sorry, i’ve changed the image URL. i post image 1st time and struggle to find a free host on line. if i post this in the wrong place, can someone move to it to where it should be. i can’t agree more.

Good job. I myself have full EarthSculptor support in my game too, already for a few months. (Including water, 8 detail texture support, skybox, automatic texscale calculation, etc.) ES is really a great tool, except that for large terrains you can better use something like L3DT.

Oh by the way, did you know Ernest Szoka has put EarthSculptor’s water shader on the ES forums? Its in ASM, you do need to port it to Cg, but that’s not that hard. :slight_smile:

pro-rsoft, you are the awesome one. i just a newbie, more should be learned. would you mind sharing your code fully supporting EarthSculptor? a lot people and me will cheer for that. if you can’t share, could you give some other guide for me, such as how can we get the texscale, do we need to parse the .map file to do automatic parameter setup etc.
thank you.

Uhm, I guess I could, but it would take some time. The stuff is scattered all over my game classes and uses a lot of my custom classes, so I’d need to rewrite a lot.
Whenever I find time, I may give it a try. I can help you with things, though.
The .map file is human-readable. It’s quite easy to parse it. I can provide you with the function I use to parse it, if you want.
It contains a variable which represents the terrain height – use this for setSz. For the texture scales, there’s a variable detailScales in the .map file with the four values.
To get the right texture scale, I think I had to divide the texture size by ES’ value. 256 / 27.365 is indeed 9.

For the water, here is the shader:
earthsculptor.com/forum/viewtopic.php?t=128
You’d need to translate it to Cg though. And I’m also not sure about copyrights.

Also note that this:

self.terrain.setFactor(400)

Such a large factor for a small terrain is almost the same as this:

self.terrain.setBruteforce(True)

The latter is faster. :slight_smile:

If there’s anything more I can help you with, I’d be happy to.

oh, i couldn’t even notice the relation between the scale figure and the map width. :frowning: thank you for correcting and informing me, pro-rsoft. yes i am glad to see a function to parse .map. i’ll try the water shader. At the same time waiting to your release. yours are better than mine certainly :smiley:

This is the function from my Map class:

from re import match
def isFloat(n):
  if match("^-?([0-9]*\\.)?[0-9]+$", n):
    return True
  return False
def isInt(n):
  if match("^-?[0-9]+$", n):
    return True
  return False

#...

class Map(object):

#...

  def loadES(self, file):
    """Imports a map from the EarthSculptor format"""
    self.reset()
    o = {}
    if(not isinstance(file,Filename)):
      file = Filename(file)
    if(not file.exists()):
      raise IOError, "Cannot find map file! ("+str(file)+")" 
    self.name = file.getBasenameWoExtension()
    f = open(file.toOsSpecific()) #python's open() command uses OS specific paths
    for l in f:
      l = l.strip()
      if(not l.startswith("//")):
        if(l.find(" ") == -1):
          o[l] = True
        else:
          ll = l.split(" ",1)
          if(ll[1].find(" ") == -1 or ll[1].find(""") != -1):
            t = ll[1].replace(""","")
            if(isInt(t)):
              t = int(t)
            elif(isFloat(t)):
              t = float(t)
          else:
            t = []
            for i in ll[1].split(" "):
              if(isInt(i)):
                t.append(int(i))
              elif(isFloat(i)):
                t.append(float(i))
              else:
                t.append(i)
          o[ll[0]] = t
    f.close()
    # Now, I access the things like:
    print o["mapHeight"]
    print [ o["tileTextureSize"] / float(s) for s in o["detailScales"]]

Also, you might have already noticed that the used colors are in some weird number format. I wrote some functions to convert to and from those color types:

def LongColorToRGB(es_color):
  hexstr = '%08x' % int(es_color)
  r, g, b = hexstr[2:4], hexstr[4:6], hexstr[6:]
  r, g, b = [int(n, 16)/255.0 for n in (r, g, b)]
  return (r, g, b)

def RGBToLongColor(rgb_tuple):
  r, g, b = [int(n*255.0) for n in rgb_tuple]
  colorstring = '%02x%02x%02x%02x' % (255,r,g,b)
  return int(colorstring)

Good luck. :slight_smile:

Well, I doubt that. My earthsculptor code is messy and it is scattered all over my game. :slight_smile:

thanks a million :slight_smile:

Just amazing…

A great tutorial for me and an excellent solution for implementing multitextured terrains, plus learning how to read data form a config file in a beautifull pythonic way.

It works perfect, thankyou very much for sharing.
c