0

initial comit

This commit is contained in:
Andrew
2010-08-22 10:16:10 -04:00
commit 2eca1a5fb5
4 changed files with 586 additions and 0 deletions

142
chunk.py Normal file
View File

@@ -0,0 +1,142 @@
import numpy
from PIL import Image
from itertools import izip, count
import nbt
import textures
from textures import texturemap as txtarray
# General note about pasting transparent image objects onto an image with an
# alpha channel:
# If you use the image as its own mask, it will work fine only if the alpha
# channel is binary. If there's any translucent parts, then the alpha channel
# of the dest image will have its alpha channel modified. To prevent this:
# first use im.split() and take the third item which is the alpha channel and
# 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_lvldata(filename):
"""Takes a filename and returns the Level struct, which contains all the
level info"""
return nbt.load(filename)[1]['Level']
def get_blockarray(level):
"""Takes the level struct as returned from get_lvldata, and returns the
Block array, which just contains all the block ids"""
return numpy.frombuffer(level['Blocks'], dtype=numpy.uint8).reshape((16,16,128))
def get_blockarray_fromfile(filename):
"""Same as get_blockarray except takes a filename and uses get_lvldata to
open it. This is a shortcut"""
level = get_lvldata(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))
# This set holds blocks ids that can be seen through
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.
# 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
# Don't render
# 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
# 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
):
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
img.paste(t[0], (imgx, imgy), t[1])
finally:
# Do this no mater how the above block exits
imgy -= 12
return img

130
nbt.py Normal file
View File

@@ -0,0 +1,130 @@
import gzip
import struct
def load(fileobj):
if isinstance(fileobj, basestring):
# Is actually a filename
fileobj = open(fileobj, 'r')
return NBTFileReader(fileobj).read_all()
class NBTFileReader(object):
def __init__(self, fileobj):
self._file = gzip.GzipFile(fileobj=fileobj, mode='r')
# These private methods read the payload only of the following types
def _read_tag_end(self):
# Nothing to read
return 0
def _read_tag_byte(self):
byte = self._file.read(1)
return struct.unpack("b", byte)[0]
def _read_tag_short(self):
bytes = self._file.read(2)
return struct.unpack(">h", bytes)[0]
def _read_tag_int(self):
bytes = self._file.read(4)
return struct.unpack(">i", bytes)[0]
def _read_tag_long(self):
bytes = self._file.read(8)
return struct.unpack(">q", bytes)[0]
def _read_tag_float(self):
bytes = self._file.read(4)
return struct.unpack(">f", bytes)[0]
def _read_tag_double(self):
bytes = self._file.read(8)
return struct.unpack(">d", bytes)[0]
def _read_tag_byte_array(self):
length = self._read_tag_int()
bytes = self._file.read(length)
return bytes
def _read_tag_string(self):
length = self._read_tag_short()
# Read the string
string = self._file.read(length)
# decode it and return
return string.decode("UTF-8")
def _read_tag_list(self):
tagid = self._read_tag_byte()
length = self._read_tag_int()
read_tagmap = {
0: self._read_tag_end,
1: self._read_tag_byte,
2: self._read_tag_short,
3: self._read_tag_int,
4: self._read_tag_long,
5: self._read_tag_float,
6: self._read_tag_double,
7: self._read_tag_byte_array,
8: self._read_tag_string,
9: self._read_tag_list,
10:self._read_tag_compound,
}
read_method = read_tagmap[tagid]
l = []
for _ in xrange(length):
l.append(read_method())
return l
def _read_tag_compound(self):
# Build a dictionary of all the tag names mapping to their payloads
tags = {}
while True:
# Read a tag
tagtype = ord(self._file.read(1))
if tagtype == 0:
break
name = self._read_tag_string()
read_tagmap = {
0: self._read_tag_end,
1: self._read_tag_byte,
2: self._read_tag_short,
3: self._read_tag_int,
4: self._read_tag_long,
5: self._read_tag_float,
6: self._read_tag_double,
7: self._read_tag_byte_array,
8: self._read_tag_string,
9: self._read_tag_list,
10:self._read_tag_compound,
}
payload = read_tagmap[tagtype]()
tags[name] = payload
return tags
def read_all(self):
"""Reads the entire file and returns (name, payload)
name is the name of the root tag, and payload is a dictionary mapping
names to their payloads
"""
# Read tag type
tagtype = ord(self._file.read(1))
if tagtype != 10:
raise Exception("Expected a tag compound")
# Read the tag name
name = self._read_tag_string()
payload = self._read_tag_compound()
return name, payload

174
textures.py Normal file
View File

