0

big commits to a bunch of stuff. See expanded message

Added an option to enter your own zoom level. Use -z to set the map at a
particular zoom level. Zoom levels define the width and height in tiles
of the highest zoom level, each new zoom level is twice as wide and
tall. (z=6 -> 2^6 tiles wide and tall)

Implemented tile re-arrangement on map expansion. Now most tiles will
get re-used if your map needs another zoom level! No longer does it need
to re-generate everything.

No longer creates empty directories for tiles, only creates directories
if needed.

Fixed some minor off-by-one logic (and the code that canceled it out to
make it work)
This commit is contained in:
Andrew Brown
2010-09-18 00:14:02 -04:00
parent 7d11f4ecef
commit c8c16d5fd3
2 changed files with 100 additions and 47 deletions

View File

@@ -21,6 +21,7 @@ def main():
cpus = 1 cpus = 1
parser = OptionParser(usage=helptext) parser = OptionParser(usage=helptext)
parser.add_option("-p", "--processes", dest="procs", help="How many chunks to render in parallel. A good number for this is the number of cores in your computer. Default %s" % cpus, default=cpus, action="store", type="int") parser.add_option("-p", "--processes", dest="procs", help="How many chunks to render in parallel. A good number for this is the number of cores in your computer. Default %s" % cpus, default=cpus, action="store", type="int")
parser.add_option("-z", "--zoom", dest="zoom", help="Sets the zoom level manually instead of calculating it. This can be useful if you have outlier chunks that make your world too big. This value will make the highest zoom level contain (2**ZOOM)^2 tiles", action="store", type="int")
parser.add_option("-d", "--delete", dest="delete", help="Clear all caches. Next time you render your world, it will have to start completely over again. This is probably not a good idea for large worlds. Use this if you change texture packs and want to re-render everything.", action="store_true") parser.add_option("-d", "--delete", dest="delete", help="Clear all caches. Next time you render your world, it will have to start completely over again. This is probably not a good idea for large worlds. Use this if you change texture packs and want to re-render everything.", action="store_true")
options, args = parser.parse_args() options, args = parser.parse_args()
@@ -43,7 +44,7 @@ def main():
w.go(options.procs) w.go(options.procs)
# Now generate the tiles # Now generate the tiles
q = quadtree.QuadtreeGen(w, destdir) q = quadtree.QuadtreeGen(w, destdir, depth=options.zoom)
q.go(options.procs) q.go(options.procs)
def delete_all(worlddir, tiledir): def delete_all(worlddir, tiledir):

View File

