Merge branch 'master' into lighting
Conflicts: chunk.py gmap.py textures.py world.py
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
*.pyc
|
||||
12
README.rst
12
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=<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.
|
||||
|
||||
48
chunk.py
48
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:
|
||||
|
||||
32
gmap.py
32
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] <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()
|
||||
|
||||
100
quadtree.py
100
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
|
||||
|
||||
13
setup.py
Normal file
13
setup.py
Normal 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,
|
||||
}},
|
||||
|
||||
|
||||
)
|
||||
166
template.html
166
template.html
@@ -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>
|
||||
|
||||
270
textures.py
270
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)
|
||||
|
||||
29
util.py
Normal file
29
util.py
Normal 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])
|
||||
19
world.py
19
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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user