diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0d20b64 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.pyc diff --git a/README.rst b/README.rst index 007de92..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 ------- @@ -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 @@ -209,6 +215,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. diff --git a/chunk.py b/chunk.py index 259adcf..45ce17f 100644 --- a/chunk.py +++ b/chunk.py @@ -15,7 +15,6 @@ import numpy from PIL import Image, ImageDraw, ImageEnhance -from itertools import izip, count import os.path import hashlib @@ -88,9 +87,14 @@ def get_blocklight_array(level): blocklight_expanded[:,:,1::2] = (blocklight & 0xF0) >> 4 return blocklight_expanded +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, 50, 51, 52, 53, - 59, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 74, 75, 76, 77, 79, 83, 85]) +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, 78, 79, 81, 83, 85]) def render_and_save(chunkfile, cachedir, worldobj, cave=False): """Used as the entry point for the multiprocessing workers (since processes @@ -309,6 +313,13 @@ class ChunkRenderer(object): blocks = blocks.copy() blocks[skylight != 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 @@ -327,7 +338,23 @@ 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) + + ## 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] + try: + t = textures.specialblockmap[(blockid, ancilData)] + except KeyError: + t = None + + else: + t = textures.blockmap[blockid] if not t: continue @@ -420,12 +447,19 @@ class ChunkRenderer(object): img.paste((0,0,0), (imgx, imgy), ImageEnhance.Brightness(facemasks[2]).enhance(black_coeff)) # Draw edge lines - if blockid not in transparent_blocks: + if blockid in (44,): # step block + increment = 6 + elif blockid in (78,): # snow + increment = 9 + else: + increment = 0 + + 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), (imgx+22,imgy+5)), 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), (imgx+12,imgy)), 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/gmap.py b/gmap.py index 1ce469c..d5f084c 100755 --- a/gmap.py +++ b/gmap.py @@ -26,14 +26,16 @@ 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 helptext = """ %prog [OPTIONS] -%prog -d [tiles dest dir] -""" +%prog -d [tiles dest dir]""" def main(): try: @@ -48,6 +50,9 @@ def main(): 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("--lighting", dest="lighting", help="Renders shadows using light data from each chunk.", action="store_true") parser.add_option("--night", dest="night", help="Renders shadows using light data from each chunk, as if it were night. Implies --lighting.", action="store_true") + 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() @@ -87,12 +92,28 @@ 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' + + 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, lighting=options.lighting, night=options.night) 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): @@ -104,7 +125,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. @@ -113,7 +134,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(): @@ -134,4 +155,5 @@ def list_worlds(): if __name__ == "__main__": + multiprocessing.freeze_support() main() diff --git a/quadtree.py b/quadtree.py index a075e77..553c67f 100644 --- a/quadtree.py +++ b/quadtree.py @@ -23,9 +23,12 @@ import re import shutil import collections import json +import logging from PIL import Image +import util + """ This module has routines related to generating a quadtree of tiles @@ -43,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 @@ -52,7 +55,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 +65,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): @@ -106,16 +112,18 @@ 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): + 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( "{maxzoom}", str(zoomlevel)) + html = html.replace( + "{imgformat}", str(imgformat)) with open(os.path.join(self.destdir, "index.html"), 'w') as output: output.write(html) @@ -125,6 +133,12 @@ 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)) + 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 index.html to see what maxZoom is set to. @@ -159,8 +173,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)+"."+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): @@ -221,7 +235,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 +247,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""" @@ -245,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() @@ -260,17 +274,17 @@ 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() 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: @@ -295,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: @@ -311,13 +325,13 @@ class QuadtreeGen(object): self.print_statusline(complete, total, level, True) - print "Done" + logging.info("Done") pool.close() 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 +362,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 +448,16 @@ def render_innertile(dest, name): img.paste(quad3, (192, 192)) # Save it - img.save(imgpath) + if imgformat == 'jpg': + img.save(imgpath, quality=95, subsampling=0) + 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 +466,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 +507,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 @@ -545,8 +562,7 @@ def render_worldtile(chunks, colstart, colend, rowstart, rowend, path): # 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) @@ -555,12 +571,12 @@ def render_worldtile(chunks, colstart, colend, rowstart, rowend, path): # 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/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, + }}, + + + ) diff --git a/template.html b/template.html index ef2868f..4387e8a 100644 --- a/template.html +++ b/template.html @@ -14,14 +14,73 @@ diff --git a/textures.py b/textures.py index 97e2fcc..61ad5b9 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) @@ -92,13 +94,19 @@ def _get_terrain_image(): def _split_terrain(terrain): """Builds and returns a length 256 array of each 16x16 chunk of texture""" textures = [] + (terrain_width, terrain_height) = terrain.size + texture_resolution = terrain_width / 16 for y in xrange(16): for x in xrange(16): - left = x*16 - upper = y*16 - right = left+16 - lower = upper+16 - region = terrain.crop((left,upper,right,lower)) + left = x*texture_resolution + upper = y*texture_resolution + right = left+texture_resolution + lower = upper+texture_resolution + region = terrain.transform( + (16, 16), + Image.EXTENT, + (left,upper,right,lower), + Image.BICUBIC) textures.append(region) return textures @@ -106,15 +114,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, 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 """ - # 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 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) + + 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)) @@ -134,10 +147,25 @@ def transform_image(img): newimg = img.transform((24,12), Image.AFFINE, transform) return newimg -def transform_image_side(img): +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)""" + 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)) @@ -151,20 +179,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) + 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) @@ -179,16 +207,34 @@ 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 + # 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 img.paste(side, (6,3), side) 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 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) + 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) + 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 @@ -218,7 +264,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/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 @@ -233,9 +279,9 @@ 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, + 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 @@ -243,8 +289,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 @@ -252,7 +301,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])) @@ -283,3 +334,176 @@ 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]) + 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]) + + 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]) + + 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]) + + 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 + + +# 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) 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]) diff --git a/world.py b/world.py index 5e710d7..c3083a6 100644 --- a/world.py +++ b/world.py @@ -18,9 +18,9 @@ import os import os.path import multiprocessing import sys +import logging import numpy -from PIL import Image import chunk import nbt @@ -180,8 +180,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) @@ -215,9 +216,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 @@ -238,7 +241,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 @@ -252,9 +255,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: @@ -277,10 +280,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