From f0d6776ad932f2c2352242ecde427d674799ae1f Mon Sep 17 00:00:00 2001 From: Alex Jurkiewicz Date: Mon, 27 Sep 2010 21:51:24 +1000 Subject: [PATCH 01/30] Add JPEG output support. --- gmap.py | 14 +++++++++++--- quadtree.py | 48 +++++++++++++++++++++++++++--------------------- 2 files changed, 38 insertions(+), 24 deletions(-) diff --git a/gmap.py b/gmap.py index 97eeb97..00c7836 100755 --- a/gmap.py +++ b/gmap.py @@ -32,8 +32,7 @@ import quadtree helptext = """ %prog [OPTIONS] -%prog -d [tiles dest dir] -""" +%prog -d [tiles dest dir]""" def main(): try: @@ -46,6 +45,7 @@ def main(): 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("--cachedir", dest="cachedir", help="Sets the directory where the Overviewer will save chunk images, which is an intermediate step before the tiles are generated. You must use the same directory each time to gain any benefit from the cache. If not set, this defaults to your world directory.") parser.add_option("--chunklist", dest="chunklist", help="A file containing, on each line, a path to a chunkfile to update. Instead of scanning the world directory for chunks, it will just use this list. Normal caching rules still apply.") + parser.add_option("--imgformat", dest="imgformat", help="The image output format to use. Currently supported: png(default), jpg. NOTE: png will always be used as the intermediate image format.") options, args = parser.parse_args() @@ -85,12 +85,20 @@ def main(): else: chunklist = None + if options.imgformat: + if options.imgformat not in ('jpg','png'): + parser.error("Unknown imgformat!") + else: + imgformat = options.imgformat + else: + imgformat = 'png' + # First generate the world's chunk images w = world.WorldRenderer(worlddir, cachedir, chunklist=chunklist) w.go(options.procs) # Now generate the tiles - q = quadtree.QuadtreeGen(w, destdir, depth=options.zoom) + q = quadtree.QuadtreeGen(w, destdir, depth=options.zoom, imgformat=imgformat) q.go(options.procs) def delete_all(worlddir, tiledir): diff --git a/quadtree.py b/quadtree.py index a075e77..fc65f2d 100644 --- a/quadtree.py +++ b/quadtree.py @@ -52,7 +52,7 @@ def catch_keyboardinterrupt(func): return newfunc class QuadtreeGen(object): - def __init__(self, worldobj, destdir, depth=None): + def __init__(self, worldobj, destdir, depth=None, imgformat=None): """Generates a quadtree from the world given into the given dest directory @@ -62,6 +62,9 @@ class QuadtreeGen(object): minimum depth that contains all chunks is calculated and used. """ + assert(imgformat) + self.imgformat = imgformat + if depth is None: # Determine quadtree depth (midpoint is always 0,0) for p in xrange(15): @@ -159,8 +162,8 @@ class QuadtreeGen(object): newdir = "new" + str(dirnum) newdirpath = getpath(newdir) - files = [str(dirnum)+".png", str(dirnum)+".hash", str(dirnum)] - newfiles = [str(newnum)+".png", str(newnum)+".hash", str(newnum)] + files = [str(dirnum)+"."+imgformat, str(dirnum)+".hash", str(dirnum)] + newfiles = [str(newnum)+"."+imgformat, str(newnum)+".hash", str(newnum)] os.mkdir(newdirpath) for f, newf in zip(files, newfiles): @@ -221,7 +224,7 @@ class QuadtreeGen(object): # (even if tilechunks is empty, render_worldtile will delete # existing images if appropriate) yield pool.apply_async(func=render_worldtile, args= (tilechunks, - colstart, colend, rowstart, rowend, dest)) + colstart, colend, rowstart, rowend, dest, self.imgformat)) def _apply_render_inntertile(self, pool, zoom): """Same as _apply_render_worltiles but for the inntertile routine. @@ -233,7 +236,7 @@ class QuadtreeGen(object): dest = os.path.join(self.destdir, "tiles", *(str(x) for x in path[:-1])) name = str(path[-1]) - yield pool.apply_async(func=render_innertile, args= (dest, name)) + yield pool.apply_async(func=render_innertile, args= (dest, name, self.imgformat)) def go(self, procs): """Renders all tiles""" @@ -317,7 +320,7 @@ class QuadtreeGen(object): pool.join() # Do the final one right here: - render_innertile(os.path.join(self.destdir, "tiles"), "base") + render_innertile(os.path.join(self.destdir, "tiles"), "base", self.imgformat) def _get_range_by_path(self, path): """Returns the x, y chunk coordinates of this tile""" @@ -348,28 +351,28 @@ class QuadtreeGen(object): return chunklist @catch_keyboardinterrupt -def render_innertile(dest, name): +def render_innertile(dest, name, imgformat): """ - Renders a tile at os.path.join(dest, name)+".png" by taking tiles from + Renders a tile at os.path.join(dest, name)+".ext" by taking tiles from os.path.join(dest, name, "{0,1,2,3}.png") """ - imgpath = os.path.join(dest, name) + ".png" + imgpath = os.path.join(dest, name) + "." + imgformat hashpath = os.path.join(dest, name) + ".hash" if name == "base": - q0path = os.path.join(dest, "0.png") - q1path = os.path.join(dest, "1.png") - q2path = os.path.join(dest, "2.png") - q3path = os.path.join(dest, "3.png") + q0path = os.path.join(dest, "0." + imgformat) + q1path = os.path.join(dest, "1." + imgformat) + q2path = os.path.join(dest, "2." + imgformat) + q3path = os.path.join(dest, "3." + imgformat) q0hash = os.path.join(dest, "0.hash") q1hash = os.path.join(dest, "1.hash") q2hash = os.path.join(dest, "2.hash") q3hash = os.path.join(dest, "3.hash") else: - q0path = os.path.join(dest, name, "0.png") - q1path = os.path.join(dest, name, "1.png") - q2path = os.path.join(dest, name, "2.png") - q3path = os.path.join(dest, name, "3.png") + q0path = os.path.join(dest, name, "0." + imgformat) + q1path = os.path.join(dest, name, "1." + imgformat) + q2path = os.path.join(dest, name, "2." + imgformat) + q3path = os.path.join(dest, name, "3." + imgformat) q0hash = os.path.join(dest, name, "0.hash") q1hash = os.path.join(dest, name, "1.hash") q2hash = os.path.join(dest, name, "2.hash") @@ -434,13 +437,16 @@ def render_innertile(dest, name): img.paste(quad3, (192, 192)) # Save it - img.save(imgpath) + if imgformat == 'jpg': + img.save(imgpath, quality=95, optimize=True) + else: # png + img.save(imgpath) with open(hashpath, "wb") as hashout: hashout.write(newhash) @catch_keyboardinterrupt -def render_worldtile(chunks, colstart, colend, rowstart, rowend, path): +def render_worldtile(chunks, colstart, colend, rowstart, rowend, path, imgformat): """Renders just the specified chunks into a tile and save it. Unlike usual python conventions, rowend and colend are inclusive. Additionally, the chunks around the edges are half-way cut off (so that neighboring tiles @@ -449,7 +455,7 @@ def render_worldtile(chunks, colstart, colend, rowstart, rowend, path): chunks is a list of (col, row, filename) of chunk images that are relevant to this call - The image is saved to path+".png" and a hash is saved to path+".hash" + The image is saved to path+".ext" and a hash is saved to path+".hash" If there are no chunks, this tile is not saved (if it already exists, it is deleted) @@ -490,7 +496,7 @@ def render_worldtile(chunks, colstart, colend, rowstart, rowend, path): # Before we render any tiles, check the hash of each image in this tile to # see if it's changed. hashpath = path + ".hash" - imgpath = path + ".png" + imgpath = path + "." + imgformat if not chunks: # No chunks were found in this tile From 244679a8772a180539b320dcff270b497cb4be21 Mon Sep 17 00:00:00 2001 From: Alex Jurkiewicz Date: Mon, 27 Sep 2010 22:09:58 +1000 Subject: [PATCH 02/30] Update README with --imgformat details. --- README.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.rst b/README.rst index 007de92..be5e232 100644 --- a/README.rst +++ b/README.rst @@ -121,6 +121,12 @@ Options python gmap.py --cachedir= +--imgformat=FORMAT + Set the output image format used for the tiles. The default is 'png', + but 'jpg' is also supported. Note that regardless of what you choose, + Overviewer will still use PNG for cached images to avoid recompression + artifacts. + -p PROCS, --processes=PROCS Adding the "-p" option will utilize more cores during processing. This can speed up rendering quite a bit. The default is set to the same From e6bfcc30333bc394affb1eba383e7af1745475d0 Mon Sep 17 00:00:00 2001 From: Alex Jurkiewicz Date: Mon, 27 Sep 2010 22:10:23 +1000 Subject: [PATCH 03/30] Write HTML file with appropriate extension config. --- quadtree.py | 6 ++++-- template.html | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/quadtree.py b/quadtree.py index fc65f2d..36d6818 100644 --- a/quadtree.py +++ b/quadtree.py @@ -112,13 +112,15 @@ class QuadtreeGen(object): print "{0}/{1} tiles complete on level {2}/{3}".format( complete, total, level, self.p) - def write_html(self, zoomlevel): + def write_html(self, zoomlevel, imgformat): """Writes out index.html""" templatepath = os.path.join(os.path.split(__file__)[0], "template.html") html = open(templatepath, 'r').read() html = html.replace( "{maxzoom}", str(zoomlevel)) + html = html.replace( + "{imgformat}", str(imgformat)) with open(os.path.join(self.destdir, "index.html"), 'w') as output: output.write(html) @@ -263,7 +265,7 @@ class QuadtreeGen(object): else: pool = multiprocessing.Pool(processes=procs) - self.write_html(self.p) + self.write_html(self.p, self.imgformat) # Render the highest level of tiles from the chunks results = collections.deque() diff --git a/template.html b/template.html index ef2868f..e6105c1 100644 --- a/template.html +++ b/template.html @@ -14,7 +14,7 @@ From aa8a369aa80f56b3e86b38da986d638997504845 Mon Sep 17 00:00:00 2001 From: Andrew Chin Date: Mon, 27 Sep 2010 21:32:26 -0400 Subject: [PATCH 08/30] Better cacti rendering --- chunk.py | 2 +- textures.py | 26 ++++++++++++++++++-------- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/chunk.py b/chunk.py index c548afa..0b965f7 100644 --- a/chunk.py +++ b/chunk.py @@ -62,7 +62,7 @@ def get_skylight_array(level): # This set holds blocks ids that can be seen through, for occlusion calculations transparent_blocks = set([0, 6, 8, 9, 18, 20, 37, 38, 39, 40, 50, 51, 52, 53, - 59, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 74, 75, 76, 77, 79, 83, 85]) + 59, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 74, 75, 76, 77, 79, 81, 83, 85]) def render_and_save(chunkfile, cachedir, cave=False): """Used as the entry point for the multiprocessing workers (since processes diff --git a/textures.py b/textures.py index 6237447..cc338ea 100644 --- a/textures.py +++ b/textures.py @@ -112,15 +112,20 @@ def _split_terrain(terrain): # This maps terainids to 16x16 images terrain_images = _split_terrain(_get_terrain_image()) -def _transform_image(img): +def _transform_image(img, texID): """Takes a PIL image and rotates it left 45 degrees and shrinks the y axis by a factor of 2. Returns the resulting image, which will be 24x12 pixels """ - # Resize to 17x17, since the diagonal is approximately 24 pixels, a nice - # even number that can be split in half twice - img = img.resize((17, 17), Image.BILINEAR) + if texID in (69,): # cacti + # Resize to 15x15, since the cactus texture is a little smaller than the other textures + img = img.resize((15, 15), Image.BILINEAR) + + else: + # Resize to 17x17, since the diagonal is approximately 24 pixels, a nice + # even number that can be split in half twice + img = img.resize((17, 17), Image.BILINEAR) # Build the Affine transformation matrix for this perspective transform = numpy.matrix(numpy.identity(3)) @@ -164,7 +169,7 @@ def _build_block(top, side, texID=None): """ img = Image.new("RGBA", (24,24), (38,92,255,0)) - top = _transform_image(top) + top = _transform_image(top, texID) if not side: img.paste(top, (0,0), top) @@ -192,9 +197,14 @@ def _build_block(top, side, texID=None): img.paste(otherside, (6,3), otherside) return img - img.paste(side, (0,6), side) - img.paste(otherside, (12,6), otherside) - img.paste(top, (0,0), top) + if texID in (69,): # cacti! + img.paste(side, (2,6), side) + img.paste(otherside, (10,6), otherside) + img.paste(top, (0,2), top) + else: + img.paste(side, (0,6), side) + img.paste(otherside, (12,6), otherside) + img.paste(top, (0,0), top) # Manually touch up 6 pixels that leave a gap because of how the # shearing works out. This makes the blocks perfectly tessellate-able From 6240f40226b6063bc9d8f8357d36842c859aa922 Mon Sep 17 00:00:00 2001 From: Andrew Chin Date: Tue, 28 Sep 2010 00:09:50 -0400 Subject: [PATCH 09/30] Half blocks (step/#44) should be rendered reasonably well. Tested 1 half block (44), 2 half blocks (43), a 3 half blocks (43,44) and all seem to look OK. --- chunk.py | 2 +- textures.py | 42 +++++++++++++++++++++++++++++++----------- 2 files changed, 32 insertions(+), 12 deletions(-) diff --git a/chunk.py b/chunk.py index 0b965f7..47d1dc8 100644 --- a/chunk.py +++ b/chunk.py @@ -61,7 +61,7 @@ def get_skylight_array(level): return numpy.frombuffer(level['SkyLight'], dtype=numpy.uint8).reshape((16,16,64)) # This set holds blocks ids that can be seen through, for occlusion calculations -transparent_blocks = set([0, 6, 8, 9, 18, 20, 37, 38, 39, 40, 50, 51, 52, 53, +transparent_blocks = set([0, 6, 8, 9, 18, 20, 37, 38, 39, 40, 44, 50, 51, 52, 53, 59, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 74, 75, 76, 77, 79, 81, 83, 85]) def render_and_save(chunkfile, cachedir, cave=False): diff --git a/textures.py b/textures.py index cc338ea..966dee4 100644 --- a/textures.py +++ b/textures.py @@ -112,13 +112,13 @@ def _split_terrain(terrain): # This maps terainids to 16x16 images terrain_images = _split_terrain(_get_terrain_image()) -def _transform_image(img, texID): +def _transform_image(img, blockID): """Takes a PIL image and rotates it left 45 degrees and shrinks the y axis by a factor of 2. Returns the resulting image, which will be 24x12 pixels """ - if texID in (69,): # cacti + if blockID in (81,): # cacti # Resize to 15x15, since the cactus texture is a little smaller than the other textures img = img.resize((15, 15), Image.BILINEAR) @@ -145,10 +145,19 @@ def _transform_image(img, texID): newimg = img.transform((24,12), Image.AFFINE, transform) return newimg -def _transform_image_side(img): +def _transform_image_side(img, blockID): """Takes an image and shears it for the left side of the cube (reflect for the right side)""" + if blockID in (44,): # step block + # make the top half transpartent + # (don't just crop img, since we want the size of + # img to be unchanged + mask = img.crop((0,8,16,16)) + n = Image.new(img.mode, img.size, (38,92,255,0)) + n.paste(mask,(0,0,16,8), mask) + img = n + # Size of the cube side before shear img = img.resize((12,12)) @@ -162,20 +171,20 @@ def _transform_image_side(img): return newimg -def _build_block(top, side, texID=None): +def _build_block(top, side, blockID=None): """From a top texture and a side texture, build a block image. top and side should be 16x16 image objects. Returns a 24x24 image """ img = Image.new("RGBA", (24,24), (38,92,255,0)) - top = _transform_image(top, texID) + top = _transform_image(top, blockID) if not side: img.paste(top, (0,0), top) return img - side = _transform_image_side(side) + side = _transform_image_side(side, blockID) otherside = side.transpose(Image.FLIP_LEFT_RIGHT) @@ -190,17 +199,23 @@ def _build_block(top, side, texID=None): otherside.putalpha(othersidealpha) ## special case for non-block things - if texID in (12,13,15,28,29,80,73): ## flowers, sapling, mushrooms, regular torch, reeds + if blockID in (37,38,6,39,40,50,83): ## flowers, sapling, mushrooms, regular torch, reeds # instead of pasting these blocks at the cube edges, place them in the middle: # and omit the top img.paste(side, (6,3), side) img.paste(otherside, (6,3), otherside) return img - if texID in (69,): # cacti! + + if blockID in (81,): # cacti! img.paste(side, (2,6), side) img.paste(otherside, (10,6), otherside) img.paste(top, (0,2), top) + elif blockID in (44,): # half step + # shift each texture down 6 pixels + img.paste(side, (0,12), side) + img.paste(otherside, (12,12), otherside) + img.paste(top, (0,6), top) else: img.paste(side, (0,6), side) img.paste(otherside, (12,6), otherside) @@ -249,7 +264,7 @@ def _build_blockimages(): # 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 34, 20, 52, 48, 49, -1, -1, -1, -1, -1, -1, -1,- 1, -1, -1, -1, # 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 - -1, -1, -1, 64, 64, 13, 12, 29, 28, 23, 22, 6, 6, 7, 8, 35, + -1, -1, -1, 64, 64, 13, 12, 29, 28, 23, 22, 5, 5, 7, 8, 35, # 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 36, 37, 80, -1, 65, 4, 25,101, 98, 24, 43, -1, 86, 1, 1, -1, # 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 @@ -259,8 +274,11 @@ def _build_blockimages(): ] # This maps block id to the texture that goes on the side of the block + if len(topids) != len(sideids): + raise Exception("mismatched lengths") + allimages = [] - for toptextureid, sidetextureid in zip(topids, sideids): + for toptextureid, sidetextureid,blockID in zip(topids, sideids,range(len(topids))): if toptextureid == -1 or sidetextureid == -1: allimages.append(None) continue @@ -268,7 +286,9 @@ def _build_blockimages(): toptexture = terrain_images[toptextureid] sidetexture = terrain_images[sidetextureid] - img = _build_block(toptexture, sidetexture, toptextureid) + ## _build_block needs to know about the block ID, not just the textures + ## of the block or the texture ID + img = _build_block(toptexture, sidetexture, blockID) allimages.append((img.convert("RGB"), img.split()[3])) From 1f8d8b1343fc928ea5d022b74df6930139b58a95 Mon Sep 17 00:00:00 2001 From: Alex Jurkiewicz Date: Tue, 28 Sep 2010 17:15:26 +1000 Subject: [PATCH 10/30] Disable jpeg image optimization to work around a PIL bug. Ref: http://mail.python.org/pipermail/image-sig/1999-August/000816.html --- quadtree.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quadtree.py b/quadtree.py index 8a55c4c..3fa32b8 100644 --- a/quadtree.py +++ b/quadtree.py @@ -440,7 +440,7 @@ def render_innertile(dest, name, imgformat): # Save it if imgformat == 'jpg': - img.save(imgpath, quality=95, optimize=True, subsampling=0) + img.save(imgpath, quality=95, subsampling=0) else: # png img.save(imgpath) with open(hashpath, "wb") as hashout: From ac7908e8151176b2a02daf7d9e3ed6d0ffd6bf3c Mon Sep 17 00:00:00 2001 From: Andrew Brown Date: Tue, 28 Sep 2010 22:35:13 -0400 Subject: [PATCH 11/30] readme update with parallel pngcrush recipe --- README.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.rst b/README.rst index 007de92..26a9498 100644 --- a/README.rst +++ b/README.rst @@ -209,6 +209,10 @@ render for my world from 85M to 67M. find /path/to/destination -name "*.png" -exec pngcrush {} {}.crush \; -exec mv {}.crush {} \; +Or if you prefer a more parallel solution, try something like this:: + + find /path/to/destination -print0 | xargs -0 -n 1 -P sh -c 'pngcrush $0 temp.$$ && mv temp.$$ $0' + If you're on Windows, I've gotten word that this command line snippet works provided pngout is installed and on your path. Note that the % symbols will need to be doubled up if this is in a batch file. From 81d86d6b8b6315de51520b88acb060252a2d81ac Mon Sep 17 00:00:00 2001 From: Andrew Brown Date: Tue, 28 Sep 2010 22:54:39 -0400 Subject: [PATCH 12/30] added the long awaited blank image to the output tiles. --- quadtree.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/quadtree.py b/quadtree.py index 3fa32b8..553164e 100644 --- a/quadtree.py +++ b/quadtree.py @@ -130,6 +130,10 @@ class QuadtreeGen(object): with open(os.path.join(self.destdir, "markers.js"), 'w') as output: output.write("var markerData=%s" % json.dumps(self.world.POI)) + # Write a blank image + blank = Image.new("RGBA", (1,1)) + blank.save(os.path.join(self.destdir, "tiles", "blank."+self.imgformat)) + def _get_cur_depth(self): """How deep is the quadtree currently in the destdir? This glances in index.html to see what maxZoom is set to. From 7a696fcee01891ef114623f126257d952a2b0a40 Mon Sep 17 00:00:00 2001 From: Andrew Brown Date: Tue, 28 Sep 2010 23:04:21 -0400 Subject: [PATCH 13/30] modified to support freezing --- gmap.py | 1 + quadtree.py | 4 +++- textures.py | 4 +++- util.py | 29 +++++++++++++++++++++++++++++ 4 files changed, 36 insertions(+), 2 deletions(-) create mode 100644 util.py diff --git a/gmap.py b/gmap.py index 00c7836..7d3f165 100755 --- a/gmap.py +++ b/gmap.py @@ -140,4 +140,5 @@ def list_worlds(): if __name__ == "__main__": + multiprocessing.freeze_support() main() diff --git a/quadtree.py b/quadtree.py index 553164e..161cb43 100644 --- a/quadtree.py +++ b/quadtree.py @@ -26,6 +26,8 @@ import json from PIL import Image +import util + """ This module has routines related to generating a quadtree of tiles @@ -114,7 +116,7 @@ class QuadtreeGen(object): def write_html(self, zoomlevel, imgformat): """Writes out index.html""" - templatepath = os.path.join(os.path.split(__file__)[0], "template.html") + templatepath = os.path.join(util.get_program_path(), "template.html") html = open(templatepath, 'r').read() html = html.replace( diff --git a/textures.py b/textures.py index 966dee4..8d34b03 100644 --- a/textures.py +++ b/textures.py @@ -23,6 +23,8 @@ import math import numpy from PIL import Image, ImageEnhance +import util + def _find_file(filename, mode="rb"): """Searches for the given file and returns an open handle to it. This searches the following locations in this order: @@ -39,7 +41,7 @@ def _find_file(filename, mode="rb"): * The program dir / textures """ - programdir = os.path.dirname(__file__) + programdir = util.get_program_path() path = os.path.join(programdir, filename) if os.path.exists(path): return open(path, mode) diff --git a/util.py b/util.py new file mode 100644 index 0000000..46d1639 --- /dev/null +++ b/util.py @@ -0,0 +1,29 @@ +# This file is part of the Minecraft Overviewer. +# +# Minecraft Overviewer is free software: you can redistribute it and/or +# modify it under the terms of the GNU General Public License as published +# by the Free Software Foundation, either version 3 of the License, or (at +# your option) any later version. +# +# Minecraft Overviewer is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General +# Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with the Overviewer. If not, see . + +""" +Misc utility routines used by multiple files that don't belong anywhere else +""" + +import imp +import os +import os.path +import sys + +def get_program_path(): + if hasattr(sys, "frozen") or imp.is_frozen("__main__"): + return os.path.dirname(sys.executable) + else: + return os.path.dirname(sys.argv[0]) From fec7f1b8e4fa9b61373f19231a62c89dcd866f7b Mon Sep 17 00:00:00 2001 From: Andrew Brown Date: Tue, 28 Sep 2010 23:13:13 -0400 Subject: [PATCH 14/30] Added a setup.py for py2exe --- setup.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 setup.py diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..49d1d22 --- /dev/null +++ b/setup.py @@ -0,0 +1,13 @@ +from distutils.core import setup +import py2exe + +setup(console=['gmap.py'], + data_files=[('textures', ['textures/lava.png', 'textures/water.png']), + ('', ['template.html'])], + zipfile = None, + options = {'py2exe': { + 'bundle_files': 1, + }}, + + + ) From 055e11719ff76577e4418ebeb40cbd47e06de027 Mon Sep 17 00:00:00 2001 From: Andrew Chin Date: Wed, 29 Sep 2010 10:05:46 -0400 Subject: [PATCH 15/30] Create the tiles dir, if necessary, before writing blank.png. Otherwise an error is raised, since .save() won't create intermediate directories --- quadtree.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/quadtree.py b/quadtree.py index 161cb43..8733881 100644 --- a/quadtree.py +++ b/quadtree.py @@ -134,7 +134,9 @@ class QuadtreeGen(object): # Write a blank image blank = Image.new("RGBA", (1,1)) - blank.save(os.path.join(self.destdir, "tiles", "blank."+self.imgformat)) + tileDir = os.path.join(self.destdir, "tiles") + if not os.path.exists(tileDir): os.mkdir(tileDir) + blank.save(os.path.join(tileDir, "blank."+self.imgformat)) def _get_cur_depth(self): """How deep is the quadtree currently in the destdir? This glances in From 971f33e763bc31f3b728f412fec0006b3da7f520 Mon Sep 17 00:00:00 2001 From: Michael Jensen Date: Thu, 30 Sep 2010 09:31:57 +1000 Subject: [PATCH 16/30] Fixed a documentation mistake, the chunkdir option is actually cachedir --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 4f7d195..a44cfed 100644 --- a/README.rst +++ b/README.rst @@ -100,7 +100,7 @@ it. The Overviewer will put a cached image for every chunk *directly in your world directory by default*. If you do not like this behavior, you can specify -another location with the --chunkdir option. See below for details. +another location with the --cachedir option. See below for details. Options ------- From 5a14f1b486892b7ba0882356faa3963151ea3a34 Mon Sep 17 00:00:00 2001 From: Stephen Fluin Date: Wed, 29 Sep 2010 21:17:22 -0500 Subject: [PATCH 17/30] Fixed scoping for imageformat --- quadtree.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/quadtree.py b/quadtree.py index 8733881..03f6672 100644 --- a/quadtree.py +++ b/quadtree.py @@ -172,8 +172,8 @@ class QuadtreeGen(object): newdir = "new" + str(dirnum) newdirpath = getpath(newdir) - files = [str(dirnum)+"."+imgformat, str(dirnum)+".hash", str(dirnum)] - newfiles = [str(newnum)+"."+imgformat, str(newnum)+".hash", str(newnum)] + files = [str(dirnum)+"."+self.imgformat, str(dirnum)+".hash", str(dirnum)] + newfiles = [str(newnum)+"."+self.imgformat, str(newnum)+".hash", str(newnum)] os.mkdir(newdirpath) for f, newf in zip(files, newfiles): From fd43331350b285e7c3f0a60e6cbd5c6e5b2631e8 Mon Sep 17 00:00:00 2001 From: Andrew Brown Date: Wed, 29 Sep 2010 23:31:19 -0400 Subject: [PATCH 18/30] removed unused imports --- chunk.py | 1 - world.py | 1 - 2 files changed, 2 deletions(-) diff --git a/chunk.py b/chunk.py index 47d1dc8..fef3a84 100644 --- a/chunk.py +++ b/chunk.py @@ -15,7 +15,6 @@ import numpy from PIL import Image, ImageDraw -from itertools import izip, count import os.path import hashlib diff --git a/world.py b/world.py index 99728c1..3ee4b5b 100644 --- a/world.py +++ b/world.py @@ -20,7 +20,6 @@ import multiprocessing import sys import numpy -from PIL import Image import chunk import nbt From 9f49bf3d774b97e594b552ebbb01e1a87f7e1136 Mon Sep 17 00:00:00 2001 From: Alex Jurkiewicz Date: Thu, 30 Sep 2010 14:35:37 +1000 Subject: [PATCH 19/30] Change logging to use the 'logging' module. --- gmap.py | 17 +++++++++++++++-- quadtree.py | 36 ++++++++++++++++++------------------ world.py | 18 +++++++++++------- 3 files changed, 44 insertions(+), 27 deletions(-) diff --git a/gmap.py b/gmap.py index 7d3f165..aad5959 100755 --- a/gmap.py +++ b/gmap.py @@ -26,6 +26,9 @@ from optparse import OptionParser import re import multiprocessing import time +import logging + +logging.basicConfig(level=logging.INFO,format="%(asctime)s [%(levelname)s] %(message)s") import world import quadtree @@ -46,6 +49,8 @@ def main(): parser.add_option("--cachedir", dest="cachedir", help="Sets the directory where the Overviewer will save chunk images, which is an intermediate step before the tiles are generated. You must use the same directory each time to gain any benefit from the cache. If not set, this defaults to your world directory.") parser.add_option("--chunklist", dest="chunklist", help="A file containing, on each line, a path to a chunkfile to update. Instead of scanning the world directory for chunks, it will just use this list. Normal caching rules still apply.") parser.add_option("--imgformat", dest="imgformat", help="The image output format to use. Currently supported: png(default), jpg. NOTE: png will always be used as the intermediate image format.") + parser.add_option("-q", "--quiet", dest="quiet", action="count", default=0, help="Print less output. You can specify this option multiple times.") + parser.add_option("-v", "--verbose", dest="verbose", action="count", default=0, help="Print more output. You can specify this option multiple times.") options, args = parser.parse_args() @@ -93,6 +98,14 @@ def main(): else: imgformat = 'png' + logging.getLogger().setLevel( + logging.getLogger().level + 10*options.quiet) + logging.getLogger().setLevel( + logging.getLogger().level - 10*options.verbose) + + logging.info("Welcome to Minecraft Overviewer!") + logging.debug("Current log level: {0}".format(logging.getLogger().level)) + # First generate the world's chunk images w = world.WorldRenderer(worlddir, cachedir, chunklist=chunklist) w.go(options.procs) @@ -110,7 +123,7 @@ def delete_all(worlddir, tiledir): for f in filenames: if matcher.match(f): filepath = os.path.join(dirpath, f) - print "Deleting {0}".format(filepath) + logging.info("Deleting {0}".format(filepath)) os.unlink(filepath) # Now delete all /hash/ files in the tile dir. @@ -119,7 +132,7 @@ def delete_all(worlddir, tiledir): for f in filenames: if f.endswith(".hash"): filepath = os.path.join(dirpath, f) - print "Deleting {0}".format(filepath) + logging.info("Deleting {0}".format(filepath)) os.unlink(filepath) def list_worlds(): diff --git a/quadtree.py b/quadtree.py index 8733881..9fb8128 100644 --- a/quadtree.py +++ b/quadtree.py @@ -23,6 +23,7 @@ import re import shutil import collections import json +import logging from PIL import Image @@ -45,7 +46,7 @@ def catch_keyboardinterrupt(func): try: return func(*args, **kwargs) except KeyboardInterrupt: - print "Ctrl-C caught!" + logging.error("Ctrl-C caught!") raise Exception("Exiting") except: import traceback @@ -111,8 +112,8 @@ class QuadtreeGen(object): else: if not complete % 1000 == 0: return - print "{0}/{1} tiles complete on level {2}/{3}".format( - complete, total, level, self.p) + logging.info("{0}/{1} tiles complete on level {2}/{3}".format( + complete, total, level, self.p)) def write_html(self, zoomlevel, imgformat): """Writes out index.html""" @@ -258,12 +259,12 @@ class QuadtreeGen(object): curdepth = self._get_cur_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..." + logging.warning("Your map seemes to have expanded beyond its previous bounds.") + logging.warning( "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..." + logging.warning("Your map seems to have shrunk. Re-arranging tiles, just a sec...") for _ in xrange(curdepth - self.p): self._decrease_depth() @@ -279,11 +280,11 @@ class QuadtreeGen(object): results = collections.deque() complete = 0 total = 4**self.p - print "Rendering highest zoom level of tiles now." - print "There are {0} tiles to render".format(total) - print "There are {0} total levels to render".format(self.p) - print "Don't worry, each level has only 25% as many tiles as the last." - print "The others will go faster" + logging.info("Rendering highest zoom level of tiles now.") + logging.info("There are {0} tiles to render".format(total)) + logging.info("There are {0} total levels to render".format(self.p)) + logging.info("Don't worry, each level has only 25% as many tiles as the last.") + logging.info("The others will go faster") for result in self._apply_render_worldtiles(pool): results.append(result) if len(results) > 10000: @@ -308,7 +309,7 @@ class QuadtreeGen(object): assert len(results) == 0 complete = 0 total = 4**zoom - print "Starting level", level + logging.info("Starting level {0}".format(level)) for result in self._apply_render_inntertile(pool, zoom): results.append(result) if len(results) > 10000: @@ -324,7 +325,7 @@ class QuadtreeGen(object): self.print_statusline(complete, total, level, True) - print "Done" + logging.info("Done") pool.close() pool.join() @@ -561,8 +562,7 @@ def render_worldtile(chunks, colstart, colend, rowstart, rowend, path, imgformat # corrupting it), then this could error. # Since we have no easy way of determining how this chunk was # generated, we need to just ignore it. - print "Error opening file", chunkfile - print "(Error was {0})".format(e) + logging.warning("Could not open chunk '{0}' ({1})".format(chunkfile,e)) try: # Remove the file so that the next run will re-generate it. os.unlink(chunkfile) @@ -571,12 +571,12 @@ def render_worldtile(chunks, colstart, colend, rowstart, rowend, path, imgformat # Ignore if file doesn't exist, another task could have already # removed it. if e.errno != errno.ENOENT: - print "Could not remove the corrupt chunk!" + logging.warning("Could not remove chunk '{0}'!".format(chunkfile)) raise else: - print "Removed the corrupt file" + logging.warning("Removed the corrupt file") - print "You will need to re-run the Overviewer to fix this chunk" + logging.warning("You will need to re-run the Overviewer to fix this chunk") continue xpos = -192 + (col-colstart)*192 diff --git a/world.py b/world.py index 99728c1..cc3f545 100644 --- a/world.py +++ b/world.py @@ -18,6 +18,7 @@ import os import os.path import multiprocessing import sys +import logging import numpy from PIL import Image @@ -171,8 +172,9 @@ class WorldRenderer(object): def go(self, procs): """Starts the render. This returns when it is finished""" - print "Scanning chunks" + logging.info("Scanning chunks") raw_chunks = self._find_chunkfiles() + logging.debug("Done scanning chunks") # Translate chunks to our diagonal coordinate system mincol, maxcol, minrow, maxrow, chunks = _convert_coords(raw_chunks) @@ -206,9 +208,11 @@ class WorldRenderer(object): p = f.split(".") all_chunks.append((base36decode(p[1]), base36decode(p[2]), os.path.join(dirpath, f))) + logging.debug((base36decode(p[1]), base36decode(p[2]), + os.path.join(dirpath, f))) if not all_chunks: - print "Error: No chunks found!" + logging.error("Error: No chunks found!") sys.exit(1) return all_chunks @@ -229,7 +233,7 @@ class WorldRenderer(object): results = {} if processes == 1: # Skip the multiprocessing stuff - print "Rendering chunks synchronously since you requested 1 process" + logging.debug("Rendering chunks synchronously since you requested 1 process") for i, (col, row, chunkfile) in enumerate(chunks): if inclusion_set and (col, row) not in inclusion_set: # Skip rendering, just find where the existing image is @@ -243,9 +247,9 @@ class WorldRenderer(object): results[(col, row)] = result if i > 0: if 1000 % i == 0 or i % 1000 == 0: - print "{0}/{1} chunks rendered".format(i, len(chunks)) + logging.info("{0}/{1} chunks rendered".format(i, len(chunks))) else: - print "Rendering chunks in {0} processes".format(processes) + logging.debug("Rendering chunks in {0} processes".format(processes)) pool = multiprocessing.Pool(processes=processes) asyncresults = [] for col, row, chunkfile in chunks: @@ -268,10 +272,10 @@ class WorldRenderer(object): results[(col, row)] = result.get() if i > 0: if 1000 % i == 0 or i % 1000 == 0: - print "{0}/{1} chunks rendered".format(i, len(asyncresults)) + logging.info("{0}/{1} chunks rendered".format(i, len(asyncresults))) pool.join() - print "Done!" + logging.info("Done!") return results From 110240b53a4480097f1e9d75e0eb372b888012b6 Mon Sep 17 00:00:00 2001 From: Sam Steele Date: Thu, 30 Sep 2010 18:35:46 -0700 Subject: [PATCH 20/30] Ignore .pyc files --- .gitignore | 1 + 1 file changed, 1 insertion(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0d20b64 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.pyc From cf971c17c6b7735d496501f43f63c953241353db Mon Sep 17 00:00:00 2001 From: Sam Steele Date: Thu, 30 Sep 2010 18:36:10 -0700 Subject: [PATCH 21/30] Render snow as half-blocks --- chunk.py | 9 +++++++-- textures.py | 4 ++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/chunk.py b/chunk.py index fef3a84..aa2aed1 100644 --- a/chunk.py +++ b/chunk.py @@ -312,12 +312,17 @@ class ChunkRenderer(object): img.paste(t[0], (imgx, imgy), t[1]) # Draw edge lines + if blockid in (44,78,): # step block, snow + half = 6 + else: + half = 0 + if blockid not in transparent_blocks: draw = ImageDraw.Draw(img) if x != 15 and blocks[x+1,y,z] == 0: - draw.line(((imgx+12,imgy), (imgx+22,imgy+5)), fill=(0,0,0), width=1) + draw.line(((imgx+12,imgy+half), (imgx+22,imgy+5+half)), fill=(0,0,0), width=1) if y != 0 and blocks[x,y-1,z] == 0: - draw.line(((imgx,imgy+6), (imgx+12,imgy)), fill=(0,0,0), width=1) + draw.line(((imgx,imgy+6+half), (imgx+12,imgy+half)), fill=(0,0,0), width=1) finally: diff --git a/textures.py b/textures.py index 8d34b03..320d980 100644 --- a/textures.py +++ b/textures.py @@ -151,7 +151,7 @@ def _transform_image_side(img, blockID): """Takes an image and shears it for the left side of the cube (reflect for the right side)""" - if blockID in (44,): # step block + if blockID in (44,78,): # step block, snow # make the top half transpartent # (don't just crop img, since we want the size of # img to be unchanged @@ -213,7 +213,7 @@ def _build_block(top, side, blockID=None): img.paste(side, (2,6), side) img.paste(otherside, (10,6), otherside) img.paste(top, (0,2), top) - elif blockID in (44,): # half step + elif blockID in (44,78,): # half step, snow # shift each texture down 6 pixels img.paste(side, (0,12), side) img.paste(otherside, (12,12), otherside) From 822dd75431e857a312a0be3d198e785130f306b6 Mon Sep 17 00:00:00 2001 From: Andrew Chin Date: Thu, 30 Sep 2010 21:38:14 -0400 Subject: [PATCH 22/30] Render minetracks (with correct orientation) Other things with direction (torches, ladders, stairs, etc) will be handled in a similar fashion. Note: minetracks on slopes are still not rendered correctly --- chunk.py | 24 +++++++++++++++++++++++- textures.py | 42 +++++++++++++++++++++++++++++++++++++++++- 2 files changed, 64 insertions(+), 2 deletions(-) diff --git a/chunk.py b/chunk.py index fef3a84..aaee2e4 100644 --- a/chunk.py +++ b/chunk.py @@ -59,6 +59,11 @@ def get_skylight_array(level): """ return numpy.frombuffer(level['SkyLight'], dtype=numpy.uint8).reshape((16,16,64)) +def get_blockdata_array(level): + """Returns the ancillary data from the 'Data' byte array. Data is packed + in a similar manner to skylight data""" + return numpy.frombuffer(level['Data'], dtype=numpy.uint8).reshape((16,16,64)) + # This set holds blocks ids that can be seen through, for occlusion calculations transparent_blocks = set([0, 6, 8, 9, 18, 20, 37, 38, 39, 40, 44, 50, 51, 52, 53, 59, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 74, 75, 76, 77, 79, 81, 83, 85]) @@ -237,6 +242,13 @@ class ChunkRenderer(object): blocks = blocks.copy() blocks[skylight_expanded != 0] = 21 + blockData = get_blockdata_array(self.level) + blockData_expanded = numpy.empty((16,16,128), dtype=numpy.uint8) + # Even elements get the lower 4 bits + blockData_expanded[:,:,::2] = blockData & 0x0F + # Odd elements get the upper 4 bits + blockData_expanded[:,:,1::2] = blockData >> 4 + # Each block is 24x24 # The next block on the X axis adds 12px to x and subtracts 6px from y in the image @@ -255,7 +267,17 @@ class ChunkRenderer(object): for z in xrange(128): try: blockid = blocks[x,y,z] - t = textures.blockmap[blockid] + + # the following blocks don't have textures that can be pre-computed from the blockid + # alone. additional data is required. + # TODO torches, redstone torches, crops, ladders, stairs, + # levers, doors, buttons, and signs all need to be handled here (and in textures.py) + if blockid in (66,): ## minecart track + ancilData = blockData_expanded[x,y,z] + t = textures.generate_special_texture(blockid, ancilData) + + else: + t = textures.blockmap[blockid] if not t: continue diff --git a/textures.py b/textures.py index 8d34b03..a73517f 100644 --- a/textures.py +++ b/textures.py @@ -114,7 +114,7 @@ def _split_terrain(terrain): # This maps terainids to 16x16 images terrain_images = _split_terrain(_get_terrain_image()) -def _transform_image(img, blockID): +def _transform_image(img, blockID=None): """Takes a PIL image and rotates it left 45 degrees and shrinks the y axis by a factor of 2. Returns the resulting image, which will be 24x12 pixels @@ -201,6 +201,8 @@ def _build_block(top, side, blockID=None): otherside.putalpha(othersidealpha) ## special case for non-block things + # TODO once torches are handled by generate_special_texture, remove + # them from this list if blockID in (37,38,6,39,40,50,83): ## flowers, sapling, mushrooms, regular torch, reeds # instead of pasting these blocks at the cube edges, place them in the middle: # and omit the top @@ -321,3 +323,41 @@ def load_water(): blockmap[10] = lavablock.convert("RGB"), lavablock blockmap[11] = blockmap[10] load_water() + + +def generate_special_texture(blockID, data): + """Generates a special texture, such as a correctly facing minecraft track""" + #print "%s has ancillary data: %X" %(blockID, data) + # TODO torches, redstone torches, crops, ladders, stairs, + # levers, doors, buttons, and signs all need to be handled here (and in chunkpy) + if blockID == 66: # minetrack: + raw_straight = terrain_images[128] + raw_corner = terrain_images[112] + + ## use _transform_image to scale and shear + if data == 0: + track = _transform_image(raw_straight, blockID) + elif data == 6: + track = _transform_image(raw_corner, blockID) + elif data == 7: + track = _transform_image(raw_corner.rotate(270), blockID) + elif data == 8: + # flip + track = _transform_image(raw_corner.transpose(Image.FLIP_TOP_BOTTOM).rotate(90), + blockID) + elif data == 9: + track = _transform_image(raw_corner.transpose(Image.FLIP_TOP_BOTTOM), + blockID) + elif data == 1: + track = _transform_image(raw_straight.rotate(90), blockID) + else: + # TODO render carts that slop up or down + track = _transform_image(raw_straight, blockID) + + img = Image.new("RGBA", (24,24), (38,92,255,0)) + img.paste(track, (0,12), track) + + return (img.convert("RGB"), img.split()[3]) + + + return None From 5d22e1cd6f667bbba7242d1f7a0ba2551b598778 Mon Sep 17 00:00:00 2001 From: Sam Steele Date: Thu, 30 Sep 2010 22:47:10 -0700 Subject: [PATCH 23/30] Fix whitespace formatting, make snow 1/4th of block --- chunk.py | 14 ++++++++------ textures.py | 17 ++++++++++++++--- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/chunk.py b/chunk.py index 474bc76..91f8ac5 100644 --- a/chunk.py +++ b/chunk.py @@ -334,17 +334,19 @@ class ChunkRenderer(object): img.paste(t[0], (imgx, imgy), t[1]) # Draw edge lines - if blockid in (44,78,): # step block, snow - half = 6 - else: - half = 0 + if blockid in (44,): # step block + increment = 6 + elif blockid in (78,): # snow + increment = 9 + else: + increment = 0 if blockid not in transparent_blocks: draw = ImageDraw.Draw(img) if x != 15 and blocks[x+1,y,z] == 0: - draw.line(((imgx+12,imgy+half), (imgx+22,imgy+5+half)), fill=(0,0,0), width=1) + draw.line(((imgx+12,imgy+increment), (imgx+22,imgy+5+increment)), fill=(0,0,0), width=1) if y != 0 and blocks[x,y-1,z] == 0: - draw.line(((imgx,imgy+6+half), (imgx+12,imgy+half)), fill=(0,0,0), width=1) + draw.line(((imgx,imgy+6+increment), (imgx+12,imgy+increment)), fill=(0,0,0), width=1) finally: diff --git a/textures.py b/textures.py index b9285d4..5bb7382 100644 --- a/textures.py +++ b/textures.py @@ -151,14 +151,20 @@ def _transform_image_side(img, blockID): """Takes an image and shears it for the left side of the cube (reflect for the right side)""" - if blockID in (44,78,): # step block, snow - # make the top half transpartent + if blockID in (44,): # step block + # make the top half transparent # (don't just crop img, since we want the size of # img to be unchanged mask = img.crop((0,8,16,16)) n = Image.new(img.mode, img.size, (38,92,255,0)) n.paste(mask,(0,0,16,8), mask) img = n + if blockID in (78,): # snow + # make the top three quarters transparent + mask = img.crop((0,12,16,16)) + n = Image.new(img.mode, img.size, (38,92,255,0)) + n.paste(mask,(0,12,16,16), mask) + img = n # Size of the cube side before shear img = img.resize((12,12)) @@ -215,11 +221,16 @@ def _build_block(top, side, blockID=None): img.paste(side, (2,6), side) img.paste(otherside, (10,6), otherside) img.paste(top, (0,2), top) - elif blockID in (44,78,): # half step, snow + elif blockID in (44,): # half step # shift each texture down 6 pixels img.paste(side, (0,12), side) img.paste(otherside, (12,12), otherside) img.paste(top, (0,6), top) + elif blockID in (78,): # snow + # shift each texture down 9 pixels + img.paste(side, (0,6), side) + img.paste(otherside, (12,6), otherside) + img.paste(top, (0,9), top) else: img.paste(side, (0,6), side) img.paste(otherside, (12,6), otherside) From 93af1ef15817a9667a34eb0a608e358cc08acdc6 Mon Sep 17 00:00:00 2001 From: Sam Steele Date: Thu, 30 Sep 2010 23:52:50 -0700 Subject: [PATCH 24/30] Add snow to the transparent_blocks array --- chunk.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/chunk.py b/chunk.py index 91f8ac5..322f6c2 100644 --- a/chunk.py +++ b/chunk.py @@ -66,7 +66,7 @@ def get_blockdata_array(level): # This set holds blocks ids that can be seen through, for occlusion calculations transparent_blocks = set([0, 6, 8, 9, 18, 20, 37, 38, 39, 40, 44, 50, 51, 52, 53, - 59, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 74, 75, 76, 77, 79, 81, 83, 85]) + 59, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 74, 75, 76, 77, 78, 79, 81, 83, 85]) def render_and_save(chunkfile, cachedir, cave=False): """Used as the entry point for the multiprocessing workers (since processes @@ -341,7 +341,7 @@ class ChunkRenderer(object): else: increment = 0 - if blockid not in transparent_blocks: + if blockid not in transparent_blocks or blockid in (78,): #special case snow so the outline is still drawn draw = ImageDraw.Draw(img) if x != 15 and blocks[x+1,y,z] == 0: draw.line(((imgx+12,imgy+increment), (imgx+22,imgy+5+increment)), fill=(0,0,0), width=1) From 86d5476234cd061713bd207785d198a2b570da9c Mon Sep 17 00:00:00 2001 From: Andrew Chin Date: Fri, 1 Oct 2010 19:45:41 -0400 Subject: [PATCH 25/30] use os.path.join to build the spawn chunk path --- world.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/world.py b/world.py index 231287b..25157d1 100644 --- a/world.py +++ b/world.py @@ -147,10 +147,8 @@ class WorldRenderer(object): chunkY = spawnZ/16 ## The filename of this chunk - chunkFile = "%s/%s/c.%s.%s.dat" % (base36encode(chunkX % 64), - base36encode(chunkY % 64), - base36encode(chunkX), - base36encode(chunkY)) + chunkFile = os.path.join(base36encode(chunkX % 64), base36encode(chunkY % 64), + "c.%s.%s.dat" % (base36encode(chunkX), base36encode(chunkY))) data=nbt.load(os.path.join(self.worlddir, chunkFile))[1] From 78962cfbe57243949856051504d71ca4f20c77fc Mon Sep 17 00:00:00 2001 From: Andrew Chin Date: Fri, 1 Oct 2010 22:33:25 -0400 Subject: [PATCH 26/30] Passable crop rendering --- chunk.py | 2 +- textures.py | 16 +++++++++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/chunk.py b/chunk.py index aaee2e4..d3b3fc4 100644 --- a/chunk.py +++ b/chunk.py @@ -272,7 +272,7 @@ class ChunkRenderer(object): # alone. additional data is required. # TODO torches, redstone torches, crops, ladders, stairs, # levers, doors, buttons, and signs all need to be handled here (and in textures.py) - if blockid in (66,): ## minecart track + if blockid in (66,59): ## minecart track, crops ancilData = blockData_expanded[x,y,z] t = textures.generate_special_texture(blockid, ancilData) diff --git a/textures.py b/textures.py index a73517f..0d4cfb1 100644 --- a/textures.py +++ b/textures.py @@ -253,7 +253,7 @@ def _build_blockimages(): # 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 -1, -1, -1, 64, 64, 13, 12, 29, 28, 23, 22, 6, 6, 7, 8, 35, # Gold/iron blocks? Doublestep? TNT from above? # 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 - 36, 37, 80, -1, 65, 4, 25,101, 98, 24, 43, -1, 86, 1, 1, -1, # Torch from above? leaving out fire. Redstone wire? Crops left out. sign post + 36, 37, 80, -1, 65, 4, 25,101, 98, 24, 43, -1, 86, 1, 1, -1, # Torch from above? leaving out fire. Redstone wire? Crops handled elsewhere. sign post # 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 -1, -1, -1, 16, -1, -1, -1, -1, -1, 51, 51, -1, -1, 1, 66, 67, # door,ladder left out. Minecart rail orientation # 80 81 82 83 84 @@ -358,6 +358,20 @@ def generate_special_texture(blockID, data): img.paste(track, (0,12), track) return (img.convert("RGB"), img.split()[3]) + if blockID == 59: # crops + raw_crop = terrain_images[88+data] + crop1 = _transform_image(raw_crop, blockID) + crop2 = _transform_image_side(raw_crop, blockID) + crop3 = crop2.transpose(Image.FLIP_LEFT_RIGHT) + + img = Image.new("RGBA", (24,24), (38,92,255,0)) + img.paste(crop1, (0,12), crop1) + img.paste(crop2, (6,3), crop2) + img.paste(crop3, (6,3), crop3) + return (img.convert("RGB"), img.split()[3]) + + + return None From 66a90bb4113cf7590a0ba0b15d884d04b3c45c45 Mon Sep 17 00:00:00 2001 From: Andrew Chin Date: Fri, 1 Oct 2010 22:51:02 -0400 Subject: [PATCH 27/30] Render furnaces with the correct texture (instead of smooth stone) These are handled specially, since one side texture is different than the other --- chunk.py | 3 ++- textures.py | 27 ++++++++++++++++++++++++--- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/chunk.py b/chunk.py index d3b3fc4..2c345c1 100644 --- a/chunk.py +++ b/chunk.py @@ -272,7 +272,8 @@ class ChunkRenderer(object): # alone. additional data is required. # TODO torches, redstone torches, crops, ladders, stairs, # levers, doors, buttons, and signs all need to be handled here (and in textures.py) - if blockid in (66,59): ## minecart track, crops + if blockid in (66,59,61,62): ## minecart track, crops + # also handle furnaces here, since one side has a different texture than the other ancilData = blockData_expanded[x,y,z] t = textures.generate_special_texture(blockid, ancilData) diff --git a/textures.py b/textures.py index 0d4cfb1..1b7d27c 100644 --- a/textures.py +++ b/textures.py @@ -147,7 +147,7 @@ def _transform_image(img, blockID=None): newimg = img.transform((24,12), Image.AFFINE, transform) return newimg -def _transform_image_side(img, blockID): +def _transform_image_side(img, blockID=None): """Takes an image and shears it for the left side of the cube (reflect for the right side)""" @@ -253,7 +253,7 @@ def _build_blockimages(): # 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 -1, -1, -1, 64, 64, 13, 12, 29, 28, 23, 22, 6, 6, 7, 8, 35, # Gold/iron blocks? Doublestep? TNT from above? # 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 - 36, 37, 80, -1, 65, 4, 25,101, 98, 24, 43, -1, 86, 1, 1, -1, # Torch from above? leaving out fire. Redstone wire? Crops handled elsewhere. sign post + 36, 37, 80, -1, 65, 4, 25,101, 98, 24, 43, -1, 86, 1, 1, -1, # Torch from above? leaving out fire. Redstone wire? Crops/furnaces handled elsewhere. sign post # 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 -1, -1, -1, 16, -1, -1, -1, -1, -1, 51, 51, -1, -1, 1, 66, 67, # door,ladder left out. Minecart rail orientation # 80 81 82 83 84 @@ -270,7 +270,7 @@ def _build_blockimages(): # 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 -1, -1, -1, 64, 64, 13, 12, 29, 28, 23, 22, 5, 5, 7, 8, 35, # 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 - 36, 37, 80, -1, 65, 4, 25,101, 98, 24, 43, -1, 86, 1, 1, -1, + 36, 37, 80, -1, 65, 4, 25,101, 98, 24, 43, -1, 86, 44, 61, -1, # 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 -1, -1, -1, 16, -1, -1, -1, -1, -1, 51, 51, -1, -1, 1, 66, 67, # 80 81 82 83 84 @@ -370,8 +370,29 @@ def generate_special_texture(blockID, data): img.paste(crop3, (6,3), crop3) return (img.convert("RGB"), img.split()[3]) + if blockID == 61: #furnace + top = _transform_image(terrain_images[1]) + side1 = _transform_image_side(terrain_images[45]) + side2 = _transform_image_side(terrain_images[44]).transpose(Image.FLIP_LEFT_RIGHT) + img = Image.new("RGBA", (24,24), (38,92,255,0)) + img.paste(side1, (0,6), side1) + img.paste(side2, (12,6), side2) + img.paste(top, (0,0), top) + return (img.convert("RGB"), img.split()[3]) + + if blockID == 62: # lit furnace + top = _transform_image(terrain_images[1]) + side1 = _transform_image_side(terrain_images[45]) + side2 = _transform_image_side(terrain_images[45+16]).transpose(Image.FLIP_LEFT_RIGHT) + + img = Image.new("RGBA", (24,24), (38,92,255,0)) + + img.paste(side1, (0,6), side1) + img.paste(side2, (12,6), side2) + img.paste(top, (0,0), top) + return (img.convert("RGB"), img.split()[3]) return None From cd97222a9b9681b7d856bb91f0511843a7bc0485 Mon Sep 17 00:00:00 2001 From: Andrew Chin Date: Fri, 1 Oct 2010 23:19:53 -0400 Subject: [PATCH 28/30] Render ladders (with correct orientation) --- chunk.py | 2 +- textures.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/chunk.py b/chunk.py index 2c345c1..2817979 100644 --- a/chunk.py +++ b/chunk.py @@ -272,7 +272,7 @@ class ChunkRenderer(object): # alone. additional data is required. # TODO torches, redstone torches, crops, ladders, stairs, # levers, doors, buttons, and signs all need to be handled here (and in textures.py) - if blockid in (66,59,61,62): ## minecart track, crops + if blockid in (66,59,61,62, 65): ## minecart track, crops, ladder # also handle furnaces here, since one side has a different texture than the other ancilData = blockData_expanded[x,y,z] t = textures.generate_special_texture(blockid, ancilData) diff --git a/textures.py b/textures.py index 1b7d27c..dbcac8a 100644 --- a/textures.py +++ b/textures.py @@ -394,5 +394,33 @@ def generate_special_texture(blockID, data): img.paste(top, (0,0), top) return (img.convert("RGB"), img.split()[3]) + if blockID == 65: # ladder + raw_texture = terrain_images[83] + #print "ladder is facing: %d" % data + if data == 5: + # normally this ladder would be obsured by the block it's attached to + # but since ladders can apparently be placed on transparent blocks, we + # have to render this thing anyway. same for data == 2 + tex = _transform_image_side(raw_texture) + img = Image.new("RGBA", (24,24), (38,92,255,0)) + img.paste(tex, (0,6), tex) + return (img.convert("RGB"), img.split()[3]) + if data == 2: + tex = _transform_image_side(raw_texture).transpose(Image.FLIP_LEFT_RIGHT) + img = Image.new("RGBA", (24,24), (38,92,255,0)) + img.paste(tex, (12,6), tex) + return (img.convert("RGB"), img.split()[3]) + if data == 3: + tex = _transform_image_side(raw_texture).transpose(Image.FLIP_LEFT_RIGHT) + img = Image.new("RGBA", (24,24), (38,92,255,0)) + img.paste(tex, (0,0), tex) + return (img.convert("RGB"), img.split()[3]) + if data == 4: + tex = _transform_image_side(raw_texture) + img = Image.new("RGBA", (24,24), (38,92,255,0)) + img.paste(tex, (12,0), tex) + return (img.convert("RGB"), img.split()[3]) + + return None From 6993f2159db6e11778166f7a38b7b9a3afa1bfc9 Mon Sep 17 00:00:00 2001 From: Andrew Chin Date: Sat, 2 Oct 2010 15:33:33 -0400 Subject: [PATCH 29/30] Render iron and wood doors with correct orientation. Note: iron doors need testing (they are currently broken in my test SMP world) --- chunk.py | 6 ++++-- textures.py | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 2 deletions(-) diff --git a/chunk.py b/chunk.py index 2817979..a1deab1 100644 --- a/chunk.py +++ b/chunk.py @@ -272,8 +272,10 @@ class ChunkRenderer(object): # alone. additional data is required. # TODO torches, redstone torches, crops, ladders, stairs, # levers, doors, buttons, and signs all need to be handled here (and in textures.py) - if blockid in (66,59,61,62, 65): ## minecart track, crops, ladder - # also handle furnaces here, since one side has a different texture than the other + + ## minecart track, crops, ladder, doors + if blockid in (66,59,61,62, 65,64,71): + # also handle furnaces here, since one side has a different texture than the other ancilData = blockData_expanded[x,y,z] t = textures.generate_special_texture(blockid, ancilData) diff --git a/textures.py b/textures.py index dbcac8a..755dae7 100644 --- a/textures.py +++ b/textures.py @@ -421,6 +421,56 @@ def generate_special_texture(blockID, data): img.paste(tex, (12,0), tex) return (img.convert("RGB"), img.split()[3]) + if blockID in (64,71): #wooden door, or iron door + if data & 0x8 == 0x8: # top of the door + raw_door = terrain_images[81 if blockID == 64 else 82] + else: # bottom of the door + raw_door = terrain_images[97 if blockID == 64 else 98] + + # if you want to render all doors as closed, then force + # force swung to be False + if data & 0x4 == 0x4: + swung=True + else: + swung=False + # mask out the high bits to figure out the orientation + img = Image.new("RGBA", (24,24), (38,92,255,0)) + if (data & 0x03) == 0: + if not swung: + tex = _transform_image_side(raw_door) + img.paste(tex, (0,6), tex) + else: + # flip first to set the doornob on the correct side + tex = _transform_image_side(raw_door.transpose(Image.FLIP_LEFT_RIGHT)) + tex = tex.transpose(Image.FLIP_LEFT_RIGHT) + img.paste(tex, (0,0), tex) + + if (data & 0x03) == 1: + if not swung: + tex = _transform_image_side(raw_door).transpose(Image.FLIP_LEFT_RIGHT) + img.paste(tex, (0,0), tex) + else: + tex = _transform_image_side(raw_door) + img.paste(tex, (12,0), tex) + + if (data & 0x03) == 2: + if not swung: + tex = _transform_image_side(raw_door.transpose(Image.FLIP_LEFT_RIGHT)) + img.paste(tex, (12,0), tex) + else: + tex = _transform_image_side(raw_door).transpose(Image.FLIP_LEFT_RIGHT) + img.paste(tex, (12,6), tex) + + if (data & 0x03) == 3: + if not swung: + tex = _transform_image_side(raw_door.transpose(Image.FLIP_LEFT_RIGHT)).transpose(Image.FLIP_LEFT_RIGHT) + img.paste(tex, (12,6), tex) + else: + tex = _transform_image_side(raw_door.transpose(Image.FLIP_LEFT_RIGHT)) + img.paste(tex, (0,6), tex) + + return (img.convert("RGB"), img.split()[3]) + return None From 60966ffa73afdcb7ce068cd6ea20b08b214a915d Mon Sep 17 00:00:00 2001 From: Andrew Chin Date: Sat, 2 Oct 2010 20:17:13 -0400 Subject: [PATCH 30/30] Improve efficiency of special textures by pre-computing them --- chunk.py | 9 ++++++--- textures.py | 22 ++++++++++++++++++++++ 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/chunk.py b/chunk.py index a1deab1..d109ee2 100644 --- a/chunk.py +++ b/chunk.py @@ -273,11 +273,14 @@ class ChunkRenderer(object): # TODO torches, redstone torches, crops, ladders, stairs, # levers, doors, buttons, and signs all need to be handled here (and in textures.py) - ## minecart track, crops, ladder, doors - if blockid in (66,59,61,62, 65,64,71): + ## minecart track, crops, ladder, doors, etc. + if blockid in textures.special_blocks: # also handle furnaces here, since one side has a different texture than the other ancilData = blockData_expanded[x,y,z] - t = textures.generate_special_texture(blockid, ancilData) + try: + t = textures.specialblockmap[(blockid, ancilData)] + except KeyError: + t = None else: t = textures.blockmap[blockid] diff --git a/textures.py b/textures.py index 755dae7..940c70c 100644 --- a/textures.py +++ b/textures.py @@ -474,3 +474,25 @@ def generate_special_texture(blockID, data): return None + + +# This set holds block ids that require special pre-computing. These are typically +# things that require ancillary data to render properly (i.e. ladder plus orientation) +special_blocks = set([66,59,61,62, 65,64,71]) + +# this is a map of special blockIDs to a list of all +# possible values for ancillary data that it might have. +special_map = {} +special_map[66] = range(10) # minecrart tracks +special_map[59] = range(8) # crops +special_map[61] = (0,) # furnace +special_map[62] = (0,) # burning furnace +special_map[65] = (2,3,4,5) # ladder +special_map[64] = range(16) # wooden door +special_map[71] = range(16) # iron door + +specialblockmap = {} + +for blockID in special_blocks: + for data in special_map[blockID]: + specialblockmap[(blockID, data)] = generate_special_texture(blockID, data)