@@ -0,0 +1,174 @@
import os
import os.path
import zipfile
from cStringIO import StringIO
import math
import numpy
from PIL import Image, ImageEnhance
def _get_terrain_image():
minecraftjar = zipfile.ZipFile(os.path.join(os.environ['HOME'], ".minecraft", "bin", "minecraft.jar"))
textures = minecraftjar.open("terrain.png")
buffer = StringIO(textures.read())
return Image.open(buffer)
def _split_terrain(terrain):
"""Builds and returns a length 256 array of each 16x16 chunk of texture"""
textures = []
for y in xrange(16):
for x in xrange(16):
left = x*16
upper = y*16
right = left+16
lower = upper+16
region = terrain.crop((left,upper,right,lower))
textures.append(region)
return textures
# This maps terainids to 16x16 images
terrain_images = _split_terrain(_get_terrain_image())
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
"""
# Resize to 17x17, since the diagonal is approximately 24 pixels, a nice
# even number that can be split in half twice
img = img.resize((17, 17), Image.BILINEAR)
# Build the Affine transformation matrix for this perspective
transform = numpy.matrix(numpy.identity(3))
# Translate up and left, since rotations are about the origin
transform *= numpy.matrix([[1,0,8.5],[0,1,8.5],[0,0,1]])
# Rotate 45 degrees
ratio = math.cos(math.pi/4)
#transform *= numpy.matrix("[0.707,-0.707,0;0.707,0.707,0;0,0,1]")
transform *= numpy.matrix([[ratio,-ratio,0],[ratio,ratio,0],[0,0,1]])
# Translate back down and right
transform *= numpy.matrix([[1,0,-12],[0,1,-12],[0,0,1]])
# scale the image down by a factor of 2
transform *= numpy.matrix("[1,0,0;0,2,0;0,0,1]")
transform = numpy.array(transform)[:2,:].ravel().tolist()
newimg = img.transform((24,12), Image.AFFINE, transform)
return newimg
def _transform_image_side(img):
"""Takes an image and shears it for the left side of the cube (reflect for
the right side)"""
# Size of the cube side before shear
img = img.resize((12,12))
# Apply shear
transform = numpy.matrix(numpy.identity(3))
transform *= numpy.matrix("[1,0,0;-0.5,1,0;0,0,1]")
transform = numpy.array(transform)[:2,:].ravel().tolist()
newimg = img.transform((12,18), Image.AFFINE, transform)
return newimg
def _build_texturemap():
""""""
t = terrain_images
# Notes are for things I've left out or will probably have to make special
# exception for
top = [-1,1,0,2,16,4,15,17,205,205,237,237,18,19,32,33,
34,21,52,48,49,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, # Cloths are left out
-1,-1,-1,64,64,13,12,29,28,23,22,6,6,7,8,35, # Gold/iron blocks? Doublestep? TNT from above?
36,37,80,-1,65,4,25,101,98,24,43,-1,86,1,1,-1, # Torch from above? leaving out fire. Redstone wire? Crops left out. sign post
-1,-1,128,16,-1,-1,-1,-1,-1,51,51,-1,-1,1,66,67, # door,ladder left out. Minecart rail orientation
66,69,72,-1,74 # clay?
]
side = [-1,1,3,2,16,4,15,17,205,205,237,237,18,19,32,33,
34,21,52,48,49,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,
-1,-1,-1,64,64,13,12,29,28,23,22,6,6,7,8,35,
36,37,80,-1,65,4,25,101,98,24,43,-1,86,1,1,-1,
-1,-1,128,16,-1,-1,-1,-1,-1,51,51,-1,-1,1,66,67,
66,69,72,-1,74
]
side[2] = 2
return (
[(t[x] if x != -1 else None) for x in top],
[(_transform_image(t[x]) if x != -1 else None) for x in top],
[(_transform_image_side(t[x]) if x != -1 else None) for x in side],
)
# texturemap maps block ids to a 16x16 image that goes on the top face
# perspective_texturemap does the same, except the texture is rotated and shrunk
# shear_texturemap maps block ids to the image that goes on the side of the
# block, sheared appropriately
texturemap, perspective_texturemap, shear_texturemap = _build_texturemap()
def _render_sprite(img):
"""Takes a 16x16 sprite image, and returns a 22x22 image to go in the
blockmap
This is for rendering things that are sticking out of the ground, like
flowers and such
torches are drawn the same way, but torches that attach to walls are
handled differently
"""
pass
def _render_ground_image(img):
"""Takes a 16x16 sprite image and skews it to look like it's on the ground.
This is for things like mine track and such
"""
pass
def _build_blockimages():
"""Returns a mapping from blockid to an image of that block in perspective
The values of the mapping are actually (image in RGB mode, alpha channel)"""
# This maps block id to the texture that goes on the side of the block
allimages = []
for top, side in zip(perspective_texturemap, shear_texturemap):
if not top or not side:
allimages.append(None)
continue
img = Image.new("RGBA", (24,24))
otherside = side.transpose(Image.FLIP_LEFT_RIGHT)
# Darken the sides slightly. These methods also affect the alpha layer,
# so save them first (we don't want to "darken" the alpha layer making
# the block transparent)
if 1:
sidealpha = side.split()[3]
side = ImageEnhance.Brightness(side).enhance(0.9)
side.putalpha(sidealpha)
othersidealpha = otherside.split()[3]
otherside = ImageEnhance.Brightness(otherside).enhance(0.8)
otherside.putalpha(othersidealpha)
# Copy on the left side
img.paste(side, (0,6), side)
# Copy on the other side
img.paste(otherside, (12,6), otherside)
# Copy on the top piece (last so it's on top)
img.paste(top, (0,0), top)
# Manually touch up 6 pixels that leave a gap because of how the
# shearing works out. This makes the blocks perfectly tessellate-able
for x,y in [(13,23), (17,21), (21,19)]:
# Copy a pixel to x,y from x-1,y
img.putpixel((x,y), img.getpixel((x-1,y)))
for x,y in [(3,4), (7,2), (11,0)]:
# Copy a pixel to x,y from x+1,y
img.putpixel((x,y), img.getpixel((x+1,y)))
allimages.append((img.convert("RGB"), img.split()[3]))
return allimages
# Maps block images to the appropriate texture on each side. This map is not
# appropriate for all block types
blockmap = _build_blockimages()

140
world.py Normal file
View File

@@ -0,0 +1,140 @@
import functools
import string
import os
import os.path
import time
from PIL import Image
import chunk
base36decode = functools.partial(int, base=36)
def base36encode(number):
"""String repr of a number in base 32"""
if number==0: return '0'
alphabet = string.digits + string.lowercase
if number < 0:
number = -number
neg = True
else:
neg = False
base36 = ''
while number != 0:
number, i = divmod(number, 36)
base36 = alphabet[i] + base36
if neg:
return "-"+base36
else:
return base36
def find_chunkfiles(worlddir):
"""Returns a list of all the chunk file locations, and the file they
correspond to"""
all_chunks = []
for dirpath, dirnames, filenames in os.walk(worlddir):
if not dirnames and filenames:
for f in filenames:
p = f.split(".")
all_chunks.append((base36decode(p[1]), base36decode(p[2]),
os.path.join(dirpath, f)))
return all_chunks
def render_world(worlddir):
print "Scanning chunks..."
all_chunks = find_chunkfiles(worlddir)
total = len(all_chunks)
print "Done! {0} chunks found".format(total)
if not total:
return
# Create an image big enough for all chunks
# Each chunk is 352 pixels across. Each chunk is vertically 1584 pixels,
# but are spaced only 16*11=176 pixels apart.
# Imagine a diagonal coordinate system to address the chunks where
# increasing x goes up-right and increasing z goes down-right. This needs
# to be embedded in a square. How big is this square?
# Each column of chunks has a constant x+z sum of their coordinates, since
# going from a chunk to the one below it involves adding 1 to z and
# subtracting 1 from x. Therefore, the leftmost column is the one that
# minimizes x+z. The rightmost column maximizes x+z
# This means the total width of the image is max sum - the min sum, times
# the horizontal spacing between each neighboring chunk. Since the rows are
# staggered, each row takes up half its actual width: 352/2
# Similarly, each row of chunks has a constant difference between their x
# and z coordinate, since going from from a chunk to the one to its right
# involves an addition of 1 to both x and z.
# So the total height of the image must be the max diff - the min diff,
# times the vertical chunk spacing which is half of 16*11. Additionally,
# 1584-8*11 must be added to the height for the rest of the bottom layer of
# chunks.
# Furthermore, the chunks with the minimum z-x are placed on the image at
# y=0 (in image coordinates, not chunk coordinates). The chunks with the
# minimum x+z are placed on the image at x=0.
# Find the max and min sum and difference. Start out by finding the sum and
# diff of the first chunk
item = all_chunks[0]
minsum = maxsum = item[0] + item[1]
mindiff = maxdiff = item[1] - item[0]
for c in all_chunks:
s = c[0] + c[1]
minsum = min(minsum, s)
maxsum = max(maxsum, s)
d = c[1] - c[0]
mindiff = min(mindiff, d)
maxdiff = max(maxdiff, d)
width = (maxsum - minsum) * 384//2
height = (maxdiff-mindiff) * 8*12 + (12*128-8*12)
print "Final image will be {0}x{1}. (That's {2} bytes!)".format(
width, height, width*height*4)
# Oh god create a giant ass image
worldimg = Image.new("RGBA", (width, height))
# Sort the chunks by their row, so when we loop through them it goes top to
# bottom
print "Sorting chunks..."
all_chunks.sort(key=lambda x: x[1]-x[0])
print "Processing chunks!"
processed = 0
starttime = time.time()
for chunkx, chunky, chunkfile in all_chunks:
# Read in and render the chunk at world coordinates chunkx,chunky
# Where should this chunk go on the image?
column = chunkx + chunky - minsum
row = chunky - chunkx - mindiff
# col0 is at x=0. row0 is at y=0.
# Each col adds 384/2. Each row adds 16*12/2
imgx = 192 * column
imgy = 96 * row
print "Drawing chunk {0},{1} at pos {2},{3}".format(
chunkx, chunky,
imgx, imgy)
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
processed += 1
print "{0}/{1} chunks rendered. Avg {2}s per chunk".format(processed, total,
(time.time()-starttime)/processed)
print "All done!"
return worldimg