From 08a86a52abfabd59ac68b37dc7e5270bd7fb328a Mon Sep 17 00:00:00 2001 From: Andrew Date: Tue, 24 Aug 2010 21:11:57 -0400 Subject: [PATCH] uses multiprocessing to speed up rendering. Caches chunks --- chunk.py | 278 ++++++++++++++++++++++++++++++++++++++----------------- world.py | 22 ++++- 2 files changed, 212 insertions(+), 88 deletions(-) diff --git a/chunk.py b/chunk.py index c398ada..9d06a04 100644 --- a/chunk.py +++ b/chunk.py @@ -1,6 +1,8 @@ import numpy -from PIL import Image +from PIL import Image, ImageDraw from itertools import izip, count +import os.path +import hashlib import nbt import textures @@ -38,105 +40,211 @@ def get_skylight_array(level): """ return numpy.frombuffer(level['SkyLight'], dtype=numpy.uint8).reshape((16,16,64)) -# This set holds blocks ids that can be seen through +# This set holds blocks ids that can be seen through, for occlusion calculations transparent_blocks = set([0, 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]) -def chunk_render(chunkfile, img=None, xoff=0, yoff=0, cave=False): - level = get_lvldata(chunkfile) - blocks = get_blockarray(level) - if cave: - skylight = get_skylight_array(level) - # Cave mode. Actually go through and 0 out all blocks that are not in a - # cave, so that it only renders caves. +def render_and_save(chunkfile, cave=False): + a = ChunkRenderer(chunkfile) + return a.render_and_save(cave) - # 1st task: this array is 2 blocks per byte, expand it so we can just - # do a bitwise and on the arrays - skylight_expanded = numpy.empty((16,16,128), dtype=numpy.uint8) - # Even elements get the lower 4 bits - skylight_expanded[:,:,::2] = skylight & 0x0F - # Odd elements get the upper 4 bits - skylight_expanded[:,:,1::2] = skylight >> 4 +class ChunkRenderer(object): + def __init__(self, chunkfile): + if not os.path.exists(chunkfile): + raise ValueError("Could not find chunkfile") + self.chunkfile = chunkfile - # Places where the skylight is not 0 (there's some amount of skylight - # touching it) change it to something that won't get rendered, AND - # won't get counted as "transparent". - blocks = blocks.copy() - blocks[skylight_expanded != 0] = 21 + def _load_level(self): + """Loads and returns the level structure""" + if not hasattr(self, "_level"): + self._level = get_lvldata(self.chunkfile) + return self._level + level = property(_load_level) + + def _load_blocks(self): + """Loads and returns the block array""" + if not hasattr(self, "_blocks"): + self._blocks = get_blockarray(self._load_level()) + return self._blocks + blocks = property(_load_blocks) - # Don't render + def _hash_blockarray(self): + """Finds a hash of the block array""" + h = hashlib.md5() + h.update(self.level['Blocks']) + digest = h.hexdigest() + # 6 digits ought to be plenty + return digest[:6] - # Each block is 24x24 - # The next block on the X axis adds 12px to x and subtracts 6px from y in the image - # The next block on the Y axis adds 12px to x and adds 6px to y in the image - # The next block up on the Z axis subtracts 12 from y axis in the image + def render_and_save(self, cave=False): + """Render the chunk using chunk_render, and then save it to a file in + the same directory as the source image. If the file already exists and + is up to date, this method doesn't render anything. + """ + destdir, filename = os.path.split(self.chunkfile) + destdir = os.path.abspath(destdir) + blockid = ".".join(filename.split(".")[1:3]) + dest_filename = "img.{0}.{1}.{2}.png".format( + blockid, + "cave" if cave else "nocave", + self._hash_blockarray(), + ) - # Since there are 16x16x128 blocks in a chunk, the image will be 384x1728 - # (height is 128*24 high, plus the size of the horizontal plane: 16*12) - if not img: - img = Image.new("RGBA", (384, 1728)) + dest_path = os.path.join(destdir, dest_filename) - 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): - try: + if os.path.exists(dest_path): + return dest_path + else: + # Remove old images for this chunk + for oldimg in os.listdir(destdir): + if oldimg.startswith("img.{0}.{1}.".format(blockid, + "cave" if cave else "nocave")) and \ + oldimg.endswith(".png"): + os.unlink(os.path.join(destdir,oldimg)) + break - blockid = blocks[x,y,z] - t = textures.blockmap[blockid] - if not t: - continue + # Render the chunk + img = self.chunk_render(cave=cave) + # Save it + img.save(dest_path) + # Return its location + return dest_path - # Check if this block is occluded - if cave and ( - x == 0 and y != 15 and z != 127 - ): - # If it's on the x face, only render if there's a - # transparent block in the y+1 direction OR the z-1 - # direction - if ( - blocks[x,y+1,z] not in transparent_blocks and - blocks[x,y,z+1] not in transparent_blocks - ): + def chunk_render(self, img=None, xoff=0, yoff=0, cave=False): + """Renders a chunk with the given parameters, and returns the image. + If img is given, the chunk is rendered to that image object. Otherwise, + a new one is created. xoff and yoff are offsets in the image. + + For cave mode, all blocks that have any direct sunlight are not + rendered, and blocks are drawn with a color tint depending on their + depth.""" + blocks = self.blocks + if cave: + skylight = get_skylight_array(self.level) + # Cave mode. Actually go through and 0 out all blocks that are not in a + # cave, so that it only renders caves. + + # 1st task: this array is 2 blocks per byte, expand it so we can just + # do a bitwise and on the arrays + skylight_expanded = numpy.empty((16,16,128), dtype=numpy.uint8) + # Even elements get the lower 4 bits + skylight_expanded[:,:,::2] = skylight & 0x0F + # Odd elements get the upper 4 bits + skylight_expanded[:,:,1::2] = skylight >> 4 + + # Places where the skylight is not 0 (there's some amount of skylight + # touching it) change it to something that won't get rendered, AND + # won't get counted as "transparent". + blocks = blocks.copy() + blocks[skylight_expanded != 0] = 21 + + + # Each block is 24x24 + # The next block on the X axis adds 12px to x and subtracts 6px from y in the image + # The next block on the Y axis adds 12px to x and adds 6px to y in the image + # The next block up on the Z axis subtracts 12 from y axis in the image + + # Since there are 16x16x128 blocks in a chunk, the image will be 384x1728 + # (height is 128*24 high, plus the size of the horizontal plane: 16*12) + if not img: + img = Image.new("RGBA", (384, 1728)) + + 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): + try: + blockid = blocks[x,y,z] + t = textures.blockmap[blockid] + if not t: continue - elif cave and ( - y == 15 and x != 0 and z != 127 - ): - # 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 - # direction - if ( - blocks[x-1,y,z] not in transparent_blocks and - blocks[x,y,z+1] not in transparent_blocks + + # Check if this block is occluded + if cave and ( + x == 0 and y != 15 and z != 127 ): - continue - elif cave and ( - y == 15 and x == 0 - ): - # If it's on the facing edge, only render if what's - # above it is transparent - if ( - blocks[x,y,z+1] not in transparent_blocks + # If it's on the x face, only render if there's a + # transparent block in the y+1 direction OR the z-1 + # direction + if ( + blocks[x,y+1,z] not in transparent_blocks and + blocks[x,y,z+1] not in transparent_blocks + ): + continue + elif cave and ( + y == 15 and x != 0 and z != 127 ): + # 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 + # direction + if ( + blocks[x-1,y,z] not in transparent_blocks and + blocks[x,y,z+1] not in transparent_blocks + ): + continue + elif cave and ( + y == 15 and x == 0 + ): + # If it's on the facing edge, only render if what's + # above it is transparent + if ( + blocks[x,y,z+1] not in transparent_blocks + ): + continue + elif ( + # Normal block or not cave mode, check sides for + # transparentcy or render unconditionally if it's + # on a shown face + x != 0 and y != 15 and z != 127 and + blocks[x-1,y,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 + ): + # Don't render if all sides aren't transparent and + # we're not on the edge continue - elif ( - # Normal block or not cave mode, check sides for - # transparentcy or render unconditionally if it's - # on a shown face - x != 0 and y != 15 and z != 127 and - blocks[x-1,y,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 - ): - # Don't render if all sides aren't transparent and - # we're not on the edge - continue - img.paste(t[0], (imgx, imgy), t[1]) + # Draw the actual block on the image. For cave images, + # tint the block with a color proportional to its depth + if cave: + img.paste(Image.blend(t[0],depth_colors[z],0.3), (imgx, imgy), t[1]) + else: + img.paste(t[0], (imgx, imgy), t[1]) - finally: - # Do this no mater how the above block exits - imgy -= 12 + # Draw edge lines + if blockid not in transparent_blocks: + draw = ImageDraw.Draw(img) + if x != 15 and blocks[x+1,y,z] == 0: + draw.line(((imgx+12,imgy), (imgx+24,imgy+6)), 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) - return img + + finally: + # Do this no mater how the above block exits + imgy -= 12 + + return img + + +# Render 128 different color images for color coded depth blending in cave mode +def generate_depthcolors(): + depth_colors = [] + r = 255 + g = 0 + b = 0 + for z in range(128): + img = Image.new("RGB", (24,24), (r,g,b)) + depth_colors.append(img) + if z < 32: + g += 7 + elif z < 64: + r -= 7 + elif z < 96: + b += 7 + else: + g -= 7 + + return depth_colors +depth_colors = generate_depthcolors() diff --git a/world.py b/world.py index 6934326..b0dc256 100644 --- a/world.py +++ b/world.py @@ -3,6 +3,7 @@ import string import os import os.path import time +import multiprocessing from PIL import Image @@ -42,7 +43,7 @@ def find_chunkfiles(worlddir): os.path.join(dirpath, f))) return all_chunks -def render_world(worlddir): +def render_world(worlddir, cavemode=False): print "Scanning chunks..." all_chunks = find_chunkfiles(worlddir) @@ -109,6 +110,14 @@ def render_world(worlddir): print "Sorting chunks..." all_chunks.sort(key=lambda x: x[1]-x[0]) + print "Starting chunk processors..." + pool = multiprocessing.Pool(processes=3) + resultsmap = {} + for chunkx, chunky, chunkfile in all_chunks: + result = pool.apply_async(chunk.render_and_save, args=(chunkfile,), + kwds=dict(cave=cavemode)) + resultsmap[(chunkx, chunky)] = result + print "Processing chunks!" processed = 0 starttime = time.time() @@ -128,8 +137,13 @@ def render_world(worlddir): print "It's in column {0} row {1}".format(column, row) # Read it and render - chunk.chunk_render(chunkfile, worldimg, imgx, imgy, cave=True) - # chunk chunk chunk chunk + result = resultsmap[(chunkx, chunky)] + chunkimagefile = result.get() + chunkimg = Image.open(chunkimagefile) + # Draw the image sans alpha layer, using the alpha layer as a mask. (We + # don't want the alpha layer actually drawn on the image, this pastes + # it as if it was a layer) + worldimg.paste(chunkimg.convert("RGB"), (imgx, imgy), chunkimg.split()[3]) processed += 1 @@ -137,4 +151,6 @@ def render_world(worlddir): (time.time()-starttime)/processed) print "All done!" + print "Took {0} minutes".format((time.time()-starttime)/60) return worldimg +