0

Merge branch 'master' into lighting

Conflicts:
	chunk.py
This commit is contained in:
Aaron Griffith
2010-10-14 15:01:31 -04:00
6 changed files with 246 additions and 131 deletions

241
chunk.py
View File

@@ -80,6 +80,21 @@ def get_blockdata_array(level):
in a similar manner to skylight data""" in a similar manner to skylight data"""
return numpy.frombuffer(level['Data'], dtype=numpy.uint8).reshape((16,16,64)) 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 # 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, 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]) 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: if not img:
img = Image.new("RGBA", (384, 1728), (38,92,255,0)) img = Image.new("RGBA", (384, 1728), (38,92,255,0))
for x in xrange(15,-1,-1): for x,y,z,imgx,imgy in iterate_chunkblocks(xoff,yoff):
for y in xrange(16): blockid = blocks[x,y,z]
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]
# the following blocks don't have textures that can be pre-computed from the blockid # the following blocks don't have textures that can be pre-computed from the blockid
# alone. additional data is required. # alone. additional data is required.
# TODO torches, redstone torches, crops, ladders, stairs, # TODO torches, redstone torches, crops, ladders, stairs,
# levers, doors, buttons, and signs all need to be handled here (and in textures.py) # levers, doors, buttons, and signs all need to be handled here (and in textures.py)
## minecart track, crops, ladder, doors, etc. ## minecart track, crops, ladder, doors, etc.
if blockid in textures.special_blocks: if blockid in textures.special_blocks:
# also handle furnaces here, since one side has a different texture than the other # also handle furnaces here, since one side has a different texture than the other
ancilData = blockData_expanded[x,y,z] ancilData = blockData_expanded[x,y,z]
try: try:
t = textures.specialblockmap[(blockid, ancilData)] t = textures.specialblockmap[(blockid, ancilData)]
except KeyError: except KeyError:
t = None t = None
else: else:
t = textures.blockmap[blockid] t = textures.blockmap[blockid]
if not t: if not t:
continue continue
# Check if this block is occluded # Check if this block is occluded
if cave and ( if cave and (
x == 0 and y != 15 and z != 127 x == 0 and y != 15 and z != 127
): ):
# If it's on the x face, only render if there's a # If it's on the x face, only render if there's a
# transparent block in the y+1 direction OR the z-1 # transparent block in the y+1 direction OR the z-1
# direction # direction
if ( if (
blocks[x,y+1,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 blocks[x,y,z+1] not in transparent_blocks
): ):
continue continue
elif cave and ( elif cave and (
y == 15 and x != 0 and z != 127 y == 15 and x != 0 and z != 127
): ):
# If it's on the facing y face, only render if there's # 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 # a transparent block in the x-1 direction OR the z-1
# direction # direction
if ( if (
blocks[x-1,y,z] not in transparent_blocks and blocks[x-1,y,z] not in transparent_blocks and
blocks[x,y,z+1] not in transparent_blocks blocks[x,y,z+1] not in transparent_blocks
): ):
continue continue
elif cave and ( elif cave and (
y == 15 and x == 0 y == 15 and x == 0
): ):
# If it's on the facing edge, only render if what's # If it's on the facing edge, only render if what's
# above it is transparent # above it is transparent
if ( if (
blocks[x,y,z+1] not in transparent_blocks blocks[x,y,z+1] not in transparent_blocks
): ):
continue continue
elif ( elif (
# Normal block or not cave mode, check sides for # Normal block or not cave mode, check sides for
# transparentcy or render unconditionally if it's # transparentcy or render unconditionally if it's
# on a shown face # on a shown face
x != 0 and y != 15 and z != 127 and x != 0 and y != 15 and z != 127 and
blocks[x-1,y,z] not in transparent_blocks and blocks[x-1,y,z] not in transparent_blocks and
blocks[x,y+1,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 blocks[x,y,z+1] not in transparent_blocks
): ):
# Don't render if all sides aren't transparent and # Don't render if all sides aren't transparent and
# we're not on the edge # we're not on the edge
continue continue
# Draw the actual block on the image. For cave images, # Draw the actual block on the image. For cave images,
# tint the block with a color proportional to its depth # tint the block with a color proportional to its depth
if cave: if cave:
# no lighting for cave -- depth is probably more useful # no lighting for cave -- depth is probably more useful
img.paste(Image.blend(t[0],depth_colors[z],0.3), (imgx, imgy), t[1]) img.paste(Image.blend(t[0],depth_colors[z],0.3), (imgx, imgy), t[1])
else: else:
if not self.world.lighting: if not self.world.lighting:
# no lighting at all # no lighting at all
img.paste(t[0], (imgx, imgy), t[1]) img.paste(t[0], (imgx, imgy), t[1])
elif blockid in transparent_blocks: elif blockid in transparent_blocks:
# transparent means draw the whole # transparent means draw the whole
# block shaded with the current # block shaded with the current
# block's light # block's light
black_coeff, _ = self.get_lighting_coefficient(x, y, z) black_coeff, _ = self.get_lighting_coefficient(x, y, z)
img.paste(Image.blend(t[0], black_color, black_coeff), (imgx, imgy), t[1]) img.paste(Image.blend(t[0], black_color, black_coeff), (imgx, imgy), t[1])
else: else:
# draw each face lit appropriately, # draw each face lit appropriately,
# but first just draw the block # but first just draw the block
img.paste(t[0], (imgx, imgy), t[1]) img.paste(t[0], (imgx, imgy), t[1])
# top face # top face
black_coeff, face_occlude = self.get_lighting_coefficient(x, y, z + 1) black_coeff, face_occlude = self.get_lighting_coefficient(x, y, z + 1)
if not face_occlude: if not face_occlude:
img.paste((0,0,0), (imgx, imgy), ImageEnhance.Brightness(facemasks[0]).enhance(black_coeff)) 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 # right face
black_coeff, face_occlude = self.get_lighting_coefficient(x - 1, y, z) black_coeff, face_occlude = self.get_lighting_coefficient(x, y + 1, z)
if not face_occlude: if not face_occlude:
img.paste((0,0,0), (imgx, imgy), ImageEnhance.Brightness(facemasks[1]).enhance(black_coeff)) img.paste((0,0,0), (imgx, imgy), ImageEnhance.Brightness(facemasks[2]).enhance(black_coeff))
# right face # Draw edge lines
black_coeff, face_occlude = self.get_lighting_coefficient(x, y + 1, z) if blockid in (44,): # step block
if not face_occlude: increment = 6
img.paste((0,0,0), (imgx, imgy), ImageEnhance.Brightness(facemasks[2]).enhance(black_coeff)) elif blockid in (78,): # snow
increment = 9
else:
increment = 0
# Draw edge lines if blockid not in transparent_blocks or blockid in (78,): #special case snow so the outline is still drawn
if blockid in (44,): # step block draw = ImageDraw.Draw(img)
increment = 6 if x != 15 and blocks[x+1,y,z] == 0:
elif blockid in (78,): # snow draw.line(((imgx+12,imgy+increment), (imgx+22,imgy+5+increment)), fill=(0,0,0), width=1)
increment = 9 if y != 0 and blocks[x,y-1,z] == 0:
else: draw.line(((imgx,imgy+6+increment), (imgx+12,imgy+increment)), fill=(0,0,0), width=1)
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
return img return img

View File

@@ -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("--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("--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("--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("-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.") 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: else:
imgformat = 'png' imgformat = 'png'
if options.optimizeimg:
optimizeimg = options.optimizeimg
else:
optimizeimg = None
logging.getLogger().setLevel( logging.getLogger().setLevel(
logging.getLogger().level + 10*options.quiet) logging.getLogger().level + 10*options.quiet)
logging.getLogger().setLevel( logging.getLogger().setLevel(
@@ -113,7 +119,7 @@ def main():
w.go(options.procs) w.go(options.procs)
# Now generate the tiles # 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) q.go(options.procs)
def delete_all(worlddir, tiledir): def delete_all(worlddir, tiledir):

36
optimizeimages.py Normal file
View File

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

View File

@@ -24,10 +24,12 @@ import shutil
import collections import collections
import json import json
import logging import logging
import util
from PIL import Image from PIL import Image
import util from optimizeimages import optimize_image
""" """
This module has routines related to generating a quadtree of tiles This module has routines related to generating a quadtree of tiles
@@ -55,7 +57,7 @@ def catch_keyboardinterrupt(func):
return newfunc return newfunc
class QuadtreeGen(object): 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 """Generates a quadtree from the world given into the
given dest directory given dest directory
@@ -67,6 +69,7 @@ class QuadtreeGen(object):
""" """
assert(imgformat) assert(imgformat)
self.imgformat = imgformat self.imgformat = imgformat
self.optimizeimg = optimizeimg
if depth is None: if depth is None:
# Determine quadtree depth (midpoint is always 0,0) # 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: with open(os.path.join(self.destdir, "index.html"), 'w') as output:
output.write(html) output.write(html)
# write out the default marker table
with open(os.path.join(self.destdir, "markers.js"), 'w') as output: with open(os.path.join(self.destdir, "markers.js"), 'w') as output:
output.write("var markerData=%s" % json.dumps(self.world.POI)) 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 # Write a blank image
blank = Image.new("RGBA", (1,1)) blank = Image.new("RGBA", (1,1))
tileDir = os.path.join(self.destdir, "tiles") tileDir = os.path.join(self.destdir, "tiles")
@@ -235,7 +247,8 @@ class QuadtreeGen(object):
# (even if tilechunks is empty, render_worldtile will delete # (even if tilechunks is empty, render_worldtile will delete
# existing images if appropriate) # existing images if appropriate)
yield pool.apply_async(func=render_worldtile, args= (tilechunks, 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): def _apply_render_inntertile(self, pool, zoom):
"""Same as _apply_render_worltiles but for the inntertile routine. """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])) dest = os.path.join(self.destdir, "tiles", *(str(x) for x in path[:-1]))
name = str(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): def go(self, procs):
"""Renders all tiles""" """Renders all tiles"""
@@ -331,7 +344,7 @@ class QuadtreeGen(object):
pool.join() pool.join()
# Do the final one right here: # 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): def _get_range_by_path(self, path):
"""Returns the x, y chunk coordinates of this tile""" """Returns the x, y chunk coordinates of this tile"""
@@ -362,7 +375,7 @@ class QuadtreeGen(object):
return chunklist return chunklist
@catch_keyboardinterrupt @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 Renders a tile at os.path.join(dest, name)+".ext" by taking tiles from
os.path.join(dest, name, "{0,1,2,3}.png") 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) img.save(imgpath, quality=95, subsampling=0)
else: # png else: # png
img.save(imgpath) img.save(imgpath)
if optimizeimg:
optimize_image(imgpath, imgformat, optimizeimg)
with open(hashpath, "wb") as hashout: with open(hashpath, "wb") as hashout:
hashout.write(newhash) hashout.write(newhash)
@catch_keyboardinterrupt @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 """Renders just the specified chunks into a tile and save it. Unlike usual
python conventions, rowend and colend are inclusive. Additionally, the python conventions, rowend and colend are inclusive. Additionally, the
chunks around the edges are half-way cut off (so that neighboring tiles 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 # Save them
tileimg.save(imgpath) tileimg.save(imgpath)
if optimizeimg:
optimize_image(imgpath, imgformat, optimizeimg)
with open(hashpath, "wb") as hashout: with open(hashpath, "wb") as hashout:
hashout.write(digest) hashout.write(digest)

View File

@@ -8,6 +8,7 @@
#mcmap { height: 100% } #mcmap { height: 100% }
</style> </style>
<script type="text/javascript" src="markers.js"></script> <script type="text/javascript" src="markers.js"></script>
<script type="text/javascript" src="regions.js"></script>
<script type="text/javascript" <script type="text/javascript"
src="http://maps.google.com/maps/api/js?sensor=false"> src="http://maps.google.com/maps/api/js?sensor=false">
</script> </script>
@@ -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() { function initialize() {
var mapOptions = { var mapOptions = {
zoom: config.defaultZoom, zoom: config.defaultZoom,
@@ -162,6 +205,7 @@
navigationControl: true, navigationControl: true,
scaleControl: false, scaleControl: false,
mapTypeControl: false, mapTypeControl: false,
streetViewControl: false,
mapTypeId: 'mcmap' mapTypeId: 'mcmap'
}; };
map = new google.maps.Map(document.getElementById("mcmap"), mapOptions); 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 // We can now set the map to use the 'coordinate' map type
map.setMapTypeId('mcmap'); map.setMapTypeId('mcmap');
// initialize the markers // initialize the markers and regions
initMarkers(); initMarkers();
initRegions();
} }
</script> </script>
</head> </head>

View File

@@ -123,6 +123,11 @@ class WorldRenderer(object):
chunklist.append((base36decode(p[1]), base36decode(p[2]), chunklist.append((base36decode(p[1]), base36decode(p[2]),
path)) 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 # Translate to col, row coordinates
_, _, _, _, chunklist = _convert_coords(chunklist) _, _, _, _, chunklist = _convert_coords(chunklist)
@@ -216,8 +221,6 @@ class WorldRenderer(object):
p = f.split(".") p = f.split(".")
all_chunks.append((base36decode(p[1]), base36decode(p[2]), all_chunks.append((base36decode(p[1]), base36decode(p[2]),
os.path.join(dirpath, f))) os.path.join(dirpath, f)))
logging.debug((base36decode(p[1]), base36decode(p[2]),
os.path.join(dirpath, f)))
if not all_chunks: if not all_chunks:
logging.error("Error: No chunks found!") logging.error("Error: No chunks found!")