Hello
Finally i finished the first version of
SNAPTOTHETERRAIN
Subdivided Terrain Multitexture Random Retro Generator v.1.0
dependencies: PIL library
good for newbies, so am i
here is the complete source with media included
http://www.handeozerden.com/snaptothegrid/panda/snaptotheterrain_v01.zip
these are some screenshots:
here is the code, with explanations of the project inside:
main.py
"""
SNAPTOTHETERRAIN
Subdivided Terrain Multitexture Random Generator v.1.0 by SnapToTheGrid
--------------------------------------------------------------------------------
Main:
Initializes application, general setup, key events
Files:
Contains several file management usefull functions (only custom_listdir for the moment)
This function is used for getting a cool list of random tile textures
DiamondMap:
Generates a random procedural heighmap of given size(**2+1) usign Diamond Square algorythm and saves it to disk
If wanted, splits the heightmap in x*y subimages and saves them to disk
strange: if we use jpg, when we join the 3d terrains, elevations in the borders dont fit,using bmp is ok , that took me crazy!
Terrain:
Based on treeterrain script, adapted to my necessities (thanks Treeform!)
options:
-Retro True/False:
Divides the elevations by int or float, giving a general aspect retro flatted or normal
-subdivide=None:
Generates a multitexture terrain with fixed resolution from a given heightmap image
-subdivide=N:
Generates x*y multitexture terrains with fixed resolution from each subheightmap and locates them in x*y grid
Improves performance a bit, i think the nodepaths outside the camera's view are not computed in the render :)
--------------------------------------------------------------------------------
Todo:
- improve performance in some way
- find a way to not having to locate the texture layers 0.02 units over, with some lights don't looks nice
- maybe change the system and generate a single colormap texture using PIL functions (?)
That WILL improve performance and i could use geoMipTerrain too, mhmmmm :)
- collision function for getting kind of getElevation() of the terrain in a given x,y point -> ideas to make it work fast(?)
- avatar+camera+a* pathfinding algorythm
--------------------------------------------------------------------------------
"""
#import panda modules
import direct.directbase.DirectStart
from direct.showbase.DirectObject import DirectObject
#import python modules
import sys
#import custom modules
import DiamondMap,Terrain
#define constants
TILEPATH="media/tiles/"
HPATH="media/hmaps/"
HFILENAME="hmap"
CFILENAME="tile128"
SIZE=65 # has to be a 2 exponent number plus one
ALT=1.0
RETRO=True
SUBDIVIDE=3
class Main(DirectObject):
#===========================================================================
"""initialize"""
def __init__(self):
print "____________________________________________________"
print "Class Main"
#setup application
#-----------------------
base.setFrameRateMeter(True)
self.toggleWireFrame()
self.keys()
# Initialize application
#-----------------------
#create a random heighmap jpg file of given size and split it into given subdivisions
map=DiamondMap.DiamondMap(path=HPATH,filename=HFILENAME,size=SIZE,subdivide=SUBDIVIDE)
#create multitextured 3d terrain from heighmap data
terrain=Terrain.Terrain(heightmap=HPATH+HFILENAME,
colormap=TILEPATH+CFILENAME,
tilepath=TILEPATH,
alt=ALT,
retro=RETRO,
subdivide=SUBDIVIDE)
#===========================================================================
"""key functions"""
def keys(self):
print "Keys"
self.accept('f',self.toggleWireFrame)
self.accept('t',self.toggleTexture)
self.accept('p',self.snapShot)
self.accept('escape',sys.exit)
def snapShot(self):
base.screenshot("Snapshot")
def toggleWireFrame(self):
base.toggleWireframe()
def toggleTexture(self):
base.toggleTexture()
main=Main()
run()
DiamondMap.py
"""
DiamondMap:
Based on a (i think) Kris Schnee schrieb script that i found in the net, originally implemented in pygame, thanks!
Generates a random procedural heighmap of given size(**2+1) usign Diamond Square algorythm and saves it to disk
If wanted, splits the heightmap in x*y subimages and saves them to disk
"""
#import python modules
import Image,ImageDraw
import random
class DiamondMap:
def __init__(self,path,filename,size,subdivide=None):
print "____________________________________________________"
print "Class DiamondMap"
heightmap=self.initMap(size,False)
heightmap =self.randomMap(heightmap,False,(0.0,0.0,0.0,0.0))
im=self.drawMap(heightmap)
im.save(path+filename+".bmp","BMP")
print filename+".bmp saved in "+path, im.size
if subdivide!=None:
self.cropImage(path,filename,subdivide)
#===========================================================================
def initMap(self,size,random_data=False):
"""Generate random static or a blank map."""
heightmap = []
if random_data:
#random noise background
for x in range(size):
heightmap.append([])
for y in range(size):
heightmap[-1].append( random.random() )
else:
#black background
for x in range(size):
heightmap.append([])
for y in range(size):
heightmap[-1].append(0.0)
return heightmap
#===========================================================================
def randomMap(self,heightmap,show_in_progress=False,seed_values=(.5,.5,.5,.5)):
"""Make procedural terrain using the D/S Algorithm."""
size = len(heightmap)
## Seed the corners.
corners = ((0,0),(size-1,0),(0,size-1),(size-1,size-1))
for n in range(4):
point = corners[n]
heightmap[point[0]][point[1]] = seed_values[n]
## Starting values.
size_minus_1 = size - 1 ## For edge-wrapping purposes.
cell_size = size-1 ## Examine squares of this size within the heightmap.
cell_size_half = cell_size / 2
iteration = 0 ## How many times have we done the algorithm? (just FYI)
chaos = 1.0 ## Total possible variation of a height from expected average.
chaos_half = chaos * .5 ## Possible variation up or down.
diamond_chaos_half = (chaos/1.414)*.5 ## Reduced by sqrt(2) for D step.
## (Wouldn't "Diamond Chaos Half" be a cool anime series title?)
while cell_size > 1: ## For actual use.
## Begin the algorithm.
#print "Iteration: "+str(iteration)
#print "Chaos: "+str(chaos)+" C.Half: "+str(chaos_half)+" D.C.Half: "+str(diamond_chaos_half)
iteration += 1
"""Find the "anchor points" that mark the upper-left corner
of each cell."""
for anchor_y in range(0,size-1,cell_size):
for anchor_x in range(0,size-1,cell_size):
## Calculate the center of the cell.
cx = anchor_x + cell_size_half
cy = anchor_y + cell_size_half
## The "Diamond" phase.
## Find the center's diagonal "neighbors."
neighbors = ([cx-cell_size_half,cy-cell_size_half],
[cx+cell_size_half,cy-cell_size_half],
[cx-cell_size_half,cy+cell_size_half],
[cx+cell_size_half,cy+cell_size_half])
## Correct for points outside the map.
for n in range(4):
neighbor = neighbors[n]
if neighbor[0] < 0:
neighbors[n][0] += size
elif neighbor[0] > size:
neighbors[n][0] -= size
if neighbor[1] < 0:
neighbors[n][1] += size
elif neighbor[1] > size:
neighbors[n][1] -= size
average = sum([heightmap[n[0]][n[1]] for n in neighbors]) * .25
h = average - chaos_half + (random.random() * chaos)
## h = average ## Test: No randomness.
h = max(0.0,min(1.0,h))
heightmap[cx][cy] = h
## The "Square" phase.
## Calculate four "edge points" surrounding the center.
edge_points = ((cx,cy-cell_size_half),
(cx-cell_size_half,cy),
(cx+cell_size_half,cy),
(cx,cy+cell_size_half))
for point in edge_points:
neighbors = [[point[0],point[1]-cell_size_half],
[point[0]-cell_size_half,point[1]],
[point[0]+cell_size_half,point[1]],
[point[0],point[1]+cell_size_half]]
## Correct for points outside the map.
for n in range(4):
neighbor = neighbors[n]
if neighbor[0] < 0:
neighbors[n][0] += size
elif neighbor[0] > size_minus_1:
neighbors[n][0] -= size
if neighbor[1] < 0:
neighbors[n][1] += size
elif neighbor[1] > size_minus_1:
neighbors[n][1] -= size
average = sum([heightmap[n[0]][n[1]] for n in neighbors]) * .25
h = average - chaos_half + (random.random() * chaos)
h = max(0.0,min(1.0,h))
## h = average ## Test: No randomness.
heightmap[point[0]][point[1]] = h
## End of iteration. Reduce cell size and chaos.
cell_size /= 2
cell_size_half /= 2
chaos *= .5
chaos_half = chaos * .5
diamond_chaos_half = (chaos / 1.414) * .25
#if show_in_progress:
#DisplayAsImage(heightmap)
return heightmap
#---------------------------------------------------------------------------
def drawMap(self,heightmap):
"""Draws the heighmap in the image
The range of values is assumed to be 0.0 to 1.0, in a 2D, square array.
The range is converted to greyscale values of 0 to 255."""
#print "drawMap"
#get size
size=len(heightmap)
#create image
im=Image.new(mode='RGB', size=(size,size), color=(0,0,0))
print "Image: ",im.format, im.size, im.mode
draw = ImageDraw.Draw(im)
#draw map
for y in range(size):
for x in range(size):
h = int(heightmap[x][y] * 255)*1
try:
#print heightmap[x][y]
draw.point((x,y), fill=(h,h,h))
except:
print "Error on x,y: "+str((x,y))+"; map --> 0-255 value: "+str((heightmap[x][y],h))
return im
#---------------------------------------------------------------------------
def cropImage(self,path,filename,subdivide=2):
"""crops the image in x*y subimages"""
"""strange: if we use jpg when we join the 3d terrains elevations dont fit,using bmp is ok"""
im = Image.open(path+filename+".bmp")
regions=[]
a=0
b=0
dx=im.size[0]/subdivide
dy=im.size[1]/subdivide
for y in range(0,subdivide):
regions.append([])
for x in range(0,subdivide):
#add 2 pixels, dont ask me why but this mut be done in order for 3d terrains join well
ddx=2
ddy=2
#if x==subdivide:
#ddx=0
#if y==subdivide:
#ddy=0
box = (a, b, ddx+a+dx, ddy+b+dy)
regions[y].append(im.crop(box))
subfilename=filename+"_"+str(x)+"_"+str(y)+".bmp"
regions[y][x].save(path+subfilename, "BMP")
print " "+subfilename+" saved in "+path, box
a+=dx
if a>=dx*subdivide:
a=0
b+=dy
return regions
#-------------------------------------------------------------------------------
#map=DiamondMap("media/hmaps/","hmap",129,subdivide=3)
Terrain.py
"""
Terrain:
-based on Treeform script named 'treeterain.py', never could done it without it :)
-generates a list of random tile texture files (uses custom_listdir function from Files class)
-generates x*y terrain meshes and locates them in x*y
-this module can work alone if heightmap data is already generated
"""
#import panda modules
if __name__ == "__main__":
import direct.directbase.DirectStart
from pandac.PandaModules import NodePath,GeomVertexData,GeomVertexFormat,Geom,GeomVertexWriter,GeomTriangles,GeomNode,Vec3
from pandac.PandaModules import Texture,DepthTestAttrib
#import python modules
import Image
from random import *
#import custom modules
import Files
################################################################################
class Terrain:
def __init__(self,heightmap,colormap,tilepath,alt=1.0,retro=False,subdivide=None):
print "____________________________________________________"
print "Class Terrain"
#get 10 random texture files for use as tile textures
dir=Files.custom_listdir(path=tilepath+"random/",extfilter=("",".bmp",".jpg",".png",".tga"),extreturn=False)
tiles = [ tile for tile in sample(dir,12) ]
#set first texture to be a sea tile (optional)
tiles[0]=tilepath+"blue64.jpg"
#terrain generation
#------------------
#create root nodePath that contains all terrains
root= NodePath("root")
root.setSz(alt)
size= Image.open(heightmap+".bmp").size
root.setPos(-(size[0]-2)/2.0,-(size[1]-2)/2.0,0)
root.reparentTo(render)
#single terrain
if subdivide==None:
#generate a terrain mesh from heightmap file
terrain=TerrainMesh(root,heightmap,colormap,tiles,alt,retro,subdivide,(0,0))
#multiple subdivided terrains
else:
#generate a grid of x*y terrains
terrains=[]
for x in range(0,subdivide):
for y in range(0,subdivide):
subheightmap=heightmap+"_"+str(x)+"_"+str(y)
terrains.append(TerrainMesh(root,subheightmap,colormap,tiles,alt,retro,subdivide,(x,y)))
#
#rescale and center root terrain
#self.root.setPos(-self.size/2.0,-self.size/2.0,0)
#
render.analyze()
################################################################################
"""
TerrainMesh:
-this mainly is a copy from Treeform script named 'treeterain.py', simplified and adapted...
-Generates a terrain mesh with fixed resolution from a given heightmap
-Generates extra meshes and assign them tile textures depending on height
"""
class TerrainMesh:
def __init__(self,root,heightmap,colormap,tiles,alt=1.0,retro=False,subdivide=None,loc=(0,0)):
print "____________________________________________________"
print "Class TerrainMesh"
#record vars
self.root=root
self.retro=retro
self.subdivide=subdivide
self.loc=loc
#generate vertex data list
self.data,self.size = self.fromFile(heightmap) #print self.data
#generate terrain base with black texture
terrain = self.generateMesh(0,colormap+".jpg",True)
#generate terrain layers with texture tiles
for i in range(0,len(tiles)):
terrain = self.generateMesh(i,tiles[i],False)
#===========================================================================
def fromFile(self,heightmap):
"""Generates data from given heightmap image file"""
#open heightmap image
hmap = Image.open(heightmap+".bmp").convert("L")
#get heightmap image size
xs,ys = hmap.size
#remove 2 rows from size if is one of he last terrains in x or y grid
if self.subdivide!=None:
if self.loc[0]==self.subdivide-1:
xs=hmap.size[0]-2
if self.loc[1]==self.subdivide-1:
ys=hmap.size[1]-2
#get divisor
cdiv=10
if self.retro:
hdiv=10
else:
hdiv=10.0
#generate data list: [[heightpixel,colorpixel],...] #(hmap.getpixel((x,ys-y-1))/hdiv, hmap.getpixel((x,ys-y-1))/cdiv )
return [[(hmap.getpixel((x,y))/hdiv, hmap.getpixel((x,y))/cdiv ) for y in range(ys)] for x in range(xs)],hmap.size
#===========================================================================
def generateMesh(self,i,tile,base=False):
""" generate a terrain """
data = self.data
#set transparency of vertex
def tp(n):
if base==False:
if i == n:
list = [1,1,1,.5]
else:
list = [1,1,1,0]
else:
list = [1,1,1,1]
return list
#create vdata
vdata = GeomVertexData('terrain', GeomVertexFormat.getV3c4t2(), Geom.UHStatic)
vertex = GeomVertexWriter(vdata, 'vertex')
uv = GeomVertexWriter(vdata, 'texcoord')
color = GeomVertexWriter(vdata, 'color')
#create vertices
number = 0
hm = 1
for x in range(1,len(data)-1):
for y in range(1,len(data[x])-1):
#get vertex data
v1 = Vec3(x,y,data[x][y][0])
c1 = data[x][y][1]
v2 = Vec3(x+1,y,data[x+1][y][0])
c2 = data[x+1][y][1]
v3 = Vec3(x+1,y+1,data[x+1][y+1][0])
c3 = data[x+1][y+1][1]
v4 = Vec3(x,y+1,data[x][y+1][0])
c4 = data[x][y+1][1]
#discard vertex that we don't want
if base==False:
if c1 != i and c2 != i and c3 != i and c4 != i :
continue
c1,c2,c3,c4 = tp(c1),tp(c2),tp(c3),tp(c4)
#
#vertex color/lighting (?) what is this (?)
#
#avg = (c1[3]+c2[3]+c3[3]+c4[3])/4.
#c1[3]/=avg
#c2[3]/=avg
#c3[3]/=avg
#c4[3]/=avg
#
#add vertices
#
vertex.addData3f(v1)
color.addData4f(*c1)
uv.addData2f(0,0)
vertex.addData3f(v2)
color.addData4f(*c2)
uv.addData2f(1,0)
vertex.addData3f(v3)
color.addData4f(*c3)
uv.addData2f(1,1)
#
vertex.addData3f(v1)
color.addData4f(*c1)
uv.addData2f(0,0)
vertex.addData3f(v3)
color.addData4f(*c3)
uv.addData2f(1,1)
vertex.addData3f(v4)
color.addData4f(*c4)
uv.addData2f(0,1)
#
number = number + 2
#add triangles
prim = GeomTriangles(Geom.UHStatic)
for n in range(number):
prim.addVertices(n*3,n*3+1,n*3+2)
prim.closePrimitive()
#create geom and geomnode
geom = Geom(vdata)
geom.addPrimitive(prim)
geomnode = GeomNode('gnode')
geomnode.addGeom(geom)
#create terrain node and parent it to parent
terain = NodePath("terain")
terain.attachNewNode(geomnode)
terain.reparentTo(self.root)
#set render order
terain.setBin("",1)
#load and assign texture
tx = loader.loadTexture(tile)
tx.setMinfilter(Texture.FTLinearMipmapLinear)
#set texture blend
if base==False:
terain.setDepthTest(DepthTestAttrib.MLessEqual)
terain.setDepthWrite(False)
terain.setTransparency(True)
terain.setTexture(tx)
#get location
dx=self.loc[0]*(-2+self.size[0])/1.0
dy=self.loc[1]*(-2+self.size[1])/1.0
#locate terrain
if base==False:
#we have to locate texture meshes a bit up in order to see them, not very nice, ideas(?)
#terain.setPos(0,0,.02)
terain.setPos(dx,dy,.02)
else:
#terain.setPos(0,0,0)
terain.setPos(dx,dy,0)
#return terrain nodePath
return terain
################################################################################
if __name__ == "__main__":
terrain=Terrain(heightmap="hmaps/h.jpg",colormap="hmaps/black.jpg",tilepath="terraintiles/")
run()
Files.py
"""
Files:
Contains several file management usefull functions
"""
import os
#===========================================================================
def custom_listdir(path,extfilter,extreturn=False):
"""
#usage: dir=custom_listdir(path="terraintiles/",extfilter=(".bmp",".jpg",".png",".tga"),extreturn=False)
#Returns a list with given directory by
#-showing directories first
#-showing files by ordering the names alphabetically.
#-removing files with unwanted extensions from the list
#-returns another list with the file extensions if extreturn=True.
#Shure there's a super more pythonic way to do this, sorry the mess!
"""
dir = sorted([d for d in os.listdir(path) if os.path.isdir(path + os.path.sep + d)])
dir.extend(sorted([f for f in os.listdir(path) if os.path.isfile(path + os.path.sep + f)]))
#filter extensions
unwanted=[]
ext=[]
dirok=[]
extok=[]
for i in range(0,len(dir)):
#add path
dir[i]=path+dir[i]
#get extension
ext.append(dir[i][dir[i].find("."):len(dir[i])])
#add unwanted files to unwanted list
n=0
for e in range(0,len(extfilter)):
if ext[i].lower()<>extfilter[e].lower():
n+=1
if n==len(extfilter):
unwanted.append(dir[i])
#make new lists without the unwanted files
dirok=[]
extok=[]
for i in range(0,len(dir)):
n=0
for u in range(0,len(unwanted)):
if dir[i]<>unwanted[u]:
n+=1
if n==len(unwanted):
dirok.append(dir[i])
extok.append(ext[i])
#return dir list
if extreturn==True:
return dirok,extok
else:
return dirok
#dir=custom_listdir(path="tiles/",extfilter=("",".bmp",".jpg",".png",".tga"),extreturn=False)
#for i in range(0,len(dir)):
#print dir[i]
Thanks Treeform for the ātreeterrainā amazing script
Thanks pro-rsoft for advice
Thatās it, enjoy retro-lovers! hope someone founds it usefull/helpfull!
Any feedback, comments, ideas, help will be apreciated
c.