class Terrain(NodePath): """A terrain contains a set of geomipmaps, and maintains their common properties.""" def __init__(self, name, focus, maxRange, populator=None, feedBackString=None, id=0): """Create a new terrain centered on the focus. The focus is the NodePath where the LOD is the greatest. id is a seed for the map and unique name for any cached heightmap images """ NodePath.__init__(self, name) ### Basic Parameters self.name = name # nodepath to center of the level of detail self.focus = focus # stores all terrain tiles that make up the terrain self.tiles = {} # stores previously built tiles we can readd to the terrain self.storage = {} self.feedBackString = feedBackString if populator == None: populator = TerrainPopulator() self.populator = populator self.graphReducer = SceneGraphReducer() if THREAD_LOAD_TERRAIN: self.tileBuilder = TerrainTileBuilder(self) ##### Terrain Tile physical properties self.maxHeight = MAX_TERRAIN_HEIGHT self.tileSize = 128 self.heightMapSize = self.tileSize + 1 ##### Terrain scale and tile distances # distances are measured in tile's smallest unit # conversion to world units may be necessary # Don't show untiled terrain below this distance etc. # scale the terrain vertically to its maximum height self.setSz(self.maxHeight) # scale horizontally to appearance/performance balance self.horizontalScale = TERRAIN_HORIZONTAL_STRETCH self.setSx(self.horizontalScale) self.setSy(self.horizontalScale) # waterHeight is expressed as a multiplier to the max height self.waterHeight = 0.3 #this is the furthest the camera can view self.maxViewRange = maxRange # Add half the tile size because distance is checked from the center, # not from the closest edge. self.minTileDistance = self.maxViewRange / self.horizontalScale + self.tileSize / 2 # to avoid excessive store / retrieve behavior on tiles we have a small # buffer where it doesn't matter whether or not the tile is present self.maxTileDistance = self.minTileDistance + self.tileSize / 2 ##### heightmap properties self.initializeHeightMap(id) ##### rendering properties self.initializeRenderProperties() ##### task handling #self._setupThreadedTasks() # newTile is a placeholder for a tile currently under construction # this has to be initialized last because it requires values from self #self.newTile = TerrainTile(self, 0, 0) # loads all terrain tiles in range immediately if THREAD_LOAD_TERRAIN: self.preload(self.focus.getX() / self.horizontalScale, self.focus.getY() / self.horizontalScale) else: taskMgr.add(self.oldPreload, "preloadTask", extraArgs=[self.focus.getX() / self.horizontalScale, self.focus.getY() / self.horizontalScale]) #self.flattenLight() def initializeHeightMap(self, id=0): """ """ logging.info("initalizing heightmap...") if id == 0: self.dice = RandomNumGen(TimeVal().getUsec()) id = self.dice.randint(2, 1000000) self.id = id #Remove old tiles that will not conform to a new heightmap for pos, tile in self.tiles.items(): self.deleteTile(pos) self.storage.clear() self.heightMap = HeightMap(id, self.waterHeight + 0.03) self.getHeight = self.heightMap.getHeight def initializeRenderingProperties(self): logging.info("initializing terrain rendering properties...") #self.bruteForce = True self.bruteForce = BRUTE_FORCE_TILES if self.bruteForce: self.blockSize = self.tileSize else: #self.blockSize = 16 self.blockSize = self.tileSize self.near = 100 self.far = self.maxViewRange * 0.5 + self.blockSize self.wireFrame = 0 #self.texturer = MonoTexturer(self) self.texturer = ShaderTexturer(self) #self.texturer = DetailTexturer(self) #self.texturer.apply(self) #self.setShaderInput("zMultiplier",) logging.info("rendering properties initialized...") def _setupSimpleTasks(self): """This sets up tasks to maintain the terrain as the focus moves.""" logging.info("initializing terrain update task...") ##Add tasks to keep updating the terrain #taskMgr.add(self.updateTilesTask, "updateTiles", sort=9, priority=0) taskMgr.doMethodLater(5, self.update, "update", sort=9, priority=0) self.updateStep = 1 def reduceSceneGraph(self, radius): gr = self.graphReducer gr.applyAttribs(self.node()) gr.setCombineRadius(radius) gr.flatten(self.node(), SceneGraphReducer.CSRecurse) gr.makeCompatibleState(self.node()) gr.collectVertexData(self.node()) gr.unify(self.node(), False) def update(self, task): """This task updates terrain as needed.""" if self.updateStep == 1: self.makeNewTile() if THREAD_LOAD_TERRAIN: self.grabBuiltTile() self.removeOldTiles() self.updateStep += 1 return Task.cont #self.updateTiles() self.tileLodUpdate() #self.buildDetailLevels() self.updateStep = 1 return Task.cont def updateLight(self): """This task moves point and directional lights. For larger projects this should be externalized. """ self.pointLight = Vec3(0, 5, 0)#self.focus.getPos() + Vec3(0,5,0) self.setShaderInput("LightPosition", self.pointLight) def updateTiles(self): """This task updates each tile, which updates the LOD. GeoMipMap updates are slow however and may cause unacceptable lag. """ #logging.info(self.focus.getPos()) for pos, tile in self.tiles.items(): tile.update() #logging.info(str(tile.getFocalPoint().getPos())) #if tile.update(): #logging.info("update success") #yield Task.cont def tileLodUpdate(self): """Updates tiles to LOD appropriate for their distance. setMinDetailLevel() doesn't flag a geomipterrain as dirty, so update will not alter detail level. It would have to be regenerated. Instead we will use a special LodTerrainTile. """ if not self.bruteForce: self.updateTiles() return focusx = self.focus.getX() / self.horizontalScale focusy = self.focus.getY() / self.horizontalScale halfTile = self.tileSize * 0.5 # switch to high, mid, and low LOD's at these distances # having a gap between the zones avoids switching back and forth too # if the focus is moving erratically highOuter = self.minTileDistance * 0.02 + self.tileSize highOuter *= highOuter midInner = self.minTileDistance * 0.02 + self.tileSize + halfTile midInner *= midInner midOuter = self.minTileDistance * 0.2 + self.tileSize midOuter *= midOuter lowInner = self.minTileDistance * 0.2 + self.tileSize + halfTile lowInner *= lowInner lowOuter = self.minTileDistance * 0.5 + self.tileSize lowOuter *= lowOuter horizonInner = self.minTileDistance * 0.5 + self.tileSize + halfTile horizonInner *= horizonInner for pos, tile in self.tiles.items(): deltaX = focusx - (pos[0] + halfTile) deltaY = focusy - (pos[1] + halfTile) distance = deltaX * deltaX + deltaY * deltaY if distance < highOuter: tile.setDetail(0) elif distance < midOuter: if distance > midInner or tile.getDetail() > 1: tile.setDetail(1) elif distance < lowOuter: if distance > lowInner or tile.getDetail() > 2: tile.setDetail(2) elif distance > horizonInner: tile.setDetail(3) def buildDetailLevels(self): """Unused.""" n = len(self.buildQueue) / 5.0 if n > 0 and n < 1: n = 1 else: n = int(n) for i in range(n): request = self.buildQueue.popleft() request[0].buildAndSet(request[1]) def oldPreload(self, task, xpos=0, ypos=0): """Loads all tiles in range immediately. This can suspend the program for a long time and is best used when first loading a level. It simply iterates through a square region building any tile that is reasonably within the max distance. It does not prioritize tiles closest to the focus. """ logging.info("preloading terrain tiles...") self.buildQueue = deque() # x and y start are rounded to the nearest multiple of tile size xstart = (int(xpos / self.horizontalScale) / self.tileSize) * self.tileSize ystart = (int(ypos / self.horizontalScale) / self.tileSize) * self.tileSize # check radius is rounded up to the nearest tile size from maxTileDistance # not every tile in checkRadius will be made checkRadius = (int(self.maxTileDistance) / self.tileSize + 1) * self.tileSize halfTile = self.tileSize * 0.5 # build distance for the preloader will be halfway in between the normal # load distance and the unloading distance buildDistanceSquared = (self.minTileDistance + self.maxTileDistance) / 2 buildDistanceSquared = buildDistanceSquared * buildDistanceSquared for x in range (xstart - checkRadius, xstart + checkRadius, self.tileSize): for y in range (ystart - checkRadius, ystart + checkRadius, self.tileSize): if not (x, y) in self.tiles: deltaX = xpos - (x + halfTile) deltaY = ypos - (y + halfTile) distanceSquared = deltaX * deltaX + deltaY * deltaY if distanceSquared < buildDistanceSquared: self.buildQueue.append((x, y)) total = len(self.buildQueue) while len(self.buildQueue): if self.feedBackString: done = total - len(self.buildQueue) feedback = "Loading Terrain " + str(done) + "/" + str(total) logging.info(feedback) self.feedBackString.setText(feedback) tile = self.buildQueue.popleft() self._generateTile(tile) yield Task.cont self._setupSimpleTasks() yield Task.done def preload(self, xpos=1, ypos=1): """ """ logging.info("preloading terrain tiles...") # x and y start are rounded to the nearest multiple of tile size xstart = (int(xpos / self.horizontalScale) / self.tileSize) * self.tileSize ystart = (int(ypos / self.horizontalScale) / self.tileSize) * self.tileSize # check radius is rounded up to the nearest tile size from maxTileDistance # not every tile in checkRadius will be made checkRadius = (int(self.maxTileDistance) / self.tileSize + 1) * self.tileSize halfTile = self.tileSize * 0.5 # build distance for the preloader will be halfway in between the normal # load distance and the unloading distance buildDistanceSquared = (self.minTileDistance + self.maxTileDistance) / 2 buildDistanceSquared = buildDistanceSquared * buildDistanceSquared for x in range (xstart - checkRadius, xstart + checkRadius, self.tileSize): for y in range (ystart - checkRadius, ystart + checkRadius, self.tileSize): if not (x, y) in self.tiles: deltaX = xpos - (x + halfTile) deltaY = ypos - (y + halfTile) distanceSquared = deltaX * deltaX + deltaY * deltaY if distanceSquared < buildDistanceSquared: self.tileBuilder.preload((x, y)) self.preloadTotal = self.tileBuilder.queue.qsize() taskMgr.add(self.preloadWait, "preloadWaitTask") def preloadWait(self, task): #loggin.info( "preloadWait()") if self.feedBackString: done = self.preloadTotal - self.tileBuilder.queue.qsize() feedback = "Loading Terrain " + str(done) + "/" + str(self.preloadTotal) logging.info(feedback) self.feedBackString.setText(feedback) #self.grabBuiltTile() if self.tileBuilder.queue.qsize() > 0: #logging.info( self.tileBuilder.queue.qsize()) return Task.cont self._setupSimpleTasks() return Task.done #@pstat def makeNewTile(self): """Generate the closest terrain tile needed.""" # tiles are placed under the terrain node path which may be scaled x = self.focus.getX(self) / self.horizontalScale y = self.focus.getY(self) / self.horizontalScale # start position is the focus position rounded to the multiple of tile size xstart = (int(x) / self.tileSize) * self.tileSize ystart = (int(y) / self.tileSize) * self.tileSize # radius is rounded up from minTileDistance to nearest multiple of tile size # not every tile within checkRadius will be made checkRadius = (int(self.minTileDistance) / self.tileSize + 1) * self.tileSize halfTile = self.tileSize * 0.49 tiles = self.tiles #logging.info( xstart, ystart, checkRadius) vec = 0 minFoundDistance = 9999999999.0 minDistanceSq = self.minTileDistance * self.minTileDistance for checkX in range (xstart - checkRadius, xstart + checkRadius, self.tileSize): for checkY in range (ystart - checkRadius, ystart + checkRadius, self.tileSize): if not (checkX, checkY) in tiles: deltaX = x - (checkX + halfTile) deltaY = y - (checkY + halfTile) distanceSq = deltaX * deltaX + deltaY * deltaY if distanceSq < minDistanceSq and distanceSq < minFoundDistance: minFoundDistance = distanceSq vec = (checkX, checkY) if not vec == 0: #logging.info( distance," < ", self.minTileDistance," and ", distance," < ", minDistance) #self.generateTile(vec.getX(), vec.getY()) if THREAD_LOAD_TERRAIN: self.dispatchTile(vec) else: self._generateTile(vec) #@pstat def dispatchTile(self, pos): """Creates a terrain tile at the input coordinates.""" if pos in self.storage: tile = self.storage[pos] self.tiles[pos] = tile tile.getRoot().reparentTo(self) del self.storage[pos] logging.info("tile recovered from storage at " + str(pos)) return self.tileBuilder.build(pos) self.tiles[pos] = 1 #@pstat def _generateTile(self, pos): """Creates a terrain tile at the input coordinates.""" if pos in self.storage: tile = self.storage[pos] self.tiles[pos] = tile tile.getRoot().reparentTo(self) del self.storage[pos] logging.info("tile recovered from storage at " + str(pos)) #self.flattenMedium() return if SAVED_TEXTURE_MAPS: tile = TextureMappedTerrainTile(self, pos[0], pos[1]) elif self.bruteForce: tile = LodTerrainTile(self, pos[0], pos[1]) else: tile = TerrainTile(self, pos[0], pos[1]) tile.make() tile.getRoot().reparentTo(self) self.tiles[pos] = tile logging.info("tile generated at " + str(pos)) #self.flattenMedium() return tile def grabBuiltTile(self): #logging.info( "grabBuiltTile()") tile = self.tileBuilder.grab() #logging.info( "tile = "+ str(tile)) if tile: pos = (tile.xOffset, tile.yOffset) tile.getRoot().reparentTo(self) self.tiles[pos] = tile logging.info("tile generated at " + str(pos)) return tile return None #@pstat def removeOldTiles(self): """Remove distant tiles to free system resources.""" x = self.focus.getX(self) / self.horizontalScale y = self.focus.getY(self) / self.horizontalScale center = self.tileSize * 0.5 maxDistanceSquared = self.maxTileDistance * self.maxTileDistance for pos, tile in self.tiles.items(): deltaX = x - (pos[0] + center) deltaY = y - (pos[1] + center) distance = deltaX * deltaX + deltaY * deltaY if distance > maxDistanceSquared: #logging.info( distance+ " > "+ self.maxTileDistance * self.maxTileDistance) self.storeTile(pos) def storeTile(self, pos): tile = self.tiles[pos] if tile != 1: tile.getRoot().detachNode() self.storage[pos] = tile del self.tiles[pos] logging.info("Tile removed from " + str(pos)) def deleteTile(self, pos): """Removes a specific tile from the Terrain.""" self.tiles[pos].getRoot().detachNode() del self.tiles[pos] logging.info("Tile deleted from " + str(pos)) def getElevation(self, x, y): """Returns the height of the terrain at the input world coordinates.""" x /= self.horizontalScale y /= self.horizontalScale if SAVED_HEIGHT_MAPS: tilex = (int(x) / self.tileSize) * self.tileSize tiley = (int(y) / self.tileSize) * self.tileSize x -= tilex y -= tiley if (tilex, tiley) in self.tiles: return self.tiles[tilex, tiley].getElevation(x, y) * self.getSz() if (tilex, tiley) in self.storage: return self.storage[tilex, tiley].getElevation(x, y) * self.getSz() return self.getHeight(x, y) * self.getSz() def setWireFrame(self, state): self.wireFrame = state if state: self.setRenderModeWireframe() else: self.setRenderModeFilled() #for pos, tile in self.tiles.items(): # tile.setWireFrame(state) def toggleWireFrame(self): self.setWireFrame(not self.wireFrame) def test(self): self.texturer.test() def setShaderFloatInput(self, name, input): logging.info("set shader input " + name + " to " + str(input)) self.setShaderInput(name, PTAFloat([input])) def setFocus(self, nodePath): self.focus = nodePath for pos, tile in self.tiles.items(): tile.setFocalPoint(self.focus)
class _Terrain(): """A terrain contains a set of geomipmaps, and maintains their common properties.""" def __init__(self): """Create a new terrain centered on the focus. The focus is the NodePath where the LOD is the greatest. id is a seed for the map and unique name for any cached heightmap images """ ##### Terrain Tile physical properties self.maxHeight = 300 self.dice = RandomNumGen(TimeVal().getUsec()) self.id = self.dice.randint(2, 1000000) # scale the terrain vertically to its maximum height #self.setSz(self.maxHeight) # scale horizontally to appearance/performance balance #self.horizontalScale = 1.0 #self.setSx(self.horizontalScale) #self.setSy(self.horizontalScale) ##### heightmap properties self.initializeHeightMap(id) def initializeHeightMap(self, id=0): """ """ # the overall smoothness/roughness of the terrain self.smoothness = 80 # how quickly altitude and roughness shift self.consistency = self.smoothness * 8 # waterHeight is expressed as a multiplier to the max height self.waterHeight = 0.3 # for realism the flatHeight should be at or very close to waterHeight self.flatHeight = self.waterHeight + 0.04 #creates noise objects that will be used by the getHeight function self.generateNoiseObjects() def generateNoiseObjects(self): """Create perlin noise.""" # See getHeight() for more details.... # where perlin 1 is low terrain will be mostly low and flat # where it is high terrain will be higher and slopes will be exagerrated # increase perlin1 to create larger areas of geographic consistency self.perlin1 = StackedPerlinNoise2() perlin1a = PerlinNoise2(0, 0, 256, seed=self.id) perlin1a.setScale(self.consistency) self.perlin1.addLevel(perlin1a) perlin1b = PerlinNoise2(0, 0, 256, seed=self.id * 2 + 123) perlin1b.setScale(self.consistency / 2) self.perlin1.addLevel(perlin1b, 1 / 2) # perlin2 creates the noticeable noise in the terrain # without perlin2 everything would look unnaturally smooth and regular # increase perlin2 to make the terrain smoother self.perlin2 = StackedPerlinNoise2() frequencySpread = 3.0 amplitudeSpread = 3.4 perlin2a = PerlinNoise2(0, 0, 256, seed=self.id * 2) perlin2a.setScale(self.smoothness) self.perlin2.addLevel(perlin2a) perlin2b = PerlinNoise2(0, 0, 256, seed=self.id * 3 + 3) perlin2b.setScale(self.smoothness / frequencySpread) self.perlin2.addLevel(perlin2b, 1 / amplitudeSpread) perlin2c = PerlinNoise2(0, 0, 256, seed=self.id * 4 + 4) perlin2c.setScale(self.smoothness / (frequencySpread * frequencySpread)) self.perlin2.addLevel(perlin2c, 1 / (amplitudeSpread * amplitudeSpread)) perlin2d = PerlinNoise2(0, 0, 256, seed=self.id * 5 + 5) perlin2d.setScale(self.smoothness / (math.pow(frequencySpread, 3))) self.perlin2.addLevel(perlin2d, 1 / (math.pow(amplitudeSpread, 3))) perlin2e = PerlinNoise2(0, 0, 256, seed=self.id * 6 + 6) perlin2e.setScale(self.smoothness / (math.pow(frequencySpread, 4))) self.perlin2.addLevel(perlin2e, 1 / (math.pow(amplitudeSpread, 4))) def getHeight(self, x, y): """Returns the height at the specified terrain coordinates. The values returned should be between 0 and 1 and use the full range. Heights should be the smoothest and flatest at flatHeight. """ # all of these should be in the range of 0 to 1 p1 = (self.perlin1(x, y) + 1) / 2 # low frequency p2 = (self.perlin2(x, y) + 1) / 2 # high frequency fh = self.flatHeight # p1 varies what kind of terrain is in the area, p1 alone would be smooth # p2 introduces the visible noise and roughness # when p1 is high the altitude will be high overall # when p1 is close to fh most of the visible noise will be muted return (p1 - fh + (p1 - fh) * (p2 - fh)) / 2 + fh
class Terrain(NodePath): """A terrain contains a set of geomipmaps, and maintains their common properties.""" def __init__(self, name, focus, maxRange, populator=None, feedBackString=None, id=0): """Create a new terrain centered on the focus. The focus is the NodePath where the LOD is the greatest. id is a seed for the map and unique name for any cached heightmap images """ NodePath.__init__(self, name) ### Basic Parameters self.name = name # nodepath to center of the level of detail self.focus = focus # stores all terrain tiles that make up the terrain self.tiles = {} # stores previously built tiles we can readd to the terrain self.storage = {} self.feedBackString = feedBackString if populator == None: populator = TerrainPopulator() self.populator = populator self.graphReducer = SceneGraphReducer() if THREAD_LOAD_TERRAIN: self.tileBuilder = TerrainTileBuilder(self) ##### Terrain Tile physical properties self.maxHeight = MAX_TERRAIN_HEIGHT self.tileSize = 128 self.heightMapSize = self.tileSize + 1 ##### Terrain scale and tile distances # distances are measured in tile's smallest unit # conversion to world units may be necessary # Don't show untiled terrain below this distance etc. # scale the terrain vertically to its maximum height self.setSz(self.maxHeight) # scale horizontally to appearance/performance balance self.horizontalScale = TERRAIN_HORIZONTAL_STRETCH self.setSx(self.horizontalScale) self.setSy(self.horizontalScale) # waterHeight is expressed as a multiplier to the max height self.waterHeight = 0.3 #this is the furthest the camera can view self.maxViewRange = maxRange # Add half the tile size because distance is checked from the center, # not from the closest edge. self.minTileDistance = self.maxViewRange / self.horizontalScale + self.tileSize / 2 # to avoid excessive store / retrieve behavior on tiles we have a small # buffer where it doesn't matter whether or not the tile is present self.maxTileDistance = self.minTileDistance + self.tileSize / 2 ##### heightmap properties self.initializeHeightMap(id) ##### rendering properties self.initializeRenderingProperties() ##### task handling #self._setupThreadedTasks() # newTile is a placeholder for a tile currently under construction # this has to be initialized last because it requires values from self #self.newTile = TerrainTile(self, 0, 0) # loads all terrain tiles in range immediately if THREAD_LOAD_TERRAIN: self.preload(self.focus.getX() / self.horizontalScale, self.focus.getY() / self.horizontalScale) else: taskMgr.add(self.oldPreload, "preloadTask", extraArgs=[self.focus.getX() / self.horizontalScale, self.focus.getY() / self.horizontalScale]) #self.flattenLight() def initializeHeightMap(self, id=0): """ """ logging.info("initializing heightmap...") if id == 0: self.dice = RandomNumGen(TimeVal().getUsec()) id = self.dice.randint(2, 1000000) self.id = id #Remove old tiles that will not conform to a new heightmap for pos, tile in self.tiles.items(): self.deleteTile(pos) self.storage.clear() self.heightMap = HeightMap(id, self.waterHeight + 0.03) self.getHeight = self.heightMap.getHeight def initializeRenderingProperties(self): logging.info("initializing terrain rendering properties...") #self.bruteForce = True self.bruteForce = BRUTE_FORCE_TILES if self.bruteForce: self.blockSize = self.tileSize else: #self.blockSize = 16 self.blockSize = self.tileSize self.near = 100 self.far = self.maxViewRange * 0.5 + self.blockSize self.wireFrame = 0 #self.texturer = MonoTexturer(self) self.texturer = ShaderTexturer(self) #self.texturer = DetailTexturer(self) #self.texturer.apply(self) #self.setShaderInput("zMultiplier", ) logging.info("rendering properties initialized...") def _setupSimpleTasks(self): """This sets up tasks to maintain the terrain as the focus moves.""" logging.info("initializing terrain update task...") ##Add tasks to keep updating the terrain #taskMgr.add(self.updateTilesTask, "updateTiles", sort=9, priority=0) taskMgr.doMethodLater(5, self.update, "update", sort=9, priority=0) self.updateStep = 1 def reduceSceneGraph(self, radius): gr = self.graphReducer gr.applyAttribs(self.node()) gr.setCombineRadius(radius) gr.flatten(self.node(), SceneGraphReducer.CSRecurse) gr.makeCompatibleState(self.node()) gr.collectVertexData(self.node()) gr.unify(self.node(), False) def update(self, task): """This task updates terrain as needed.""" if self.updateStep == 1: self.makeNewTile() if THREAD_LOAD_TERRAIN: self.grabBuiltTile() self.removeOldTiles() self.updateStep += 1 return Task.cont #self.updateTiles() self.tileLodUpdate() #self.buildDetailLevels() self.updateStep = 1 return Task.cont def updateLight(self): """This task moves point and directional lights. For larger projects this should be externalized. """ self.pointLight = vec3(0, 5, 0)#self.focus.getPos() + vec3(0,5,0) self.setShaderInput("LightPosition", self.pointLight) def updateTiles(self): """This task updates each tile, which updates the LOD. GeoMipMap updates are slow however and may cause unacceptable lag. """ #logging.info(self.focus.getPos()) for pos, tile in self.tiles.items(): tile.update() #logging.info(str(tile.getFocalPoint().getPos())) #if tile.update(): #logging.info("update success") #yield Task.cont def tileLodUpdate(self): """Updates tiles to LOD appropriate for their distance. setMinDetailLevel() doesn't flag a geomipterrain as dirty, so update will not alter detail level. It would have to be regenerated. Instead we will use a special LodTerrainTile. """ if not self.bruteForce: self.updateTiles() return focusx = self.focus.getX() / self.horizontalScale focusy = self.focus.getY() / self.horizontalScale halfTile = self.tileSize * 0.5 # switch to high, mid, and low LOD's at these distances # having a gap between the zones avoids switching back and forth too # if the focus is moving erratically highOuter = self.minTileDistance * 0.02 + self.tileSize highOuter *= highOuter midInner = self.minTileDistance * 0.02 + self.tileSize + halfTile midInner *= midInner midOuter = self.minTileDistance * 0.2 + self.tileSize midOuter *= midOuter lowInner = self.minTileDistance * 0.2 + self.tileSize + halfTile lowInner *= lowInner lowOuter = self.minTileDistance * 0.5 + self.tileSize lowOuter *= lowOuter horizonInner = self.minTileDistance * 0.5 + self.tileSize + halfTile horizonInner *= horizonInner for pos, tile in self.tiles.items(): deltaX = focusx - (pos[0] + halfTile) deltaY = focusy - (pos[1] + halfTile) distance = deltaX * deltaX + deltaY * deltaY if distance < highOuter: tile.setDetail(0) elif distance < midOuter: if distance > midInner or tile.getDetail() > 1: tile.setDetail(1) elif distance < lowOuter: if distance > lowInner or tile.getDetail() > 2: tile.setDetail(2) elif distance > horizonInner: tile.setDetail(3) def buildDetailLevels(self): """Unused.""" n = len(self.buildQueue) / 5.0 if n > 0 and n < 1: n = 1 else: n = int(n) for i in range(n): request = self.buildQueue.popleft() request[0].buildAndSet(request[1]) def oldPreload(self, task, xpos=0, ypos=0): """Loads all tiles in range immediately. This can suspend the program for a long time and is best used when first loading a level. It simply iterates through a square region building any tile that is reasonably within the max distance. It does not prioritize tiles closest to the focus. """ logging.info("preloading terrain tiles...") self.buildQueue = deque() # x and y start are rounded to the nearest multiple of tile size xstart = (int(xpos / self.horizontalScale) / self.tileSize) * self.tileSize ystart = (int(ypos / self.horizontalScale) / self.tileSize) * self.tileSize # check radius is rounded up to the nearest tile size from maxTileDistance # not every tile in checkRadius will be made checkRadius = (int(self.maxTileDistance) / self.tileSize + 1) * self.tileSize halfTile = self.tileSize * 0.5 # build distance for the preloader will be halfway in between the normal # load distance and the unloading distance buildDistanceSquared = (self.minTileDistance + self.maxTileDistance) / 2 buildDistanceSquared = buildDistanceSquared * buildDistanceSquared for x in range (xstart - checkRadius, xstart + checkRadius, self.tileSize): for y in range (ystart - checkRadius, ystart + checkRadius, self.tileSize): if not (x, y) in self.tiles: deltaX = xpos - (x + halfTile) deltaY = ypos - (y + halfTile) distanceSquared = deltaX * deltaX + deltaY * deltaY if distanceSquared < buildDistanceSquared: self.buildQueue.append((x, y)) total = len(self.buildQueue) while len(self.buildQueue): if self.feedBackString: done = total - len(self.buildQueue) feedback = "Loading Terrain " + str(done) + "/" + str(total) logging.info(feedback) self.feedBackString.setText(feedback) tile = self.buildQueue.popleft() self._generateTile(tile) yield Task.cont self._setupSimpleTasks() yield Task.done def preload(self, xpos=1, ypos=1): """ """ logging.info("preloading terrain tiles...") # x and y start are rounded to the nearest multiple of tile size xstart = (int(xpos / self.horizontalScale) / self.tileSize) * self.tileSize ystart = (int(ypos / self.horizontalScale) / self.tileSize) * self.tileSize # check radius is rounded up to the nearest tile size from maxTileDistance # not every tile in checkRadius will be made checkRadius = (int(self.maxTileDistance) / self.tileSize + 1) * self.tileSize halfTile = self.tileSize * 0.5 # build distance for the preloader will be halfway in between the normal # load distance and the unloading distance buildDistanceSquared = (self.minTileDistance + self.maxTileDistance) / 2 buildDistanceSquared = buildDistanceSquared * buildDistanceSquared for x in range (xstart - checkRadius, xstart + checkRadius, self.tileSize): for y in range (ystart - checkRadius, ystart + checkRadius, self.tileSize): if not (x, y) in self.tiles: deltaX = xpos - (x + halfTile) deltaY = ypos - (y + halfTile) distanceSquared = deltaX * deltaX + deltaY * deltaY if distanceSquared < buildDistanceSquared: self.tileBuilder.preload((x, y)) self.preloadTotal = self.tileBuilder.queue.qsize() taskMgr.add(self.preloadWait, "preloadWaitTask") def preloadWait(self, task): #logging.info( "preloadWait()") if self.feedBackString: done = self.preloadTotal - self.tileBuilder.queue.qsize() feedback = "Loading Terrain " + str(done) + "/" + str(self.preloadTotal) logging.info(feedback) self.feedBackString.setText(feedback) #self.grabBuiltTile() if self.tileBuilder.queue.qsize() > 0: #logging.info( self.tileBuilder.queue.qsize()) return Task.cont self._setupSimpleTasks() return Task.done #@pstat def makeNewTile(self): """Generate the closest terrain tile needed.""" # tiles are placed under the terrain node path which may be scaled x = self.focus.getX(self) / self.horizontalScale y = self.focus.getY(self) / self.horizontalScale # start position is the focus position rounded to the multiple of tile size xstart = (int(x) / self.tileSize) * self.tileSize ystart = (int(y) / self.tileSize) * self.tileSize # radius is rounded up from minTileDistance to nearest multiple of tile size # not every tile within checkRadius will be made checkRadius = (int(self.minTileDistance) / self.tileSize + 1) * self.tileSize halfTile = self.tileSize * 0.49 tiles = self.tiles #logging.info( xstart, ystart, checkRadius) vec = 0 minFoundDistance = 9999999999.0 minDistanceSq = self.minTileDistance * self.minTileDistance for checkX in range (xstart - checkRadius, xstart + checkRadius, self.tileSize): for checkY in range (ystart - checkRadius, ystart + checkRadius, self.tileSize): if not (checkX, checkY) in tiles: deltaX = x - (checkX + halfTile) deltaY = y - (checkY + halfTile) distanceSq = deltaX * deltaX + deltaY * deltaY if distanceSq < minDistanceSq and distanceSq < minFoundDistance: minFoundDistance = distanceSq vec = (checkX, checkY) if not vec == 0: #logging.info( distance," < ",self.minTileDistance," and ",distance," < ",minDistance) #self.generateTile(vec.getX(), vec.getY()) if THREAD_LOAD_TERRAIN: self.dispatchTile(vec) else: self._generateTile(vec) #@pstat def dispatchTile(self, pos): """Creates a terrain tile at the input coordinates.""" if pos in self.storage: tile = self.storage[pos] self.tiles[pos] = tile tile.getRoot().reparentTo(self) del self.storage[pos] logging.info("tile recovered from storage at " + str(pos)) return self.tileBuilder.build(pos) self.tiles[pos] = 1 #@pstat def _generateTile(self, pos): """Creates a terrain tile at the input coordinates.""" if pos in self.storage: tile = self.storage[pos] self.tiles[pos] = tile tile.getRoot().reparentTo(self) del self.storage[pos] logging.info("tile recovered from storage at " + str(pos)) #self.flattenMedium() return if SAVED_TEXTURE_MAPS: tile = TextureMappedTerrainTile(self, pos[0], pos[1]) elif self.bruteForce: tile = LodTerrainTile(self, pos[0], pos[1]) else: tile = TerrainTile(self, pos[0], pos[1]) tile.make() tile.getRoot().reparentTo(self) self.tiles[pos] = tile logging.info("tile generated at " + str(pos)) #self.flattenMedium() return tile def grabBuiltTile(self): #logging.info( "grabBuiltTile()") tile = self.tileBuilder.grab() #logging.info( "tlie = "+ str(tile)) if tile: pos = (tile.xOffset, tile.yOffset) tile.getRoot().reparentTo(self) self.tiles[pos] = tile logging.info("tile generated at " + str(pos)) return tile return None #@pstat def removeOldTiles(self): """Remove distant tiles to free system resources.""" x = self.focus.getX(self) / self.horizontalScale y = self.focus.getY(self) / self.horizontalScale center = self.tileSize * 0.5 maxDistanceSquared = self.maxTileDistance * self.maxTileDistance for pos, tile in self.tiles.items(): deltaX = x - (pos[0] + center) deltaY = y - (pos[1] + center) distance = deltaX * deltaX + deltaY * deltaY if distance > maxDistanceSquared: #logging.info( distance+ " > "+ self.maxTileDistance * self.maxTileDistance) self.storeTile(pos) def storeTile(self, pos): tile = self.tiles[pos] if tile != 1: tile.getRoot().detachNode() self.storage[pos] = tile del self.tiles[pos] logging.info("Tile removed from " + str(pos)) def deleteTile(self, pos): """Removes a specific tile from the Terrain.""" self.tiles[pos].getRoot().detachNode() del self.tiles[pos] logging.info("Tile deleted from " + str(pos)) def getElevation(self, x, y): """Returns the height of the terrain at the input world coordinates.""" x /= self.horizontalScale y /= self.horizontalScale if SAVED_HEIGHT_MAPS: tilex = (int(x) / self.tileSize) * self.tileSize tiley = (int(y) / self.tileSize) * self.tileSize x -= tilex y -= tiley if (tilex, tiley) in self.tiles: return self.tiles[tilex, tiley].getElevation(x, y) * self.getSz() if (tilex, tiley) in self.storage: return self.storage[tilex, tiley].getElevation(x, y) * self.getSz() return self.getHeight(x, y) * self.getSz() def setWireFrame(self, state): self.wireFrame = state if state: self.setRenderModeWireframe() else: self.setRenderModeFilled() #for pos, tile in self.tiles.items(): # tile.setWireFrame(state) def toggleWireFrame(self): self.setWireFrame(not self.wireFrame) def test(self): self.texturer.test() def setShaderFloatInput(self, name, input): logging.info("set shader input " + name + " to " + str(input)) self.setShaderInput(name, PTAFloat([input])) def setFocus(self, nodePath): self.focus = nodePath for pos, tile in self.tiles.items(): tile.setFocalPoint(self.focus)