I could of sworn GeoMipTerrain had a function that would render blocks after a certain distance. Looks like I am wrong though.
For my city builder I’ve started writing a “PagedGeoMipTerrain” class. Right now the only thing it can do is create none power of two terrains by sticking a bunch of geomipterrains together.
However I am looking for the same functionality you are, so I will post what I have so far here.
I did the best I could to have the same interface as a GeoMipTerrain so it should be able to drop right in to any code. It is not well tested, so there may be some bugs.
'''PagedGeoMipTerrain.py
Hacked together by croxis 2010
BSD licence'''
from panda3d.core import NodePath, PNMImage, GeoMipTerrain, Texture, TextureStage
from panda3d.core import Vec2
from panda3d.core import GeomVertexReader, GeomVertexWriter
import math
class PagedGeoMipTerrain(object):
'''Terrain using GeoMipTerrains from a heightfield.
Syntax is as close to GeoMipTerrain as possible.
Can accept any heightmap based on block*chunksize + 1
The higher these values the fewer geoms, but the less variability allowed in terrain size.'''
def __init__(self, name):
self.name = name
self.root = NodePath(name)
self.blockSize = 32
self.chunkSize = 1
self.colorMap = None
self.terrains = []
self.bruteForce = False
self.heightfield = None
self.xsize = 0
self.ysize = 0
def clearColorMap(self):
'''Clears the color map. Non functional.'''
def generate(self):
'''(Re)generate the entire terrain erasing any current changes'''
factor = self.blockSize*self.chunkSize
#print "Factor:", factor
for terrain in self.terrains:
terrain.getRoot().removeNode()
self.terrains = []
# Breaking master heightmap into subimages
heightmaps = []
self.xchunks = (self.heightfield.getXSize()-1)/factor
self.ychunks = (self.heightfield.getYSize()-1)/factor
#print "X,Y chunks:", self.xchunks, self.ychunks
n = 0
for y in range(0, self.ychunks):
for x in range(0, self.xchunks):
heightmap = PNMImage(factor+1, factor+1)
heightmap.copySubImage(self.heightfield, 0, 0, xfrom = x*factor, yfrom = y*factor)
heightmaps.append(heightmap)
n += 1
# Generate GeoMipTerrains
n = 0
y = self.ychunks-1
x = 0
for heightmap in heightmaps:
terrain = GeoMipTerrain(str(n))
terrain.setHeightfield(heightmap)
terrain.setBruteforce(self.bruteForce)
terrain.setBlockSize(self.blockSize)
terrain.generate()
self.terrains.append(terrain)
root = terrain.getRoot()
root.reparentTo(self.root)
root.setPos(n%self.xchunks*factor, (y)*factor, 0)
# In order to texture span properly we need to reiterate through every vertex
# and redefine the uv coordinates based on our size, not the subGeoMipTerrain's
root = terrain.getRoot()
children = root.getChildren()
for child in children:
geomNode = child.node()
for i in range(geomNode.getNumGeoms()):
geom = geomNode.modifyGeom(i)
vdata = geom.modifyVertexData()
texcoord = GeomVertexWriter(vdata, 'texcoord')
vertex = GeomVertexReader(vdata, 'vertex')
while not vertex.isAtEnd():
v = vertex.getData3f()
t = texcoord.setData2f((v[0]+ self.blockSize/2 + self.blockSize*x)/(self.xsize - 1),
(v[1] + self.blockSize/2 + self.blockSize*y)/(self.ysize - 1))
x += 1
if x >= self.xchunks:
x = 0
y -= 1
n += 1
def getBlockFromPos(self, x, y):
'''Gets the coordinates of the block at the specific position.'''
if x < 0: x=0
if y < 0: y=0
if x > self.xsize - 1: x = self.xsize - 1
if y > self.ysize - 1: y = self.ysize - 1
factor = self.chunkSize * self.blockSize
x = math.floor(x/factor)
y = math.floor(y/factor)
return Vec2(x, y)
def getBlockNodePath(self, x, y):
'''Returns NodePath of the specified block'''
# Divide by chunksize to get terrain responsible
xchunk = x/self.chunkSize
ychunk = y/self.chunkSize
terrain = self.terrains[int(ychunk*self.xchunks + xchunk)]
nodePath = terrain.getBlockNodePath(x-(xchunk*self.chunkSize), y-(ychunk*self.chunkSize))
return nodePath
def getChunk(self, x, y):
'''Returns the GeoMipTerrain at the specificed position'''
def getBlockSize(self):
'''Returns the block size.'''
return self.blockSize
def getBruteforce(self):
'''Returns boolian if terrain is being rendered with brute force or not'''
return bruitForce
def getElevation(self, x, y):
'''Returns elevation at specific point in px.
Due to specific customizations for CityMania xy is in world coordinates.
When this is generalized to a PagedGeoMipTerrain this will need to be redone.
Z scale is not observed'''
factor = self.blockSize*self.chunkSize
# Determine which geomip holds the terrain
row = y/(factor)
col = x/(factor)
#chunk = -row*self.ychunks - col
chunk = self.getBlockFromPos(x, y)
xchunk = chunk[0]/self.chunkSize
ychunk = chunk[1]/self.chunkSize
terrain = self.terrains[int(ychunk*self.xchunks + xchunk)]
#terrain = self.terrains[int(chunk)]
#terrain = self.terrains[int(row*self.xchunks + col)]
# Convert to xy of chunk
chunkx = x - col*factor
chunky = y - row*factor
elevation = terrain.getElevation(chunkx, chunky)
return elevation
def getFocalPoint(self, x, y):
'''Returns focal point as a node path'''
def getMaxLevel(self):
''''Returns highest level of detail possible with current block size'''
def getMinLevel(self):
''''Returns highest level of detail possible with current block size'''
def getNormal(self, x, y):
'''Returns normal at specified pixles'''
def getRoot(self):
'''Returns root nodepath'''
return self.root
def getSz(self):
'''Citymania overide for sz'''
return self.sz
def hasColorMap(self):
'''Returns if a color map has been set'''
if self.colorMap:
return True
return False
def getHeightfield(self):
'''Returns a reference to the master heightfield'''
return self.heightfield
def isDirty(self):
'''Returns a bool indicating is the terain will need to be updated next update().
Usually because the heightfield has changed.'''
def makeSlopeImage(self):
'''Returns a greyscale PNMImage containing the slope angles.
This is composited from the assorted GeoMipTerrains.'''
slopeImage = PNMImage(self.heightfield.getXSize(), self.heightfield.getYSize())
factor = self.blockSize*self.chunkSize
n = 0
for y in range(0, self.ychunks):
for x in range(0, self.xchunks):
slopei = self.terrains[n].makeSlopeImage()
#slopeImage.copySubImage(slopei, x*factor, y*factor, 0, 0)
slopeImage.copySubImage(slopei, x*factor, y*factor)
n += 1
return slopeImage
def makeTextureMap(self):
'''Citymania function that generates and sets the 4 channel texture map'''
self.colorTextures = []
for terrain in self.terrains:
terrain.getRoot().clearTexture()
heightmap = terrain.heightfield()
colormap = PNMImage(heightmap.getXSize()-1, heightmap.getYSize()-1)
colormap.addAlpha()
slopemap = terrain.makeSlopeImage()
for x in range(0, colormap.getXSize()):
for y in range(0, colormap.getYSize()):
# Else if statements used to make sure one channel is used per pixel
# Also for some optimization
# Snow. We do things funky here as alpha will be 1 already.
if heightmap.getGrayVal(x, y) < 200:
colormap.setAlpha(x, y, 0)
else:
colormap.setAlpha(x, y, 1)
# Beach. Estimations from http://www.simtropolis.com/omnibus/index.cfm/Main.SimCity_4.Custom_Content.Custom_Terrains_and_Using_USGS_Data
if heightmap.getGrayVal(x,y) < 62:
colormap.setBlue(x, y, 1)
# Rock
elif slopemap.getGrayVal(x, y) > 170:
colormap.setRed(x, y, 1)
else:
colormap.setGreen(x, y, 1)
colorTexture = Texture()
colorTexture.load(colormap)
colorTS = TextureStage('color')
colorTS.setSort(0)
colorTS.setPriority(1)
self.colorTextures.append((colorTexture, colorTS))
def setAutoFlatten(self, mode):
'''The terrain can be automatically flattened after each update'''
def setBlockSize(self, size):
'''Sets the block size. Must be power of 2.'''
# TODO: Add power of two check
self.blockSize = size
def setBorderStitching(self, stitching):
'''If set true the LOG at borders will be 0'''
def setBruteforce(self, bruteForce):
'''Set boolian if rendering will happen by brute force'''
self.bruteForce = bruteForce
def setColorMap(self, cm):
'''Sets the color map. As GeoMipTerrain has several possible entries we will need to typecheck.'''
def setFar(self, far):
'''Sets the far LOD distance at which the terrain will be rendered at the lowest quality.'''
for terrain in self.terrains:
terrain.setFar(far)
def setFocalPoint(self, focalPoint):
'''Sets the focal point. Can be Nodepath, LPoint3, LPoint2'''
for terrain in self.terrains:
terrain.setFocalPoint(focalPoint)
def setHeightfield(self, heightfield):
'''Loads the heighmap image. Currently only accepts PNMIMage
TODO: str path, FileName'''
if type(heightfield) is str:
heightfield = PNMImage(heightfield)
self.heightfield = heightfield
self.xsize = heightfield.getXSize()
self.ysize = heightfield.getYSize()
def setMinLevel(self):
''' Sets the minimum level of detail at which blocks may be generated by generate() or update(). '''
def setNear(self, near):
'''Sets the near LOD distance, at which the terrain will be rendered at highest quality. '''
for terrain in self.terrains:
terrain.setNear(near)
def setNearFar(self,near, far):
'''Sets the near and far LOD distances in one call. '''
for terrain in self.terrains:
terrain.setNearFar(near, far)
def setSz(self, z):
'''Citymania override to set the scale to the terrains'''
self.sz = z
for terrain in self.terrains:
terrain.getRoot().setSz(z)
def setTextureMap(self):
'''Reactivates the texture map'''
for n in range(0, len(self.terrains)):
terrain = self.terrains[n]
colorTexture, colorTS = self.colorTextures[n]
terrain.getRoot().setTexture( colorTS, colorTexture )
def update(self):
'''Loops through all of the terrain blocks, and checks whether they need to be updated. '''
for terrain in self.terrains:
terrain.update()