diff --git a/chunk.py b/chunk.py index 510a4e2..f67c5d8 100644 --- a/chunk.py +++ b/chunk.py @@ -80,6 +80,21 @@ def get_blockdata_array(level): in a similar manner to skylight data""" return numpy.frombuffer(level['Data'], dtype=numpy.uint8).reshape((16,16,64)) +def iterate_chunkblocks(xoff,yoff): + """Iterates over the 16x16x128 blocks of a chunk in rendering order. + Yields (x,y,z,imgx,imgy) + x,y,z is the block coordinate in the chunk + imgx,imgy is the image offset in the chunk image where that block should go + """ + for x in xrange(15,-1,-1): + for y in xrange(16): + imgx = xoff + x*12 + y*12 + imgy = yoff - x*6 + y*6 + 128*12 + 16*12//2 + for z in xrange(128): + yield x,y,z,imgx,imgy + imgy -= 12 + + # 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, 78, 79, 81, 83, 85]) @@ -456,132 +471,122 @@ class ChunkRenderer(object): if not img: img = Image.new("RGBA", (384, 1728), (38,92,255,0)) - for x in xrange(15,-1,-1): - for y in xrange(16): - imgx = xoff + x*12 + y*12 - imgy = yoff - x*6 + y*6 + 128*12 + 16*12//2 - for z in xrange(128): - try: - blockid = blocks[x,y,z] + for x,y,z,imgx,imgy in iterate_chunkblocks(xoff,yoff): + blockid = blocks[x,y,z] - # 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) + # 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 + ## 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 + else: + t = textures.blockmap[blockid] + if not t: + continue - # Check if this block is occluded - if cave and ( - x == 0 and y != 15 and z != 127 - ): - # If it's on the x face, only render if there's a - # transparent block in the y+1 direction OR the z-1 - # direction - if ( - blocks[x,y+1,z] not in transparent_blocks and - blocks[x,y,z+1] not in transparent_blocks - ): - continue - elif cave and ( - y == 15 and x != 0 and z != 127 - ): - # If it's on the facing y face, only render if there's - # a transparent block in the x-1 direction OR the z-1 - # direction - if ( - blocks[x-1,y,z] not in transparent_blocks and - blocks[x,y,z+1] not in transparent_blocks - ): - continue - elif cave and ( - y == 15 and x == 0 - ): - # If it's on the facing edge, only render if what's - # above it is transparent - if ( - blocks[x,y,z+1] not in transparent_blocks - ): - continue - elif ( - # Normal block or not cave mode, check sides for - # transparentcy or render unconditionally if it's - # on a shown face - x != 0 and y != 15 and z != 127 and - blocks[x-1,y,z] not in transparent_blocks and - blocks[x,y+1,z] not in transparent_blocks and - blocks[x,y,z+1] not in transparent_blocks - ): - # Don't render if all sides aren't transparent and - # we're not on the edge - continue + # Check if this block is occluded + if cave and ( + x == 0 and y != 15 and z != 127 + ): + # If it's on the x face, only render if there's a + # transparent block in the y+1 direction OR the z-1 + # direction + if ( + blocks[x,y+1,z] not in transparent_blocks and + blocks[x,y,z+1] not in transparent_blocks + ): + continue + elif cave and ( + y == 15 and x != 0 and z != 127 + ): + # If it's on the facing y face, only render if there's + # a transparent block in the x-1 direction OR the z-1 + # direction + if ( + blocks[x-1,y,z] not in transparent_blocks and + blocks[x,y,z+1] not in transparent_blocks + ): + continue + elif cave and ( + y == 15 and x == 0 + ): + # If it's on the facing edge, only render if what's + # above it is transparent + if ( + blocks[x,y,z+1] not in transparent_blocks + ): + continue + elif ( + # Normal block or not cave mode, check sides for + # transparentcy or render unconditionally if it's + # on a shown face + x != 0 and y != 15 and z != 127 and + blocks[x-1,y,z] not in transparent_blocks and + blocks[x,y+1,z] not in transparent_blocks and + blocks[x,y,z+1] not in transparent_blocks + ): + # Don't render if all sides aren't transparent and + # we're not on the edge + continue - # Draw the actual block on the image. For cave images, - # tint the block with a color proportional to its depth - if cave: - # no lighting for cave -- depth is probably more useful - img.paste(Image.blend(t[0],depth_colors[z],0.3), (imgx, imgy), t[1]) - else: - if not self.world.lighting: - # no lighting at all - img.paste(t[0], (imgx, imgy), t[1]) - elif blockid in transparent_blocks: - # transparent means draw the whole - # block shaded with the current - # block's light - black_coeff, _ = self.get_lighting_coefficient(x, y, z) - img.paste(Image.blend(t[0], black_color, black_coeff), (imgx, imgy), t[1]) - else: - # draw each face lit appropriately, - # but first just draw the block - img.paste(t[0], (imgx, imgy), t[1]) - - # top face - black_coeff, face_occlude = self.get_lighting_coefficient(x, y, z + 1) - if not face_occlude: - img.paste((0,0,0), (imgx, imgy), ImageEnhance.Brightness(facemasks[0]).enhance(black_coeff)) + # Draw the actual block on the image. For cave images, + # tint the block with a color proportional to its depth + if cave: + # no lighting for cave -- depth is probably more useful + img.paste(Image.blend(t[0],depth_colors[z],0.3), (imgx, imgy), t[1]) + else: + if not self.world.lighting: + # no lighting at all + img.paste(t[0], (imgx, imgy), t[1]) + elif blockid in transparent_blocks: + # transparent means draw the whole + # block shaded with the current + # block's light + black_coeff, _ = self.get_lighting_coefficient(x, y, z) + img.paste(Image.blend(t[0], black_color, black_coeff), (imgx, imgy), t[1]) + else: + # draw each face lit appropriately, + # but first just draw the block + img.paste(t[0], (imgx, imgy), t[1]) + + # top face + black_coeff, face_occlude = self.get_lighting_coefficient(x, y, z + 1) + if not face_occlude: + img.paste((0,0,0), (imgx, imgy), ImageEnhance.Brightness(facemasks[0]).enhance(black_coeff)) + + # left face + black_coeff, face_occlude = self.get_lighting_coefficient(x - 1, y, z) + if not face_occlude: + img.paste((0,0,0), (imgx, imgy), ImageEnhance.Brightness(facemasks[1]).enhance(black_coeff)) - # left face - black_coeff, face_occlude = self.get_lighting_coefficient(x - 1, y, z) - if not face_occlude: - img.paste((0,0,0), (imgx, imgy), ImageEnhance.Brightness(facemasks[1]).enhance(black_coeff)) + # right face + black_coeff, face_occlude = self.get_lighting_coefficient(x, y + 1, z) + if not face_occlude: + img.paste((0,0,0), (imgx, imgy), ImageEnhance.Brightness(facemasks[2]).enhance(black_coeff)) - # right face - black_coeff, face_occlude = self.get_lighting_coefficient(x, y + 1, z) - if not face_occlude: - img.paste((0,0,0), (imgx, imgy), ImageEnhance.Brightness(facemasks[2]).enhance(black_coeff)) + # Draw edge lines + if blockid in (44,): # step block + increment = 6 + elif blockid in (78,): # snow + increment = 9 + else: + increment = 0 - # Draw edge lines - 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+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+increment), (imgx+12,imgy+increment)), fill=(0,0,0), width=1) - - - finally: - # Do this no mater how the above block exits - imgy -= 12 + 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) + if y != 0 and blocks[x,y-1,z] == 0: + draw.line(((imgx,imgy+6+increment), (imgx+12,imgy+increment)), fill=(0,0,0), width=1) return img diff --git a/gmap.py b/gmap.py index d5f084c..1ba41d8 100755 --- a/gmap.py +++ b/gmap.py @@ -51,6 +51,7 @@ def main(): 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("--optimize-img", dest="optimizeimg", help="If using png, perform image file size optimizations on the output. Specify 1 for pngcrush, 2 for pngcrush+optipng+advdef. This may double (or more) render times, but will produce up to 30% smaller images. NOTE: requires corresponding programs in $PATH or %PATH%") 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.") @@ -100,6 +101,11 @@ def main(): else: imgformat = 'png' + if options.optimizeimg: + optimizeimg = options.optimizeimg + else: + optimizeimg = None + logging.getLogger().setLevel( logging.getLogger().level + 10*options.quiet) logging.getLogger().setLevel( @@ -113,7 +119,7 @@ def main(): w.go(options.procs) # Now generate the tiles - q = quadtree.QuadtreeGen(w, destdir, depth=options.zoom, imgformat=imgformat) + q = quadtree.QuadtreeGen(w, destdir, depth=options.zoom, imgformat=imgformat, optimizeimg=optimizeimg) q.go(options.procs) def delete_all(worlddir, tiledir): diff --git a/optimizeimages.py b/optimizeimages.py new file mode 100644 index 0000000..120b60b --- /dev/null +++ b/optimizeimages.py @@ -0,0 +1,36 @@ +# 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 . + +import os +import subprocess +import shlex + +def optimize_image(imgpath, imgformat, optimizeimg): + if imgformat == 'png': + if optimizeimg == "1" or optimizeimg == "2": + # we can't do an atomic replace here because windows is terrible + # so instead, we make temp files, delete the old ones, and rename + # the temp files. go windows! + subprocess.Popen(shlex.split("pngcrush " + imgpath + " " + imgpath + ".tmp"), + stderr=subprocess.STDOUT, stdout=subprocess.PIPE).communicate()[0] + os.remove(imgpath) + os.rename(imgpath+".tmp", imgpath) + + if optimizeimg == "2": + subprocess.Popen(shlex.split("optipng " + imgpath), stderr=subprocess.STDOUT, + stdout=subprocess.PIPE).communicate()[0] + subprocess.Popen(shlex.split("advdef -z4 " + imgpath), stderr=subprocess.STDOUT, + stdout=subprocess.PIPE).communicate()[0] + diff --git a/quadtree.py b/quadtree.py index 553c67f..8bd84ee 100644 --- a/quadtree.py +++ b/quadtree.py @@ -24,10 +24,12 @@ import shutil import collections import json import logging +import util from PIL import Image -import util +from optimizeimages import optimize_image + """ This module has routines related to generating a quadtree of tiles @@ -55,7 +57,7 @@ def catch_keyboardinterrupt(func): return newfunc class QuadtreeGen(object): - def __init__(self, worldobj, destdir, depth=None, imgformat=None): + def __init__(self, worldobj, destdir, depth=None, imgformat=None, optimizeimg=None): """Generates a quadtree from the world given into the given dest directory @@ -67,6 +69,7 @@ class QuadtreeGen(object): """ assert(imgformat) self.imgformat = imgformat + self.optimizeimg = optimizeimg if depth is None: # Determine quadtree depth (midpoint is always 0,0) @@ -128,11 +131,20 @@ class QuadtreeGen(object): with open(os.path.join(self.destdir, "index.html"), 'w') as output: output.write(html) - - + # write out the default marker table with open(os.path.join(self.destdir, "markers.js"), 'w') as output: output.write("var markerData=%s" % json.dumps(self.world.POI)) + # write out the default (empty, but documented) region table + with open(os.path.join(self.destdir, "regions.js"), 'w') as output: + output.write('var regionData=[\n') + output.write(' // {"color": "#FFAA00", "opacity": 0.5, "closed": true, "path": [\n') + output.write(' // {"x": 0, "y": 0, "z": 0},\n') + output.write(' // {"x": 0, "y": 10, "z": 0},\n') + output.write(' // {"x": 0, "y": 0, "z": 10}\n') + output.write(' // ]},\n') + output.write('];') + # Write a blank image blank = Image.new("RGBA", (1,1)) tileDir = os.path.join(self.destdir, "tiles") @@ -235,7 +247,8 @@ 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, self.imgformat)) + colstart, colend, rowstart, rowend, dest, self.imgformat, + self.optimizeimg)) def _apply_render_inntertile(self, pool, zoom): """Same as _apply_render_worltiles but for the inntertile routine. @@ -247,7 +260,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, self.imgformat)) + yield pool.apply_async(func=render_innertile, args= (dest, name, self.imgformat, self.optimizeimg)) def go(self, procs): """Renders all tiles""" @@ -331,7 +344,7 @@ class QuadtreeGen(object): pool.join() # Do the final one right here: - render_innertile(os.path.join(self.destdir, "tiles"), "base", self.imgformat) + render_innertile(os.path.join(self.destdir, "tiles"), "base", self.imgformat, self.optimizeimg) def _get_range_by_path(self, path): """Returns the x, y chunk coordinates of this tile""" @@ -362,7 +375,7 @@ class QuadtreeGen(object): return chunklist @catch_keyboardinterrupt -def render_innertile(dest, name, imgformat): +def render_innertile(dest, name, imgformat, optimizeimg): """ Renders a tile at os.path.join(dest, name)+".ext" by taking tiles from os.path.join(dest, name, "{0,1,2,3}.png") @@ -452,12 +465,15 @@ def render_innertile(dest, name, imgformat): img.save(imgpath, quality=95, subsampling=0) else: # png img.save(imgpath) + if optimizeimg: + optimize_image(imgpath, imgformat, optimizeimg) + with open(hashpath, "wb") as hashout: hashout.write(newhash) @catch_keyboardinterrupt -def render_worldtile(chunks, colstart, colend, rowstart, rowend, path, imgformat): +def render_worldtile(chunks, colstart, colend, rowstart, rowend, path, imgformat, optimizeimg): """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 @@ -586,6 +602,10 @@ def render_worldtile(chunks, colstart, colend, rowstart, rowend, path, imgformat # Save them tileimg.save(imgpath) + + if optimizeimg: + optimize_image(imgpath, imgformat, optimizeimg) + with open(hashpath, "wb") as hashout: hashout.write(digest) diff --git a/template.html b/template.html index 4387e8a..96ccae2 100644 --- a/template.html +++ b/template.html @@ -8,6 +8,7 @@ #mcmap { height: 100% } + @@ -155,6 +156,48 @@ } } + var regionsInit = false; + function initRegions() { + if (regionsInit) { return; } + + regionsInit = true; + + for (i in regionData) { + var region = regionData[i]; + var converted = new google.maps.MVCArray(); + for (j in region.path) { + var point = region.path[j]; + converted.push(fromWorldToLatLng(point.x, point.y, point.z)); + } + + if (region.closed) { + new google.maps.Polygon({ + clickable: false, + geodesic: false, + map: map, + strokeColor: region.color, + strokeOpacity: region.opacity, + strokeWeight: 2, + fillColor: region.color, + fillOpacity: region.opacity * 0.25, + zIndex: i, + paths: converted + }); + } else { + new google.maps.Polyline({ + clickable: false, + geodesic: false, + map: map, + strokeColor: region.color, + strokeOpacity: region.opacity, + strokeWeight: 2, + zIndex: i, + path: converted + }); + } + } + } + function initialize() { var mapOptions = { zoom: config.defaultZoom, @@ -162,6 +205,7 @@ navigationControl: true, scaleControl: false, mapTypeControl: false, + streetViewControl: false, mapTypeId: 'mcmap' }; map = new google.maps.Map(document.getElementById("mcmap"), mapOptions); @@ -187,8 +231,9 @@ // We can now set the map to use the 'coordinate' map type map.setMapTypeId('mcmap'); - // initialize the markers + // initialize the markers and regions initMarkers(); + initRegions(); } diff --git a/world.py b/world.py index c3083a6..8ffdef4 100644 --- a/world.py +++ b/world.py @@ -123,6 +123,11 @@ class WorldRenderer(object): chunklist.append((base36decode(p[1]), base36decode(p[2]), path)) + if not chunklist: + logging.error("No valid chunks specified in your chunklist!") + logging.error("HINT: chunks are in your world directory and have names of the form 'c.*.*.dat'") + sys.exit(1) + # Translate to col, row coordinates _, _, _, _, chunklist = _convert_coords(chunklist) @@ -216,8 +221,6 @@ 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: logging.error("Error: No chunks found!")