0

Merge branch 'master' into lighting

Conflicts:
	chunk.py
	gmap.py
	textures.py
	world.py
This commit is contained in:
Aaron Griffith
2010-10-05 08:35:23 -04:00
10 changed files with 532 additions and 158 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
*.pyc

View File

@@ -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=<chunk cache dir> <world> <output dir>
--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 <nr_procs> 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.

View File

@@ -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:

32
gmap.py
View File

@@ -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] <World # / Path to World> <tiles dest dir>
%prog -d <World # / Path to World / Path to cache dir> [tiles dest dir]
"""
%prog -d <World # / Path to World / Path to cache dir> [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()

View File

@@ -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

13
setup.py Normal file
View File

@@ -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,
}},
)

View File

@@ -14,14 +14,73 @@
<script type="text/javascript">
var config = {
path: 'tiles',
fileExt: 'png',
fileExt: '{imgformat}',
tileSize: 384,
defaultZoom: 1,
maxZoom: {maxzoom},
cacheMinutes: 0, // Change this to have browsers automatically requiest new images every x minutes
cacheMinutes: 0, // Change this to have browsers automatically request new images every x minutes
debug: false
};
// our custom projection maps Latitude to Y, and Longitude to X as normal,
// but it maps the range [0.0, 1.0] to [0, tileSize] in both directions
// so it is easier to position markers, etc. based on their position
// (find their position in the lowest-zoom image, and divide by tileSize)
function MCMapProjection() {
this.inverseTileSize = 1.0 / config.tileSize;
}
MCMapProjection.prototype.fromLatLngToPoint = function(latLng) {
var x = latLng.lng() * config.tileSize;
var y = latLng.lat() * config.tileSize;
return new google.maps.Point(x, y);
};
MCMapProjection.prototype.fromPointToLatLng = function(point) {
var lng = point.x * this.inverseTileSize;
var lat = point.y * this.inverseTileSize;
return new google.maps.LatLng(lat, lng);
};
// helper to get map LatLng from world coordinates
// takes arguments in X, Y, Z order
// (arguments are *out of order*, because within the function we use
// the axes like the rest of Minecraft Overviewer -- with the Z and Y
// flipped from normal minecraft usage.)
function fromWorldToLatLng(x, z, y)
{
// the width and height of all the highest-zoom tiles combined, inverted
var perPixel = 1.0 / (config.tileSize * Math.pow(2, config.maxZoom));
// This information about where the center column is may change with a different
// drawing implementation -- check it again after any drawing overhauls!
// point (0, 0, 127) is at (0.5, 0.0) of tile (tiles/2 - 1, tiles/2)
// so the Y coordinate is at 0.5, and the X is at 0.5 - ((tileSize / 2) / (tileSize * 2^maxZoom))
// or equivalently, 0.5 - (1 / 2^(maxZoom + 1))
var lng = 0.5 - (1.0 / Math.pow(2, config.maxZoom + 1));
var lat = 0.5;
// the following metrics mimic those in ChunkRenderer.chunk_render in "chunk.py"
// each block on X axis adds 12px to x and subtracts 6px from y
lng += 12 * x * perPixel;
lat -= 6 * x * perPixel;
// each block on Y axis adds 12px to x and adds 6px to y
lng += 12 * y * perPixel;
lat += 6 * y * perPixel;
// each block down along Z adds 12px to y
lat += 12 * (128 - z) * perPixel;
// add on 12 px to the X coordinate and 18px to the Y to center our point
lng += 12 * perPixel;
lat += 18 * perPixel;
return new google.maps.LatLng(lat, lng);
}
var MCMapOptions = {
getTileUrl: function(tile, zoom) {
var url = config.path;
@@ -52,6 +111,7 @@
var MCMapType = new google.maps.ImageMapType(MCMapOptions);
MCMapType.name = "MC Map";
MCMapType.alt = "Minecraft Map";
MCMapType.projection = new MCMapProjection();
function CoordMapType() {
}
@@ -75,61 +135,30 @@
};
var map;
var prot;
var markersInit = false;
function convertCoords (x,y,z) {
var imgx = 0;
var imgy = 0;
imgx = imgx + (12*x);
imgy = imgy - (6*x);
imgx = imgx + (12 * y);
imgy = imgy + (6* y);
imgy = imgy - (12*z);
// this math is mysterious. i don't fully understand it
// but the idea is to assume that block 0,0,0 in chunk 0,0
// is drawn in the very middle of the gmap at (192,192)
return [192*Math.pow(2,config.maxZoom)+imgx, 192*Math.pow(2,config.maxZoom)+imgy+768+768];
}
function initMarkers() {
if (markersInit) { return; }
markersInit = true;
prot = map.getProjection();
for (i in markerData) {
var item = markerData[i];
var converted = convertCoords(item.x-16, item.z, item.y);
var x = converted[0] / Math.pow(2, config.maxZoom);
var y = converted[1] / Math.pow(2, config.maxZoom);
var p = new google.maps.Point(x,y);
var marker = new google.maps.Marker({
position: prot.fromPointToLatLng(p),
map: map,
title:item.msg
});
}
}
if (markersInit) { return; }
markersInit = true;
for (i in markerData) {
var item = markerData[i];
var converted = fromWorldToLatLng(item.x, item.y, item.z);
var marker = new google.maps.Marker({
position: converted,
map: map,
title: item.msg
});
}
}
function initialize() {
var mapOptions = {
zoom: config.defaultZoom,
center: new google.maps.LatLng(-45, 90),
center: new google.maps.LatLng(0.5, 0.5),
navigationControl: true,
scaleControl: false,
mapTypeControl: false,
@@ -139,34 +168,27 @@ title:item.msg
if(config.debug) {
map.overlayMapTypes.insertAt(0, new CoordMapType(new google.maps.Size(config.tileSize, config.tileSize)));
google.maps.event.addListener(map, 'click', function(event) {
console.log("latLng; " + event.latLng.lat() + ", " + event.latLng.lng());
var pnt = map.getProjection().fromLatLngToPoint(event.latLng);
console.log("point: " + pnt);
var pxx = pnt.x * config.tileSize * Math.pow(2, config.maxZoom);
var pxy = pnt.y * config.tileSize * Math.pow(2, config.maxZoom);
console.log("pixel: " + pxx + ", " + pxy);
});
}
// Now attach the coordinate map type to the map's registry
map.mapTypes.set('mcmap', MCMapType);
// We can now set the map to use the 'coordinate' map type
map.setMapTypeId('mcmap');
prot = map.getProjection();
if (config.debug)
google.maps.event.addListener(map, 'click', function(event) {
console.log("latLng: " + event.latLng.lat() + ", " + event.latLng.lng());
var pnt = prot.fromLatLngToPoint(event.latLng);
console.log("point: " + pnt);//
var pxx = pnt.x * Math.pow(2,config.maxZoom);
var pxy = pnt.y * Math.pow(2,config.maxZoom);
console.log("pixel: " + pxx + ", " + pxy);
});
google.maps.event.addListener(map, 'projection_changed', function(event) {
initMarkers();
});
// initialize the markers
initMarkers();
}
</script>
</head>

View File

@@ -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)

29
util.py Normal file
View File

@@ -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 <http://www.gnu.org/licenses/>.
"""
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])

View File

@@ -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