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