From a15390cc47c6ec64e2d77e3a0813bd3e7ea48171 Mon Sep 17 00:00:00 2001 From: Aaron Griffith Date: Mon, 27 Sep 2010 14:55:16 -0400 Subject: [PATCH 01/13] initial stab at lighting support --- chunk.py | 44 ++++++++++++++++++++++++++++---------------- 1 file changed, 28 insertions(+), 16 deletions(-) diff --git a/chunk.py b/chunk.py index c548afa..d2308e5 100644 --- a/chunk.py +++ b/chunk.py @@ -54,11 +54,26 @@ def get_blockarray_fromfile(filename): return get_blockarray(level) def get_skylight_array(level): - """Returns the skylight array. Remember this is 4 bits per block, so divide - the z component by 2 when accessing the array. and mask off the top or - bottom 4 bits if it's odd or even respectively - """ - return numpy.frombuffer(level['SkyLight'], dtype=numpy.uint8).reshape((16,16,64)) + """Returns the skylight array. This is 4 bits per block, but it is + expanded for you so you may index it normally.""" + skylight = numpy.frombuffer(level['SkyLight'], dtype=numpy.uint8).reshape((16,16,64)) + # this array is 2 blocks per byte, so expand it + 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 & 0xF0) >> 4 + return skylight_expanded + +def get_blocklight_array(level): + """Returns the blocklight array. This is 4 bits per block, but it + is expanded for you so you may index it normally.""" + # expand just like get_skylight_array() + blocklight = numpy.frombuffer(level['BlockLight'], dtype=numpy.uint8).reshape((16,16,64)) + blocklight_expanded = numpy.empty((16,16,128), dtype=numpy.uint8) + blocklight_expanded[:,:,::2] = blocklight & 0x0F + blocklight_expanded[:,:,1::2] = (blocklight & 0xF0) >> 4 + return blocklight_expanded # 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, @@ -219,24 +234,18 @@ class ChunkRenderer(object): rendered, and blocks are drawn with a color tint depending on their depth.""" blocks = self.blocks + skylight = get_skylight_array(self.level) + blocklight = get_blocklight_array(self.level) + 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 + blocks[skylight != 0] = 21 # Each block is 24x24 @@ -310,7 +319,9 @@ class ChunkRenderer(object): 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]) + light_coeff = pow(0.95, 15 - max(blocklight[x,y,z], skylight[x,y,z])) + img.paste(Image.blend(t[0], black_color, 1.0 - light_coeff), (imgx, imgy), t[1]) + #img.paste(t[0], (imgx, imgy), t[1]) # Draw edge lines if blockid not in transparent_blocks: @@ -347,4 +358,5 @@ def generate_depthcolors(): g -= 7 return depth_colors +black_color = Image.new("RGB", (24,24), (0,0,0)) depth_colors = generate_depthcolors() From db62ad94ee63829c0baafa2c9f981f4ec0c1a5ec Mon Sep 17 00:00:00 2001 From: Aaron Griffith Date: Mon, 27 Sep 2010 19:04:35 -0400 Subject: [PATCH 02/13] correct (though chunk-local only) per-face lighting --- chunk.py | 69 +++++++++++++++++++++++++++++++++++++++++++++++++---- textures.py | 8 +++---- 2 files changed, 68 insertions(+), 9 deletions(-) diff --git a/chunk.py b/chunk.py index d2308e5..03469af 100644 --- a/chunk.py +++ b/chunk.py @@ -14,7 +14,7 @@ # with the Overviewer. If not, see . import numpy -from PIL import Image, ImageDraw +from PIL import Image, ImageDraw, ImageEnhance from itertools import izip, count import os.path import hashlib @@ -37,6 +37,13 @@ image # use that as the mask. Then take the image and use im.convert("RGB") to strip # the image from its alpha channel, and use that as the source to paste() +def get_lighting_coefficient(skylight, blocklight): + """Takes a raw blocklight and skylight, and returns a value + between 0.0 (fully lit) and 1.0 (fully black) that can be used as + an alpha value for a blend with a black source image. It mimics + Minecraft lighting calculations.""" + return 1.0 - pow(0.8, 15 - max(blocklight, skylight)) + def get_lvldata(filename): """Takes a filename and returns the Level struct, which contains all the level info""" @@ -317,11 +324,38 @@ class ChunkRenderer(object): # Draw the actual block on the image. For cave images, # tint the block with a color proportional to its depth if cave: + # no lighting for cave -- depth is probably more useful img.paste(Image.blend(t[0],depth_colors[z],0.3), (imgx, imgy), t[1]) else: - light_coeff = pow(0.95, 15 - max(blocklight[x,y,z], skylight[x,y,z])) - img.paste(Image.blend(t[0], black_color, 1.0 - light_coeff), (imgx, imgy), t[1]) - #img.paste(t[0], (imgx, imgy), t[1]) + if blockid in transparent_blocks: + # transparent means draw the whole + # block shaded with the current + # block's light + black_coeff = get_lighting_coefficient(skylight[x,y,z], blocklight[x,y,z]) + img.paste(Image.blend(t[0], black_color, black_coeff), (imgx, imgy), t[1]) + else: + # draw each face lit appropriately, + # but first just draw the block + img.paste(t[0], (imgx, imgy), t[1]) + + # top face + if z != 127 and (blocks[x,y,z+1] in transparent_blocks): + black_coeff = get_lighting_coefficient(skylight[x,y,z+1], blocklight[x,y,z+1]) + img.paste((0,0,0), (imgx, imgy), ImageEnhance.Brightness(facemasks[0]).enhance(black_coeff)) + + # left face + black_coeff = 0.0 + if x != 0: + black_coeff = get_lighting_coefficient(skylight[x-1,y,z], blocklight[x-1,y,z]) + if x == 0 or (blocks[x-1,y,z] in transparent_blocks): + img.paste((0,0,0), (imgx, imgy), ImageEnhance.Brightness(facemasks[1]).enhance(black_coeff)) + + # right face + black_coeff = 0.0 + if y != 15: + black_coeff = get_lighting_coefficient(skylight[x,y+1,z], blocklight[x,y+1,z]) + if y == 15 or (blocks[x,y+1,z] in transparent_blocks): + img.paste((0,0,0), (imgx, imgy), ImageEnhance.Brightness(facemasks[2]).enhance(black_coeff)) # Draw edge lines if blockid not in transparent_blocks: @@ -338,6 +372,32 @@ class ChunkRenderer(object): return img +# Render 3 blending masks for lighting +# first is top (+Z), second is left (-X), third is right (+Y) +def generate_facemasks(): + white = Image.new("L", (24,24), 255) + + top = Image.new("L", (24,24), 0) + left = Image.new("L", (24,24), 0) + whole = Image.new("L", (24,24), 0) + + toppart = textures.transform_image(white) + leftpart = textures.transform_image_side(white) + + top.paste(toppart, (0,0)) + left.paste(leftpart, (0,6)) + right = left.transpose(Image.FLIP_LEFT_RIGHT) + + # Manually touch up 6 pixels that leave a gap, like in + # textures._build_block() + for x,y in [(13,23), (17,21), (21,19)]: + right.putpixel((x,y), 255) + for x,y in [(3,4), (7,2), (11,0)]: + top.putpixel((x,y), 255) + + return (top, left, right) +facemasks = generate_facemasks() +black_color = Image.new("RGB", (24,24), (0,0,0)) # Render 128 different color images for color coded depth blending in cave mode def generate_depthcolors(): @@ -358,5 +418,4 @@ def generate_depthcolors(): g -= 7 return depth_colors -black_color = Image.new("RGB", (24,24), (0,0,0)) depth_colors = generate_depthcolors() diff --git a/textures.py b/textures.py index 28953d5..97e2fcc 100644 --- a/textures.py +++ b/textures.py @@ -106,7 +106,7 @@ 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): """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 @@ -134,7 +134,7 @@ def _transform_image(img): newimg = img.transform((24,12), Image.AFFINE, transform) return newimg -def _transform_image_side(img): +def transform_image_side(img): """Takes an image and shears it for the left side of the cube (reflect for the right side)""" @@ -158,13 +158,13 @@ def _build_block(top, side, texID=None): """ img = Image.new("RGBA", (24,24), (38,92,255,0)) - top = _transform_image(top) + top = transform_image(top) if not side: img.paste(top, (0,0), top) return img - side = _transform_image_side(side) + side = transform_image_side(side) otherside = side.transpose(Image.FLIP_LEFT_RIGHT) From 8d145f2b58c2842eaf8aa22c124b73fb59c3b5f9 Mon Sep 17 00:00:00 2001 From: Aaron Griffith Date: Mon, 27 Sep 2010 19:59:41 -0400 Subject: [PATCH 03/13] added correct chunk boundary lighting --- chunk.py | 44 ++++++++++++++++++++++++++++++++++++++++---- world.py | 29 ++++++++++++++++++----------- 2 files changed, 58 insertions(+), 15 deletions(-) diff --git a/chunk.py b/chunk.py index 03469af..a2e314f 100644 --- a/chunk.py +++ b/chunk.py @@ -21,6 +21,7 @@ import hashlib import nbt import textures +import world """ This module has routines related to rendering one particular chunk into an @@ -86,12 +87,12 @@ def get_blocklight_array(level): 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]) -def render_and_save(chunkfile, cachedir, cave=False): +def render_and_save(chunkfile, cachedir, worldobj, cave=False): """Used as the entry point for the multiprocessing workers (since processes can't target bound methods) or to easily render and save one chunk Returns the image file location""" - a = ChunkRenderer(chunkfile, cachedir) + a = ChunkRenderer(chunkfile, cachedir, worldobj) try: return a.render_and_save(cave) except Exception, e: @@ -108,7 +109,7 @@ def render_and_save(chunkfile, cachedir, cave=False): raise Exception() class ChunkRenderer(object): - def __init__(self, chunkfile, cachedir): + def __init__(self, chunkfile, cachedir, worldobj): """Make a new chunk renderer for the given chunkfile. chunkfile should be a full path to the .dat file to process cachedir is a directory to save the resulting chunk images to @@ -117,7 +118,11 @@ class ChunkRenderer(object): raise ValueError("Could not find chunkfile") self.chunkfile = chunkfile destdir, filename = os.path.split(self.chunkfile) - self.blockid = ".".join(filename.split(".")[1:3]) + + chunkcoords = filename.split(".")[1:3] + self.coords = map(world.base36decode, chunkcoords) + self.blockid = ".".join(chunkcoords) + self.world = worldobj # Cachedir here is the base directory of the caches. We need to go 2 # levels deeper according to the chunk file. Get the last 2 components @@ -241,9 +246,36 @@ class ChunkRenderer(object): rendered, and blocks are drawn with a color tint depending on their depth.""" blocks = self.blocks + + # light data for the current chunk skylight = get_skylight_array(self.level) blocklight = get_blocklight_array(self.level) + # light data for the chunk to the lower left + chunk_path = self.world.get_chunk_path(self.coords[0] - 1, self.coords[1]) + try: + chunk_data = get_lvldata(chunk_path) + left_skylight = get_skylight_array(chunk_data) + left_blocklight = get_blocklight_array(chunk_data) + del chunk_data + except IOError: + left_skylight = None + left_blocklight = None + + # light data for the chunk to the lower right + chunk_path = self.world.get_chunk_path(self.coords[0], self.coords[1] + 1) + try: + chunk_data = get_lvldata(chunk_path) + right_skylight = get_skylight_array(chunk_data) + right_blocklight = get_blocklight_array(chunk_data) + del chunk_data + except IOError: + right_skylight = None + right_blocklight = None + + # clean up namespace a bit + del chunk_path + if cave: # Cave mode. Actually go through and 0 out all blocks that are not in a # cave, so that it only renders caves. @@ -347,6 +379,8 @@ class ChunkRenderer(object): black_coeff = 0.0 if x != 0: black_coeff = get_lighting_coefficient(skylight[x-1,y,z], blocklight[x-1,y,z]) + elif left_skylight != None and left_blocklight != None: + black_coeff = get_lighting_coefficient(left_skylight[15,y,z], left_blocklight[15,y,z]) if x == 0 or (blocks[x-1,y,z] in transparent_blocks): img.paste((0,0,0), (imgx, imgy), ImageEnhance.Brightness(facemasks[1]).enhance(black_coeff)) @@ -354,6 +388,8 @@ class ChunkRenderer(object): black_coeff = 0.0 if y != 15: black_coeff = get_lighting_coefficient(skylight[x,y+1,z], blocklight[x,y+1,z]) + elif right_skylight != None and right_blocklight != None: + black_coeff = get_lighting_coefficient(right_skylight[x,0,z], right_blocklight[x,0,z]) if y == 15 or (blocks[x,y+1,z] in transparent_blocks): img.paste((0,0,0), (imgx, imgy), ImageEnhance.Brightness(facemasks[2]).enhance(black_coeff)) diff --git a/world.py b/world.py index 99728c1..a03a00a 100644 --- a/world.py +++ b/world.py @@ -130,7 +130,18 @@ class WorldRenderer(object): inclusion_set.add((col, row)) return inclusion_set - + + def get_chunk_path(self, chunkX, chunkY): + """Returns the path to the chunk file at (chunkX, chunkY), if + it exists.""" + + chunkFile = "%s/%s/c.%s.%s.dat" % (base36encode(chunkX % 64), + base36encode(chunkY % 64), + base36encode(chunkX), + base36encode(chunkY)) + + return os.path.join(self.worlddir, chunkFile) + def findTrueSpawn(self): """Adds the true spawn location to self.POI. The spawn Y coordinate is almost always the default of 64. Find the first air block above @@ -147,13 +158,9 @@ class WorldRenderer(object): chunkY = spawnZ/16 ## The filename of this chunk - chunkFile = "%s/%s/c.%s.%s.dat" % (base36encode(chunkX % 64), - base36encode(chunkY % 64), - base36encode(chunkX), - base36encode(chunkY)) + chunkFile = self.get_chunk_path(chunkX, chunkY) - - data=nbt.load(os.path.join(self.worlddir, chunkFile))[1] + data=nbt.load(chunkFile)[1] level = data['Level'] blockArray = numpy.frombuffer(level['Blocks'], dtype=numpy.uint8).reshape((16,16,128)) @@ -234,12 +241,12 @@ class WorldRenderer(object): if inclusion_set and (col, row) not in inclusion_set: # Skip rendering, just find where the existing image is _, imgpath = chunk.ChunkRenderer(chunkfile, - self.cachedir).find_oldimage(False) + self.cachedir, self).find_oldimage(False) if imgpath: results[(col, row)] = imgpath continue - result = chunk.render_and_save(chunkfile, self.cachedir, cave=self.caves) + result = chunk.render_and_save(chunkfile, self.cachedir, self, cave=self.caves) results[(col, row)] = result if i > 0: if 1000 % i == 0 or i % 1000 == 0: @@ -252,13 +259,13 @@ class WorldRenderer(object): if inclusion_set and (col, row) not in inclusion_set: # Skip rendering, just find where the existing image is _, imgpath = chunk.ChunkRenderer(chunkfile, - self.cachedir).find_oldimage(False) + self.cachedir, self).find_oldimage(False) if imgpath: results[(col, row)] = imgpath continue result = pool.apply_async(chunk.render_and_save, - args=(chunkfile,self.cachedir), + args=(chunkfile,self.cachedir,self), kwds=dict(cave=self.caves)) asyncresults.append((col, row, result)) From 27e9b1cb15dcfd03295231e98636b6c94789ee68 Mon Sep 17 00:00:00 2001 From: Aaron Griffith Date: Mon, 27 Sep 2010 20:31:31 -0400 Subject: [PATCH 04/13] added (commented out, for now) night render code --- chunk.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/chunk.py b/chunk.py index a2e314f..c7f538e 100644 --- a/chunk.py +++ b/chunk.py @@ -43,7 +43,10 @@ def get_lighting_coefficient(skylight, blocklight): between 0.0 (fully lit) and 1.0 (fully black) that can be used as an alpha value for a blend with a black source image. It mimics Minecraft lighting calculations.""" + # Daytime return 1.0 - pow(0.8, 15 - max(blocklight, skylight)) + # Nighttime + #return 1.0 - pow(0.8, 15 - max(blocklight, skylight - 11)) def get_lvldata(filename): """Takes a filename and returns the Level struct, which contains all the From d6fc49e1d9731ec65f3986a41cdeb2c8a11fd7fd Mon Sep 17 00:00:00 2001 From: Aaron Griffith Date: Tue, 28 Sep 2010 10:47:19 -0400 Subject: [PATCH 05/13] store less data at once, and chunk boundary lighting occlusion checks I also changed the default light level to be based off the light calculation function, so night renders have a reasonable default light level now. --- chunk.py | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/chunk.py b/chunk.py index c7f538e..a884018 100644 --- a/chunk.py +++ b/chunk.py @@ -258,23 +258,29 @@ class ChunkRenderer(object): chunk_path = self.world.get_chunk_path(self.coords[0] - 1, self.coords[1]) try: chunk_data = get_lvldata(chunk_path) - left_skylight = get_skylight_array(chunk_data) - left_blocklight = get_blocklight_array(chunk_data) + # we only need +X-most side + left_skylight = get_skylight_array(chunk_data)[15,:,:] + left_blocklight = get_blocklight_array(chunk_data)[15,:,:] + left_blocks = get_blockarray(chunk_data)[15,:,:] del chunk_data except IOError: left_skylight = None left_blocklight = None + left_blocks = None # light data for the chunk to the lower right chunk_path = self.world.get_chunk_path(self.coords[0], self.coords[1] + 1) try: chunk_data = get_lvldata(chunk_path) - right_skylight = get_skylight_array(chunk_data) - right_blocklight = get_blocklight_array(chunk_data) + # we only need -Y-most side + right_skylight = get_skylight_array(chunk_data)[:,0,:] + right_blocklight = get_blocklight_array(chunk_data)[:,0,:] + right_blocks = get_blockarray(chunk_data)[:,0,:] del chunk_data except IOError: right_skylight = None right_blocklight = None + right_blocks = None # clean up namespace a bit del chunk_path @@ -379,21 +385,21 @@ class ChunkRenderer(object): img.paste((0,0,0), (imgx, imgy), ImageEnhance.Brightness(facemasks[0]).enhance(black_coeff)) # left face - black_coeff = 0.0 + black_coeff = get_lighting_coefficient(15, 0) if x != 0: black_coeff = get_lighting_coefficient(skylight[x-1,y,z], blocklight[x-1,y,z]) elif left_skylight != None and left_blocklight != None: - black_coeff = get_lighting_coefficient(left_skylight[15,y,z], left_blocklight[15,y,z]) - if x == 0 or (blocks[x-1,y,z] in transparent_blocks): + black_coeff = get_lighting_coefficient(left_skylight[y,z], left_blocklight[y,z]) + if (x == 0 and (left_blocks == None or left_blocks[y,z] in transparent_blocks)) or (x != 0 and blocks[x-1,y,z] in transparent_blocks): img.paste((0,0,0), (imgx, imgy), ImageEnhance.Brightness(facemasks[1]).enhance(black_coeff)) # right face - black_coeff = 0.0 + black_coeff = get_lighting_coefficient(15, 0) if y != 15: black_coeff = get_lighting_coefficient(skylight[x,y+1,z], blocklight[x,y+1,z]) elif right_skylight != None and right_blocklight != None: - black_coeff = get_lighting_coefficient(right_skylight[x,0,z], right_blocklight[x,0,z]) - if y == 15 or (blocks[x,y+1,z] in transparent_blocks): + black_coeff = get_lighting_coefficient(right_skylight[x,z], right_blocklight[x,z]) + if (y == 15 and (right_blocks == None or right_blocks[x,z] in transparent_blocks)) or (y != 15 and blocks[x,y+1,z] in transparent_blocks): img.paste((0,0,0), (imgx, imgy), ImageEnhance.Brightness(facemasks[2]).enhance(black_coeff)) # Draw edge lines From 8dccf4162c408f02a98231ebb2334cace55f85d1 Mon Sep 17 00:00:00 2001 From: Aaron Griffith Date: Tue, 28 Sep 2010 16:57:23 -0400 Subject: [PATCH 06/13] added run-time flags to enable lighting or nighttime rendering --- chunk.py | 109 ++++++++++++++++++++++++++++++++----------------------- gmap.py | 4 +- world.py | 4 +- 3 files changed, 69 insertions(+), 48 deletions(-) diff --git a/chunk.py b/chunk.py index a884018..259adcf 100644 --- a/chunk.py +++ b/chunk.py @@ -38,15 +38,17 @@ image # use that as the mask. Then take the image and use im.convert("RGB") to strip # the image from its alpha channel, and use that as the source to paste() -def get_lighting_coefficient(skylight, blocklight): +def get_lighting_coefficient(skylight, blocklight, night): """Takes a raw blocklight and skylight, and returns a value between 0.0 (fully lit) and 1.0 (fully black) that can be used as an alpha value for a blend with a black source image. It mimics Minecraft lighting calculations.""" - # Daytime - return 1.0 - pow(0.8, 15 - max(blocklight, skylight)) - # Nighttime - #return 1.0 - pow(0.8, 15 - max(blocklight, skylight - 11)) + if not night: + # Daytime + return 1.0 - pow(0.8, 15 - max(blocklight, skylight)) + else: + # Nighttime + return 1.0 - pow(0.8, 15 - max(blocklight, skylight - 11)) def get_lvldata(filename): """Takes a filename and returns the Level struct, which contains all the @@ -250,40 +252,52 @@ class ChunkRenderer(object): depth.""" blocks = self.blocks - # light data for the current chunk - skylight = get_skylight_array(self.level) - blocklight = get_blocklight_array(self.level) + # dummy variables that may be filled in later based on render options + skylight = None + blocklight = None + left_skylight = None + left_blocklight = None + left_blocks = None + right_skylight = None + right_blocklight = None + right_blocks = None - # light data for the chunk to the lower left - chunk_path = self.world.get_chunk_path(self.coords[0] - 1, self.coords[1]) - try: - chunk_data = get_lvldata(chunk_path) - # we only need +X-most side - left_skylight = get_skylight_array(chunk_data)[15,:,:] - left_blocklight = get_blocklight_array(chunk_data)[15,:,:] - left_blocks = get_blockarray(chunk_data)[15,:,:] - del chunk_data - except IOError: - left_skylight = None - left_blocklight = None - left_blocks = None + if self.world.lighting or cave: + # light data for the current chunk + skylight = get_skylight_array(self.level) + blocklight = get_blocklight_array(self.level) + + if self.world.lighting: + # light data for the chunk to the lower left + chunk_path = self.world.get_chunk_path(self.coords[0] - 1, self.coords[1]) + try: + chunk_data = get_lvldata(chunk_path) + # we only need +X-most side + left_skylight = get_skylight_array(chunk_data)[15,:,:] + left_blocklight = get_blocklight_array(chunk_data)[15,:,:] + left_blocks = get_blockarray(chunk_data)[15,:,:] + del chunk_data + except IOError: + left_skylight = None + left_blocklight = None + left_blocks = None - # light data for the chunk to the lower right - chunk_path = self.world.get_chunk_path(self.coords[0], self.coords[1] + 1) - try: - chunk_data = get_lvldata(chunk_path) - # we only need -Y-most side - right_skylight = get_skylight_array(chunk_data)[:,0,:] - right_blocklight = get_blocklight_array(chunk_data)[:,0,:] - right_blocks = get_blockarray(chunk_data)[:,0,:] - del chunk_data - except IOError: - right_skylight = None - right_blocklight = None - right_blocks = None - - # clean up namespace a bit - del chunk_path + # light data for the chunk to the lower right + chunk_path = self.world.get_chunk_path(self.coords[0], self.coords[1] + 1) + try: + chunk_data = get_lvldata(chunk_path) + # we only need -Y-most side + right_skylight = get_skylight_array(chunk_data)[:,0,:] + right_blocklight = get_blocklight_array(chunk_data)[:,0,:] + right_blocks = get_blockarray(chunk_data)[:,0,:] + del chunk_data + except IOError: + right_skylight = None + right_blocklight = None + right_blocks = None + + # clean up namespace a bit + del chunk_path if cave: # Cave mode. Actually go through and 0 out all blocks that are not in a @@ -368,11 +382,14 @@ class ChunkRenderer(object): # no lighting for cave -- depth is probably more useful img.paste(Image.blend(t[0],depth_colors[z],0.3), (imgx, imgy), t[1]) else: - if blockid in transparent_blocks: + if not (blocklight != None and skylight != None): + # no lighting at all + img.paste(t[0], (imgx, imgy), t[1]) + elif blockid in transparent_blocks: # transparent means draw the whole # block shaded with the current # block's light - black_coeff = get_lighting_coefficient(skylight[x,y,z], blocklight[x,y,z]) + black_coeff = get_lighting_coefficient(skylight[x,y,z], blocklight[x,y,z], self.world.night) img.paste(Image.blend(t[0], black_color, black_coeff), (imgx, imgy), t[1]) else: # draw each face lit appropriately, @@ -381,24 +398,24 @@ class ChunkRenderer(object): # top face if z != 127 and (blocks[x,y,z+1] in transparent_blocks): - black_coeff = get_lighting_coefficient(skylight[x,y,z+1], blocklight[x,y,z+1]) + black_coeff = get_lighting_coefficient(skylight[x,y,z+1], blocklight[x,y,z+1], self.world.night) img.paste((0,0,0), (imgx, imgy), ImageEnhance.Brightness(facemasks[0]).enhance(black_coeff)) # left face - black_coeff = get_lighting_coefficient(15, 0) + black_coeff = get_lighting_coefficient(15, 0, self.world.night) if x != 0: - black_coeff = get_lighting_coefficient(skylight[x-1,y,z], blocklight[x-1,y,z]) + black_coeff = get_lighting_coefficient(skylight[x-1,y,z], blocklight[x-1,y,z], self.world.night) elif left_skylight != None and left_blocklight != None: - black_coeff = get_lighting_coefficient(left_skylight[y,z], left_blocklight[y,z]) + black_coeff = get_lighting_coefficient(left_skylight[y,z], left_blocklight[y,z], self.world.night) if (x == 0 and (left_blocks == None or left_blocks[y,z] in transparent_blocks)) or (x != 0 and blocks[x-1,y,z] in transparent_blocks): img.paste((0,0,0), (imgx, imgy), ImageEnhance.Brightness(facemasks[1]).enhance(black_coeff)) # right face - black_coeff = get_lighting_coefficient(15, 0) + black_coeff = get_lighting_coefficient(15, 0, self.world.night) if y != 15: - black_coeff = get_lighting_coefficient(skylight[x,y+1,z], blocklight[x,y+1,z]) + black_coeff = get_lighting_coefficient(skylight[x,y+1,z], blocklight[x,y+1,z], self.world.night) elif right_skylight != None and right_blocklight != None: - black_coeff = get_lighting_coefficient(right_skylight[x,z], right_blocklight[x,z]) + black_coeff = get_lighting_coefficient(right_skylight[x,z], right_blocklight[x,z], self.world.night) if (y == 15 and (right_blocks == None or right_blocks[x,z] in transparent_blocks)) or (y != 15 and blocks[x,y+1,z] in transparent_blocks): img.paste((0,0,0), (imgx, imgy), ImageEnhance.Brightness(facemasks[2]).enhance(black_coeff)) diff --git a/gmap.py b/gmap.py index 97eeb97..1ce469c 100755 --- a/gmap.py +++ b/gmap.py @@ -46,6 +46,8 @@ def main(): parser.add_option("-d", "--delete", dest="delete", help="Clear all caches. Next time you render your world, it will have to start completely over again. This is probably not a good idea for large worlds. Use this if you change texture packs and want to re-render everything.", action="store_true") parser.add_option("--cachedir", dest="cachedir", help="Sets the directory where the Overviewer will save chunk images, which is an intermediate step before the tiles are generated. You must use the same directory each time to gain any benefit from the cache. If not set, this defaults to your world directory.") 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") options, args = parser.parse_args() @@ -86,7 +88,7 @@ def main(): chunklist = None # First generate the world's chunk images - w = world.WorldRenderer(worlddir, cachedir, chunklist=chunklist) + w = world.WorldRenderer(worlddir, cachedir, chunklist=chunklist, lighting=options.lighting, night=options.night) w.go(options.procs) # Now generate the tiles diff --git a/world.py b/world.py index a03a00a..5e710d7 100644 --- a/world.py +++ b/world.py @@ -92,9 +92,11 @@ class WorldRenderer(object): files to update. If it includes a trailing newline, it is stripped, so you can pass in file handles just fine. """ - def __init__(self, worlddir, cachedir, chunklist=None): + def __init__(self, worlddir, cachedir, chunklist=None, lighting=False, night=False): self.worlddir = worlddir self.caves = False + self.lighting = lighting or night + self.night = night self.cachedir = cachedir self.chunklist = chunklist From 4c58d28854a340779b764afbc073e305dbb46913 Mon Sep 17 00:00:00 2001 From: Aaron Griffith Date: Tue, 5 Oct 2010 08:55:35 -0400 Subject: [PATCH 07/13] fixed remaining function renames from merge --- textures.py | 56 ++++++++++++++++++++++++++--------------------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/textures.py b/textures.py index 61ad5b9..b942168 100644 --- a/textures.py +++ b/textures.py @@ -345,25 +345,25 @@ def generate_special_texture(blockID, data): raw_straight = terrain_images[128] raw_corner = terrain_images[112] - ## use _transform_image to scale and shear + ## use transform_image to scale and shear if data == 0: - track = _transform_image(raw_straight, blockID) + track = transform_image(raw_straight, blockID) elif data == 6: - track = _transform_image(raw_corner, blockID) + track = transform_image(raw_corner, blockID) elif data == 7: - track = _transform_image(raw_corner.rotate(270), blockID) + track = transform_image(raw_corner.rotate(270), blockID) elif data == 8: # flip - track = _transform_image(raw_corner.transpose(Image.FLIP_TOP_BOTTOM).rotate(90), + 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), + track = transform_image(raw_corner.transpose(Image.FLIP_TOP_BOTTOM), blockID) elif data == 1: - track = _transform_image(raw_straight.rotate(90), blockID) + track = transform_image(raw_straight.rotate(90), blockID) else: # TODO render carts that slop up or down - track = _transform_image(raw_straight, blockID) + track = transform_image(raw_straight, blockID) img = Image.new("RGBA", (24,24), (38,92,255,0)) img.paste(track, (0,12), track) @@ -371,8 +371,8 @@ def generate_special_texture(blockID, data): 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) + 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)) @@ -382,9 +382,9 @@ def generate_special_texture(blockID, data): 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) + 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)) @@ -394,9 +394,9 @@ def generate_special_texture(blockID, data): 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) + 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)) @@ -412,22 +412,22 @@ def generate_special_texture(blockID, data): # 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) + 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) + 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) + 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) + 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]) @@ -449,36 +449,36 @@ def generate_special_texture(blockID, data): img = Image.new("RGBA", (24,24), (38,92,255,0)) if (data & 0x03) == 0: if not swung: - tex = _transform_image_side(raw_door) + 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 = 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) + tex = transform_image_side(raw_door).transpose(Image.FLIP_LEFT_RIGHT) img.paste(tex, (0,0), tex) else: - tex = _transform_image_side(raw_door) + 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)) + 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) + 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) + 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)) + tex = transform_image_side(raw_door.transpose(Image.FLIP_LEFT_RIGHT)) img.paste(tex, (0,6), tex) return (img.convert("RGB"), img.split()[3]) From e4e52cee0b5ce9676028c5b76ea0588d44069daa Mon Sep 17 00:00:00 2001 From: Aaron Griffith Date: Tue, 12 Oct 2010 19:05:44 -0400 Subject: [PATCH 08/13] moved get_lighting_coefficient into the ChunkRenderer class --- chunk.py | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/chunk.py b/chunk.py index 45ce17f..c0ac97c 100644 --- a/chunk.py +++ b/chunk.py @@ -37,18 +37,6 @@ image # use that as the mask. Then take the image and use im.convert("RGB") to strip # the image from its alpha channel, and use that as the source to paste() -def get_lighting_coefficient(skylight, blocklight, night): - """Takes a raw blocklight and skylight, and returns a value - between 0.0 (fully lit) and 1.0 (fully black) that can be used as - an alpha value for a blend with a black source image. It mimics - Minecraft lighting calculations.""" - if not night: - # Daytime - return 1.0 - pow(0.8, 15 - max(blocklight, skylight)) - else: - # Nighttime - return 1.0 - pow(0.8, 15 - max(blocklight, skylight - 11)) - def get_lvldata(filename): """Takes a filename and returns the Level struct, which contains all the level info""" @@ -246,6 +234,18 @@ class ChunkRenderer(object): # Return its location return dest_path + def get_lighting_coefficient(self, skylight, blocklight): + """Takes a raw blocklight and skylight, and returns a value + between 0.0 (fully lit) and 1.0 (fully black) that can be used as + an alpha value for a blend with a black source image. It mimics + Minecraft lighting calculations.""" + if not self.world.night: + # Daytime + return 1.0 - pow(0.8, 15 - max(blocklight, skylight)) + else: + # Nighttime + return 1.0 - pow(0.8, 15 - max(blocklight, skylight - 11)) + 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, @@ -416,7 +416,7 @@ class ChunkRenderer(object): # transparent means draw the whole # block shaded with the current # block's light - black_coeff = get_lighting_coefficient(skylight[x,y,z], blocklight[x,y,z], self.world.night) + black_coeff = self.get_lighting_coefficient(skylight[x,y,z], blocklight[x,y,z]) img.paste(Image.blend(t[0], black_color, black_coeff), (imgx, imgy), t[1]) else: # draw each face lit appropriately, @@ -425,24 +425,24 @@ class ChunkRenderer(object): # top face if z != 127 and (blocks[x,y,z+1] in transparent_blocks): - black_coeff = get_lighting_coefficient(skylight[x,y,z+1], blocklight[x,y,z+1], self.world.night) + black_coeff = self.get_lighting_coefficient(skylight[x,y,z+1], blocklight[x,y,z+1]) img.paste((0,0,0), (imgx, imgy), ImageEnhance.Brightness(facemasks[0]).enhance(black_coeff)) # left face - black_coeff = get_lighting_coefficient(15, 0, self.world.night) + black_coeff = self.get_lighting_coefficient(15, 0) if x != 0: - black_coeff = get_lighting_coefficient(skylight[x-1,y,z], blocklight[x-1,y,z], self.world.night) + black_coeff = self.get_lighting_coefficient(skylight[x-1,y,z], blocklight[x-1,y,z]) elif left_skylight != None and left_blocklight != None: - black_coeff = get_lighting_coefficient(left_skylight[y,z], left_blocklight[y,z], self.world.night) + black_coeff = self.get_lighting_coefficient(left_skylight[y,z], left_blocklight[y,z]) if (x == 0 and (left_blocks == None or left_blocks[y,z] in transparent_blocks)) or (x != 0 and blocks[x-1,y,z] in transparent_blocks): img.paste((0,0,0), (imgx, imgy), ImageEnhance.Brightness(facemasks[1]).enhance(black_coeff)) # right face - black_coeff = get_lighting_coefficient(15, 0, self.world.night) + black_coeff = self.get_lighting_coefficient(15, 0) if y != 15: - black_coeff = get_lighting_coefficient(skylight[x,y+1,z], blocklight[x,y+1,z], self.world.night) + black_coeff = self.get_lighting_coefficient(skylight[x,y+1,z], blocklight[x,y+1,z]) elif right_skylight != None and right_blocklight != None: - black_coeff = get_lighting_coefficient(right_skylight[x,z], right_blocklight[x,z], self.world.night) + black_coeff = self.get_lighting_coefficient(right_skylight[x,z], right_blocklight[x,z]) if (y == 15 and (right_blocks == None or right_blocks[x,z] in transparent_blocks)) or (y != 15 and blocks[x,y+1,z] in transparent_blocks): img.paste((0,0,0), (imgx, imgy), ImageEnhance.Brightness(facemasks[2]).enhance(black_coeff)) From e51556f314a0401415dfbcdec30bab54cdba7238 Mon Sep 17 00:00:00 2001 From: Aaron Griffith Date: Tue, 12 Oct 2010 21:11:27 -0400 Subject: [PATCH 09/13] moved lighting data into properties, and light logic into a function --- chunk.py | 210 +++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 142 insertions(+), 68 deletions(-) diff --git a/chunk.py b/chunk.py index c0ac97c..dbfd9f6 100644 --- a/chunk.py +++ b/chunk.py @@ -149,6 +149,88 @@ class ChunkRenderer(object): self._blocks = get_blockarray(self._load_level()) return self._blocks blocks = property(_load_blocks) + + def _load_skylight(self): + """Loads and returns skylight array""" + if not hasattr(self, "_skylight"): + self._skylight = get_skylight_array(self.level) + return self._skylight + skylight = property(_load_skylight) + + def _load_blocklight(self): + """Loads and returns blocklight array""" + if not hasattr(self, "_blocklight"): + self._blocklight = get_blocklight_array(self.level) + return self._blocklight + blocklight = property(_load_blocklight) + + def _load_left(self): + """Loads and sets data from lower-left chunk""" + chunk_path = self.world.get_chunk_path(self.coords[0] - 1, self.coords[1]) + try: + chunk_data = get_lvldata(chunk_path) + self._left_skylight = get_skylight_array(chunk_data) + self._left_blocklight = get_blocklight_array(chunk_data) + self._left_blocks = get_blockarray(chunk_data) + except IOError: + self._left_skylight = None + self._left_blocklight = None + self._left_blocks = None + + def _load_left_blocks(self): + """Loads and returns lower-left block array""" + if not hasattr(self, "_left_blocks"): + self._load_left() + return self._left_blocks + left_blocks = property(_load_left_blocks) + + def _load_left_skylight(self): + """Loads and returns lower-left skylight array""" + if not hasattr(self, "_left_skylight"): + self._load_left() + return self._left_skylight + left_skylight = property(_load_left_skylight) + + def _load_left_blocklight(self): + """Loads and returns lower-left blocklight array""" + if not hasattr(self, "_left_blocklight"): + self._load_left() + return self._left_blocklight + left_blocklight = property(_load_left_blocklight) + + def _load_right(self): + """Loads and sets data from lower-right chunk""" + chunk_path = self.world.get_chunk_path(self.coords[0], self.coords[1] + 1) + try: + chunk_data = get_lvldata(chunk_path) + self._right_skylight = get_skylight_array(chunk_data) + self._right_blocklight = get_blocklight_array(chunk_data) + self._right_blocks = get_blockarray(chunk_data) + except IOError: + self._right_skylight = None + self._right_blocklight = None + self._right_blocks = None + + def _load_right_blocks(self): + """Loads and returns lower-right block array""" + if not hasattr(self, "_right_blocks"): + self._load_right() + return self._right_blocks + right_blocks = property(_load_right_blocks) + + def _load_right_skylight(self): + """Loads and returns lower-right skylight array""" + if not hasattr(self, "_right_skylight"): + self._load_right() + return self._right_skylight + right_skylight = property(_load_right_skylight) + + def _load_right_blocklight(self): + """Loads and returns lower-right blocklight array""" + if not hasattr(self, "_right_blocklight"): + self._load_right() + return self._right_blocklight + right_blocklight = property(_load_right_blocklight) def _hash_blockarray(self): """Finds a hash of the block array""" @@ -234,7 +316,7 @@ class ChunkRenderer(object): # Return its location return dest_path - def get_lighting_coefficient(self, skylight, blocklight): + def calculate_darkness(self, skylight, blocklight): """Takes a raw blocklight and skylight, and returns a value between 0.0 (fully lit) and 1.0 (fully black) that can be used as an alpha value for a blend with a black source image. It mimics @@ -246,6 +328,56 @@ class ChunkRenderer(object): # Nighttime return 1.0 - pow(0.8, 15 - max(blocklight, skylight - 11)) + def get_lighting_coefficient(self, x, y, z): + """Calculates the lighting coefficient for the given + coordinate, using default lighting and peeking into + neighboring chunks, if needed. A lighting coefficient of 1.0 + means fully black. Returns a tuple (coefficient, occluded), + where occluded is true if the given coordinate is filled with + a solid block, and therefore the returned coefficient is + just the default.""" + + # fill it in with the default first (full skylight) + coefficient = self.calculate_darkness(15, 0) + + # placeholders for later data arrays, coordinates + blocks = None + skylight = None + blocklight = None + local_x = x + local_y = y + local_z = z + + if x >= 0 and y < 16: + blocks = self.blocks + skylight = self.skylight + blocklight = self.blocklight + elif x < 0: + local_x += 16 + blocks = self.left_blocks + skylight = self.left_skylight + blocklight = self.left_blocklight + elif y >= 16: + local_y -= 16 + blocks = self.right_blocks + skylight = self.right_skylight + blocklight = self.right_blocklight + + # make sure we have a correctly-ranged coordinates and enough + # info about the chunk + if not (blocks != None and skylight != None and blocklight != None and + local_x >= 0 and local_x < 16 and local_y >= 0 and local_y < 16 and + local_z >= 0 and local_z < 128): + # we have no useful info, return default + return (coefficient, False) + + # calculate the return + occluded = not (blocks[local_x, local_y, local_z] in transparent_blocks) + if not occluded: + coefficient = self.calculate_darkness(skylight[local_x, local_y, local_z], blocklight[local_x, local_y, local_z]) + + return (coefficient, occluded) + 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, @@ -256,53 +388,6 @@ class ChunkRenderer(object): depth.""" blocks = self.blocks - # dummy variables that may be filled in later based on render options - skylight = None - blocklight = None - left_skylight = None - left_blocklight = None - left_blocks = None - right_skylight = None - right_blocklight = None - right_blocks = None - - if self.world.lighting or cave: - # light data for the current chunk - skylight = get_skylight_array(self.level) - blocklight = get_blocklight_array(self.level) - - if self.world.lighting: - # light data for the chunk to the lower left - chunk_path = self.world.get_chunk_path(self.coords[0] - 1, self.coords[1]) - try: - chunk_data = get_lvldata(chunk_path) - # we only need +X-most side - left_skylight = get_skylight_array(chunk_data)[15,:,:] - left_blocklight = get_blocklight_array(chunk_data)[15,:,:] - left_blocks = get_blockarray(chunk_data)[15,:,:] - del chunk_data - except IOError: - left_skylight = None - left_blocklight = None - left_blocks = None - - # light data for the chunk to the lower right - chunk_path = self.world.get_chunk_path(self.coords[0], self.coords[1] + 1) - try: - chunk_data = get_lvldata(chunk_path) - # we only need -Y-most side - right_skylight = get_skylight_array(chunk_data)[:,0,:] - right_blocklight = get_blocklight_array(chunk_data)[:,0,:] - right_blocks = get_blockarray(chunk_data)[:,0,:] - del chunk_data - except IOError: - right_skylight = None - right_blocklight = None - right_blocks = None - - # clean up namespace a bit - del chunk_path - if cave: # Cave mode. Actually go through and 0 out all blocks that are not in a # cave, so that it only renders caves. @@ -311,7 +396,7 @@ class ChunkRenderer(object): # touching it) change it to something that won't get rendered, AND # won't get counted as "transparent". blocks = blocks.copy() - blocks[skylight != 0] = 21 + blocks[self.skylight != 0] = 21 blockData = get_blockdata_array(self.level) blockData_expanded = numpy.empty((16,16,128), dtype=numpy.uint8) @@ -409,14 +494,14 @@ class ChunkRenderer(object): # no lighting for cave -- depth is probably more useful img.paste(Image.blend(t[0],depth_colors[z],0.3), (imgx, imgy), t[1]) else: - if not (blocklight != None and skylight != None): + if not self.world.lighting: # no lighting at all img.paste(t[0], (imgx, imgy), t[1]) elif blockid in transparent_blocks: # transparent means draw the whole # block shaded with the current # block's light - black_coeff = self.get_lighting_coefficient(skylight[x,y,z], blocklight[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]) else: # draw each face lit appropriately, @@ -424,27 +509,16 @@ class ChunkRenderer(object): img.paste(t[0], (imgx, imgy), t[1]) # top face - if z != 127 and (blocks[x,y,z+1] in transparent_blocks): - black_coeff = self.get_lighting_coefficient(skylight[x,y,z+1], blocklight[x,y,z+1]) - img.paste((0,0,0), (imgx, imgy), ImageEnhance.Brightness(facemasks[0]).enhance(black_coeff)) + black_coeff, _ = self.get_lighting_coefficient(x, y, z + 1) + img.paste((0,0,0), (imgx, imgy), ImageEnhance.Brightness(facemasks[0]).enhance(black_coeff)) # left face - black_coeff = self.get_lighting_coefficient(15, 0) - if x != 0: - black_coeff = self.get_lighting_coefficient(skylight[x-1,y,z], blocklight[x-1,y,z]) - elif left_skylight != None and left_blocklight != None: - black_coeff = self.get_lighting_coefficient(left_skylight[y,z], left_blocklight[y,z]) - if (x == 0 and (left_blocks == None or left_blocks[y,z] in transparent_blocks)) or (x != 0 and blocks[x-1,y,z] in transparent_blocks): - img.paste((0,0,0), (imgx, imgy), ImageEnhance.Brightness(facemasks[1]).enhance(black_coeff)) + black_coeff, _ = self.get_lighting_coefficient(x - 1, y, z) + img.paste((0,0,0), (imgx, imgy), ImageEnhance.Brightness(facemasks[1]).enhance(black_coeff)) # right face - black_coeff = self.get_lighting_coefficient(15, 0) - if y != 15: - black_coeff = self.get_lighting_coefficient(skylight[x,y+1,z], blocklight[x,y+1,z]) - elif right_skylight != None and right_blocklight != None: - black_coeff = self.get_lighting_coefficient(right_skylight[x,z], right_blocklight[x,z]) - if (y == 15 and (right_blocks == None or right_blocks[x,z] in transparent_blocks)) or (y != 15 and blocks[x,y+1,z] in transparent_blocks): - img.paste((0,0,0), (imgx, imgy), ImageEnhance.Brightness(facemasks[2]).enhance(black_coeff)) + black_coeff, _ = self.get_lighting_coefficient(x, y + 1, z) + img.paste((0,0,0), (imgx, imgy), ImageEnhance.Brightness(facemasks[2]).enhance(black_coeff)) # Draw edge lines if blockid in (44,): # step block From b44cb9c3f8bb50d64ed228d79ae258b9e98d583f Mon Sep 17 00:00:00 2001 From: Aaron Griffith Date: Thu, 14 Oct 2010 13:36:20 -0400 Subject: [PATCH 10/13] added proper occlusion tracking to lighting function --- chunk.py | 39 ++++++++++++++++++++++++++++----------- 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/chunk.py b/chunk.py index dbfd9f6..9cb5be2 100644 --- a/chunk.py +++ b/chunk.py @@ -332,10 +332,11 @@ class ChunkRenderer(object): """Calculates the lighting coefficient for the given coordinate, using default lighting and peeking into neighboring chunks, if needed. A lighting coefficient of 1.0 - means fully black. Returns a tuple (coefficient, occluded), - where occluded is true if the given coordinate is filled with - a solid block, and therefore the returned coefficient is - just the default.""" + means fully black. + + Returns a tuple (coefficient, occluded), where occluded is + True if the given coordinate is filled with a solid block, and + therefore the returned coefficient is just the default.""" # fill it in with the default first (full skylight) coefficient = self.calculate_darkness(15, 0) @@ -347,11 +348,14 @@ class ChunkRenderer(object): local_x = x local_y = y local_z = z + is_local_chunk = False + # find out what chunk we're in, and translate accordingly if x >= 0 and y < 16: blocks = self.blocks skylight = self.skylight blocklight = self.blocklight + is_local_chunk = True elif x < 0: local_x += 16 blocks = self.left_blocks @@ -371,11 +375,21 @@ class ChunkRenderer(object): # we have no useful info, return default return (coefficient, False) - # calculate the return + # calculate the return... occluded = not (blocks[local_x, local_y, local_z] in transparent_blocks) + + # only calculate the coefficient if we're not occluded if not occluded: coefficient = self.calculate_darkness(skylight[local_x, local_y, local_z], blocklight[local_x, local_y, local_z]) + # only say we're occluded if the point is in the CURRENT + # chunk, so that we don't get obvious inter-chunk dependencies + # (we want this here so we still have the default coefficient + # for occluded blocks, even when we don't report them as + # occluded) + if not is_local_chunk: + occluded = False + return (coefficient, occluded) def chunk_render(self, img=None, xoff=0, yoff=0, cave=False): @@ -509,16 +523,19 @@ class ChunkRenderer(object): img.paste(t[0], (imgx, imgy), t[1]) # top face - black_coeff, _ = self.get_lighting_coefficient(x, y, z + 1) - img.paste((0,0,0), (imgx, imgy), ImageEnhance.Brightness(facemasks[0]).enhance(black_coeff)) + black_coeff, face_occlude = self.get_lighting_coefficient(x, y, z + 1) + if not face_occlude: + img.paste((0,0,0), (imgx, imgy), ImageEnhance.Brightness(facemasks[0]).enhance(black_coeff)) # left face - black_coeff, _ = self.get_lighting_coefficient(x - 1, y, z) - img.paste((0,0,0), (imgx, imgy), ImageEnhance.Brightness(facemasks[1]).enhance(black_coeff)) + 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)) # right face - black_coeff, _ = self.get_lighting_coefficient(x, y + 1, z) - img.paste((0,0,0), (imgx, imgy), ImageEnhance.Brightness(facemasks[2]).enhance(black_coeff)) + black_coeff, face_occlude = self.get_lighting_coefficient(x, y + 1, z) + if not face_occlude: + img.paste((0,0,0), (imgx, imgy), ImageEnhance.Brightness(facemasks[2]).enhance(black_coeff)) # Draw edge lines if blockid in (44,): # step block From c11e23a18ff52db8c4f1dfe76c46fd26b92f71ef Mon Sep 17 00:00:00 2001 From: Aaron Griffith Date: Thu, 14 Oct 2010 13:57:48 -0400 Subject: [PATCH 11/13] added semi-correct lighting for half-blocks --- chunk.py | 38 +++++++++++++++++++++++++++++--------- 1 file changed, 29 insertions(+), 9 deletions(-) diff --git a/chunk.py b/chunk.py index 9cb5be2..67571f2 100644 --- a/chunk.py +++ b/chunk.py @@ -328,7 +328,7 @@ class ChunkRenderer(object): # Nighttime return 1.0 - pow(0.8, 15 - max(blocklight, skylight - 11)) - def get_lighting_coefficient(self, x, y, z): + def get_lighting_coefficient(self, x, y, z, norecurse=False): """Calculates the lighting coefficient for the given coordinate, using default lighting and peeking into neighboring chunks, if needed. A lighting coefficient of 1.0 @@ -337,10 +337,7 @@ class ChunkRenderer(object): Returns a tuple (coefficient, occluded), where occluded is True if the given coordinate is filled with a solid block, and therefore the returned coefficient is just the default.""" - - # fill it in with the default first (full skylight) - coefficient = self.calculate_darkness(15, 0) - + # placeholders for later data arrays, coordinates blocks = None skylight = None @@ -373,13 +370,36 @@ class ChunkRenderer(object): local_x >= 0 and local_x < 16 and local_y >= 0 and local_y < 16 and local_z >= 0 and local_z < 128): # we have no useful info, return default - return (coefficient, False) + return (self.calculate_darkness(15, 0), False) + + # special handling for half-blocks + # (don't recurse more than once!) + if blocks[local_x, local_y, local_z] == 44 and not norecurse: + # average gathering variables + averagegather = 0.0 + averagecount = 0 + + # how bright we need before we consider a side "lit" + threshold = self.calculate_darkness(0, 0) + # iterate through all the sides of the block + sides = [(x-1, y, z), (x+1, y, z), (x, y, z-1), (x, y, z+1), (x, y-1, z), (x, y+1, z)] + + for side in sides: + val, occ = self.get_lighting_coefficient(*side, norecurse=True) + if (not occ) and (val < threshold): + averagegather += val + averagecount += 1 + + # if at least one side was lit, return the average + if averagecount > 0: + return (averagegather / averagecount, False) # calculate the return... occluded = not (blocks[local_x, local_y, local_z] in transparent_blocks) - - # only calculate the coefficient if we're not occluded - if not occluded: + # only calculate the non-default coefficient if we're not occluded + if occluded: + coefficient = self.calculate_darkness(15, 0) + else: coefficient = self.calculate_darkness(skylight[local_x, local_y, local_z], blocklight[local_x, local_y, local_z]) # only say we're occluded if the point is in the CURRENT From 66e39f8b4d03d6930a387db4071ef467d74eef72 Mon Sep 17 00:00:00 2001 From: Aaron Griffith Date: Thu, 14 Oct 2010 14:02:04 -0400 Subject: [PATCH 12/13] made sure lava blocks are fully lit --- chunk.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/chunk.py b/chunk.py index 67571f2..510a4e2 100644 --- a/chunk.py +++ b/chunk.py @@ -372,9 +372,11 @@ class ChunkRenderer(object): # we have no useful info, return default return (self.calculate_darkness(15, 0), False) + blocktype = blocks[local_x, local_y, local_z] + # special handling for half-blocks # (don't recurse more than once!) - if blocks[local_x, local_y, local_z] == 44 and not norecurse: + if blocktype == 44 and not norecurse: # average gathering variables averagegather = 0.0 averagecount = 0 @@ -395,9 +397,13 @@ class ChunkRenderer(object): return (averagegather / averagecount, False) # calculate the return... - occluded = not (blocks[local_x, local_y, local_z] in transparent_blocks) + occluded = not (blocktype in transparent_blocks) + # only calculate the non-default coefficient if we're not occluded - if occluded: + if (blocktype == 10) or (blocktype == 11): + # lava blocks should always be lit! + coefficient = 0.0 + elif occluded: coefficient = self.calculate_darkness(15, 0) else: coefficient = self.calculate_darkness(skylight[local_x, local_y, local_z], blocklight[local_x, local_y, local_z]) From 35e664a32cf64c6b6eb2210914242b9db263485c Mon Sep 17 00:00:00 2001 From: Aaron Griffith Date: Thu, 14 Oct 2010 21:04:41 -0400 Subject: [PATCH 13/13] updated README to reflect new lighting options --- README.rst | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index a44cfed..5d8e158 100644 --- a/README.rst +++ b/README.rst @@ -190,6 +190,18 @@ Options a certain date. Or perhaps you can incrementally update your map by passing in a subset of chunks each time. It's up to you! +--lighting + This option enables map lighting, using lighting information stored by + Minecraft inside the chunks. This will make your map prettier, at the cost + of update speed. + + Note that for existing, unlit maps, you may want to clear your cache + (with -d) before updating the map to use lighting. Otherwise, only updated + chunks will have lighting enabled. + +--night + This option enables --lighting, and renders the world at night. + Viewing the Results ------------------- Within the output directory you will find two things: an index.html file, and a @@ -247,8 +259,6 @@ An incomplete list of things I want to do soon is: doors, and the like. Right now they are either not rendered at all, or rendered as if they were a cube, so it looks funny. -* Add lighting - * Some kind of graphical interface. * A Windows exe for easier access for Windows users.