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
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("-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")
options, args = parser.parse_args()
@@ -43,7 +44,7 @@ def main():
w.go(options.procs)
# Now generate the tiles
q = quadtree.QuadtreeGen(w, destdir)
q = quadtree.QuadtreeGen(w, destdir, depth=options.zoom)
q.go(options.procs)
def delete_all(worlddir, tiledir):

View File

@@ -4,6 +4,8 @@ import os
import os.path
import hashlib
import functools
import re
import shutil
from PIL import Image
@@ -33,30 +35,44 @@ def catch_keyboardinterrupt(func):
return newfunc
class QuadtreeGen(object):
def __init__(self, worldobj, destdir):
def __init__(self, worldobj, destdir, depth=None):
"""Generates a quadtree from the world given into the
given dest directory
worldobj is a world.WorldRenderer object that has already been processed
"""
# Determine quadtree depth (midpoint is always 0,0)
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!")
If depth is given, it overrides the calculated value. Otherwise, the
minimum depth that contains all chunks is calculated and used.
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
self.mincol = -xdiameter
self.maxcol = xdiameter
self.minrow = -ydiameter
self.maxrow = ydiameter
self.mincol = -xradius
self.maxcol = xradius
self.minrow = -yradius
self.maxrow = yradius
self.world = worldobj
self.destdir = destdir
@@ -72,14 +88,22 @@ class QuadtreeGen(object):
output.write(html)
def _get_cur_depth(self):
"""How deep is the quadtree currently in the destdir? This assumes all
the directories are created, even if they don't have any images
"""How deep is the quadtree currently in the destdir? This glances in
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
curdir = os.path.join(self.destdir, "tiles")
while "0" in os.listdir(curdir):
curdir = os.path.join(curdir, "0")
p += 1
indexfile = os.path.join(self.destdir, "index.html")
if not os.path.exists(indexfile):
return -1
matcher = re.compile(r"maxZoom:\s*(\d+)")
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
def _increase_depth(self):
@@ -108,16 +132,46 @@ class QuadtreeGen(object):
os.rename(p, getpath(newdir, newf))
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):
"""Renders all tiles"""
curdepth = self._get_cur_depth()
if self.p > curdepth:
print "Your map seemes to have expanded beyond its previous bounds."
print "Doing some tile re-arrangements... just a sec..."
for _ in xrange(self.p-curdepth):
print "Increasing depth..."
self._increase_depth()
if curdepth != -1:
if self.p > curdepth:
print "Your map seemes to have expanded beyond its previous bounds."
print "Doing some tile re-arrangements... just a sec..."
for _ in xrange(self.p-curdepth):
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
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 "This takes the longest. The other levels will go quicker"
results = []
for path in iterate_base4(self.p+1):
for path in iterate_base4(self.p):
# Get the range for this tile
colstart, rowstart = self._get_range_by_path(path)
colend = colstart + 2
@@ -135,11 +189,6 @@ class QuadtreeGen(object):
# This image is rendered at:
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
tilechunks = self._get_chunks_in_range(colstart, colend, rowstart,
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
print "Rendering inner most zoom level tiles now!"
@@ -161,15 +210,14 @@ class QuadtreeGen(object):
# get() instead of wait() so we can see errors
result.get()
if i > 0 and (i % 100 == 0 or 100 % i == 0):
print "{0}/{1} tiles complete on level {2}/{3}".format(
i, len(results), 1, self.p+1)
print "{0}/{1} tiles complete on level 1/{2}".format(
i, len(results), self.p)
print "Done"
# Now do the other layers
for zoom in xrange(self.p, 0, -1):
level = self.p+2-zoom
print "Preparing level", level
for zoom in xrange(self.p-1, 0, -1):
level = self.p - zoom + 1
print "Starting level", level
results = []
for path in iterate_base4(zoom):
# 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):
# get() instead of wait() so we can see errors
result.get()
if i > 0 and (i % 100 == 0 or 100 % i == 0):
print "{0}/{1} tiles complete on level {2}/{3}".format(
i, len(results), level, self.p+1)
print "{0}/{1} tiles complete for level {2}/{3}".format(
i, len(results), level, self.p)
print "Done"
# Do the final one right here:
@@ -274,7 +321,7 @@ def render_innertile(dest, name):
if os.path.exists(hashpath):
os.unlink(hashpath)
return
# Now check the hashes
hasher = hashlib.md5()
if q0hash:
@@ -378,6 +425,11 @@ def render_worldtile(chunks, colstart, colend, rowstart, rowend, path):
os.unlink(hashpath)
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()
for col, row, chunkfile in chunks:
# Get the hash of this image and add it to our hash for this tile