My Minecraft server is seeing some use again, and I decided to build a life size model of the Philadelphia Museum of Art. I also thought it would be cool to have an animated gif of the build progress as things go.
Configuring Overviewer
We use Minecraft Overviewer to generate Google-maps style views of our world for the web. I created a config file limiting the render area to the coordinates around the building
worlds["Main"] = "/minecraft/Minecraft/world"
renders["normalrender"] = {
"world": "Main",
"title": "Overworld",
"dimension": "overworld",
"crop" : (200, -90, 420, 70),
}
outputdir="/minecraft/renders/museum"
Compositing the tiles
I found a script for making composites from google map data, originally written for use with Overviewer, but it was pretty far out of date and written for a different version of python than what I’ve got installed. I used it as a jumping off point for writing my own composite script.
#!/usr/bin/env python
import Image, ImageChops
import os, fnmatch
import os.path
import re
import sys
CHUNK_SIZE = 384
def trim(im):
bg = Image.new(im.mode, im.size, im.getpixel((0,0)))
diff = ImageChops.difference(im, bg)
diff = ImageChops.add(diff, diff, 2.0, -100)
bbox = diff.getbbox()
if bbox:
return im.crop(bbox)
def find_files(directory, pattern):
regex = re.compile(pattern)
for root, dirs, files in os.walk(directory):
for basename in files:
if regex.match(basename):
filename = os.path.join(root, basename)
yield filename
def getAllFiles(srcdir):
return find_files(srcdir, "[0-9]+.png")
def getCoordinates(f):
return map(lambda x: int(x), re.findall(r'[0-9-]+', f))
def getX(c):
return {
0: 0,
1: 1,
2: 0,
3: 1,
}[c]
def getY(c):
return {
0:0,
1:0,
2:1,
3:1,
}[c]
if len(sys.argv) != 4:
print "Usage:", sys.argv[0], "<source directory (Dir)> <output file> <zoom level>"
sys.exit(1)
sourceDirectory = sys.argv[1]
zoomLevel = int(sys.argv[3])
outputName = sys.argv[2]
width = (2**zoomLevel) * CHUNK_SIZE
height = (2**zoomLevel) * CHUNK_SIZE
print "Width:", width, "Height:", height
output = Image.new("RGBA", (width, height))
for f in getAllFiles(sourceDirectory):
coords = getCoordinates(f)
if len(coords) == zoomLevel:
chunk = Image.open(os.path.join(sourceDirectory, f))
#print chunk
xbin = ""
ybin = ""
for c in coords:
xbin = xbin + str(getX(c))
ybin = ybin + str(getY(c))
y = int(ybin,2)
x = int(xbin,2)
output.paste(chunk, (x*CHUNK_SIZE, y*CHUNK_SIZE))
print "Map merged, saving..."
output = trim(output)
if outputName[-3:] == "jpg" or outputName[-4:] == "jpeg":
output.save(outputName, quality=100)
else:
try:
output.save(outputName, quality=85, progressive=True, optimize=True)
except:
print "Error saving with progressive=True and optimize=True, trying normally..."
output.save(outputName, quality=85)
print "Done!"
This generates a daily snapshot and puts it in a web-accessible folder. I can then make a gif of all the images in that folder with ImageMagick’s convert utility
convert -delay 80 -loop 0 *jpg animated.gif
Checking for modifications
Originally I ran the script once daily on a cron, but later decided to run the world generator every half hour and only generate an image if there’s something new to see.
#!/bin/bash rendersecs=$(expr `date +%s` - `stat -c %Y /minecraft/renders/museum/normalrender/3/`) snapsecs=$(expr `date +%s` - `stat -c %Y /minecraft/renders/museum/last-snapshot`) if [ "$rendersecs" -lt "$snapsecs" ]; then echo "Render was modified $rendersecs secs ago. Last snapshot $snapsecs secondds ago. Updating snapshot." /minecraft/renders/merge.py /minecraft/renders/museum /var/www/html/museum/$(date +\%Y-\%m-\%d-\%H\%M).jpg 3 touch -m /minecraft/renders/museum/last-snapshot convert -delay 40 -loop 0 /var/www/html/museum/*jpg /var/www/html/museum/animated.gif fi
Setting up cron tasks
I put two new jobs in my crontab file: one to generate the terrain and one to run my shell script. I give Overviewer a bit of a head start in case it has a lot of work to do.
*/30 * * * * overviewer.py --conifg=/minecraft/overviewer-museum.conf 10,40 * * * * /minecraft/update-museum.sh