@@ -4,6 +4,8 @@ import os
import os.path import os.path
import hashlib import hashlib
import functools import functools
import re
import shutil
from PIL import Image from PIL import Image
@@ -33,30 +35,44 @@ def catch_keyboardinterrupt(func):
return newfunc return newfunc
class QuadtreeGen(object): class QuadtreeGen(object):
def __init__(self, worldobj, destdir): def __init__(self, worldobj, destdir, depth=None):
"""Generates a quadtree from the world given into the """Generates a quadtree from the world given into the
given dest directory given dest directory
worldobj is a world.WorldRenderer object that has already been processed worldobj is a world.WorldRenderer object that has already been processed
""" If depth is given, it overrides the calculated value. Otherwise, the
# Determine quadtree depth (midpoint is always 0,0) minimum depth that contains all chunks is calculated and used.
for p in xrange(15):
xdiameter = 2*2**p
ydiameter = 4*2**p
if xdiameter >= worldobj.maxcol and -xdiameter <= worldobj.mincol and \
ydiameter >= worldobj.maxrow and -ydiameter <= worldobj.minrow:
break
else:
raise ValueError("Your map is waaaay to big!")
self.p = p """
if depth is None:
# Determine quadtree depth (midpoint is always 0,0)
for p in xrange(15):
# Will 2^p tiles wide and high suffice?
# X has twice as many chunks as tiles, then halved since this is a
# radius
xradius = 2**p
# Y has 4 times as many chunks as tiles, then halved since this is
# a radius
yradius = 2*2**p
if xradius >= worldobj.maxcol and -xradius <= worldobj.mincol and \
yradius >= worldobj.maxrow and -yradius <= worldobj.minrow:
break
else:
raise ValueError("Your map is waaaay too big!")
self.p = p
else:
self.p = depth
xradius = 2**depth
yradius = 2*2**depth
# Make new row and column ranges # Make new row and column ranges
self.mincol = -xdiameter self.mincol = -xradius
self.maxcol = xdiameter self.maxcol = xradius
self.minrow = -ydiameter self.minrow = -yradius
self.maxrow = ydiameter self.maxrow = yradius
self.world = worldobj self.world = worldobj
self.destdir = destdir self.destdir = destdir
@@ -72,14 +88,22 @@ class QuadtreeGen(object):
output.write(html) output.write(html)
def _get_cur_depth(self): def _get_cur_depth(self):
"""How deep is the quadtree currently in the destdir? This assumes all """How deep is the quadtree currently in the destdir? This glances in
the directories are created, even if they don't have any images index.html to see what maxZoom is set to.
returns -1 if it couldn't be detected, file not found, or nothing in
index.html matched
""" """
p = 0 indexfile = os.path.join(self.destdir, "index.html")
curdir = os.path.join(self.destdir, "tiles") if not os.path.exists(indexfile):
while "0" in os.listdir(curdir): return -1
curdir = os.path.join(curdir, "0") matcher = re.compile(r"maxZoom:\s*(\d+)")
p += 1 p = -1
for line in open(indexfile, "r"):
res = matcher.search(line)
if res:
p = int(res.group(1))
break
print "detected previous zoom level:", p
return p return p
def _increase_depth(self): def _increase_depth(self):
@@ -108,16 +132,46 @@ class QuadtreeGen(object):
os.rename(p, getpath(newdir, newf)) os.rename(p, getpath(newdir, newf))
os.rename(newdirpath, getpath(str(dirnum))) os.rename(newdirpath, getpath(str(dirnum)))
def _decrease_depth(self):
"""If the map size decreases, or perhaps the user has a depth override
in effect, re-arrange existing tiles for a smaller tree"""
getpath = functools.partial(os.path.join, self.destdir, "tiles")
# quadrant 0/3 goes to 0
# 1/2 goes to 1
# 2/1 goes to 2
# 3/0 goes to 3
# Just worry about the directories here, the files at the top two
# levels are cheap enough to replace
os.rename(getpath("0", "3"), getpath("new0"))
os.rename(getpath("1", "2"), getpath("new1"))
os.rename(getpath("2", "1"), getpath("new2"))
os.rename(getpath("3", "0"), getpath("new3"))
shutil.rmtree(getpath("0"))
shutil.rmtree(getpath("1"))
shutil.rmtree(getpath("2"))
shutil.rmtree(getpath("3"))
os.rename(getpath("new0"), getpath("0"))
os.rename(getpath("new1"), getpath("1"))
os.rename(getpath("new2"), getpath("2"))
os.rename(getpath("new3"), getpath("3"))
def go(self, procs): def go(self, procs):
"""Renders all tiles""" """Renders all tiles"""
curdepth = self._get_cur_depth() curdepth = self._get_cur_depth()
if self.p > curdepth: if curdepth != -1:
print "Your map seemes to have expanded beyond its previous bounds." if self.p > curdepth:
print "Doing some tile re-arrangements... just a sec..." print "Your map seemes to have expanded beyond its previous bounds."
for _ in xrange(self.p-curdepth): print "Doing some tile re-arrangements... just a sec..."
print "Increasing depth..." for _ in xrange(self.p-curdepth):
self._increase_depth() self._increase_depth()
elif self.p < curdepth:
print "Your map seems to have shrunk. Re-arranging tiles, just a sec..."
for _ in xrange(curdepth - self.p):
self._decrease_depth()
# Create a pool # Create a pool
pool = multiprocessing.Pool(processes=procs) pool = multiprocessing.Pool(processes=procs)
@@ -126,7 +180,7 @@ class QuadtreeGen(object):
print "Computing the tile ranges and starting tile processers for inner-most tiles..." print "Computing the tile ranges and starting tile processers for inner-most tiles..."
print "This takes the longest. The other levels will go quicker" print "This takes the longest. The other levels will go quicker"
results = [] results = []
for path in iterate_base4(self.p+1): for path in iterate_base4(self.p):
# Get the range for this tile # Get the range for this tile
colstart, rowstart = self._get_range_by_path(path) colstart, rowstart = self._get_range_by_path(path)
colend = colstart + 2 colend = colstart + 2
@@ -135,11 +189,6 @@ class QuadtreeGen(object):
# This image is rendered at: # This image is rendered at:
dest = os.path.join(self.destdir, "tiles", *(str(x) for x in path)) dest = os.path.join(self.destdir, "tiles", *(str(x) for x in path))
# The directory, create it if not exists
dirdest = os.path.dirname(dest)
if not os.path.exists(dirdest):
os.makedirs(dirdest)
# And uses these chunks # And uses these chunks
tilechunks = self._get_chunks_in_range(colstart, colend, rowstart, tilechunks = self._get_chunks_in_range(colstart, colend, rowstart,
rowend) rowend)
@@ -153,7 +202,7 @@ class QuadtreeGen(object):
) )
) )
self.write_html(self.p+1) self.write_html(self.p)
# Wait for all results to finish # Wait for all results to finish
print "Rendering inner most zoom level tiles now!" print "Rendering inner most zoom level tiles now!"
@@ -161,15 +210,14 @@ class QuadtreeGen(object):
# get() instead of wait() so we can see errors # get() instead of wait() so we can see errors
result.get() result.get()
if i > 0 and (i % 100 == 0 or 100 % i == 0): if i > 0 and (i % 100 == 0 or 100 % i == 0):
print "{0}/{1} tiles complete on level {2}/{3}".format( print "{0}/{1} tiles complete on level 1/{2}".format(
i, len(results), 1, self.p+1) i, len(results), self.p)
print "Done" print "Done"
# Now do the other layers # Now do the other layers
for zoom in xrange(self.p, 0, -1): for zoom in xrange(self.p-1, 0, -1):
level = self.p+2-zoom level = self.p - zoom + 1
print "Preparing level", level print "Starting level", level
results = [] results = []
for path in iterate_base4(zoom): for path in iterate_base4(zoom):
# This image is rendered at: # This image is rendered at:
@@ -182,13 +230,12 @@ class QuadtreeGen(object):
) )
) )
print "Rendering level {0}/{1} now!".format(level, self.p+1)
for i, result in enumerate(results): for i, result in enumerate(results):
# get() instead of wait() so we can see errors # get() instead of wait() so we can see errors
result.get() result.get()
if i > 0 and (i % 100 == 0 or 100 % i == 0): if i > 0 and (i % 100 == 0 or 100 % i == 0):
print "{0}/{1} tiles complete on level {2}/{3}".format( print "{0}/{1} tiles complete for level {2}/{3}".format(
i, len(results), level, self.p+1) i, len(results), level, self.p)
print "Done" print "Done"
# Do the final one right here: # Do the final one right here:
@@ -274,7 +321,7 @@ def render_innertile(dest, name):
if os.path.exists(hashpath): if os.path.exists(hashpath):
os.unlink(hashpath) os.unlink(hashpath)
return return
# Now check the hashes # Now check the hashes
hasher = hashlib.md5() hasher = hashlib.md5()
if q0hash: if q0hash:
@@ -378,6 +425,11 @@ def render_worldtile(chunks, colstart, colend, rowstart, rowend, path):
os.unlink(hashpath) os.unlink(hashpath)
return None return None
# Create the directory if not exists
dirdest = os.path.dirname(path)
if not os.path.exists(dirdest):
os.makedirs(dirdest)
imghash = hashlib.md5() imghash = hashlib.md5()
for col, row, chunkfile in chunks: for col, row, chunkfile in chunks:
# Get the hash of this image and add it to our hash for this tile # Get the hash of this image and add it to our hash for this tile