Minecraft-Overviewer/textures.py

262 lines
9.3 KiB
Python

# This file is part of the Minecraft Overviewer.
#
# Minecraft Overviewer is free software: you can redistribute it and/or
# modify it under the terms of the GNU General Public License as published
# by the Free Software Foundation, either version 3 of the License, or (at
# your option) any later version.
#
# Minecraft Overviewer is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
# Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with the Overviewer. If not, see <http://www.gnu.org/licenses/>.
import sys
import os
import os.path
import zipfile
from cStringIO import StringIO
import math
import numpy
from PIL import Image, ImageEnhance
def _find_file(filename, mode="rb"):
"""Searches for the given file and returns an open handle to it.
This searches the following locations in this order:
* The program dir (same dir as this file)
* On Darwin, in /Applications/Minecraft
* Inside minecraft.jar, which is looked for at these locations
* On Windows, at %APPDATA%/.minecraft/bin/minecraft.jar
* On Darwin, at $HOME/Library/Application Support/minecraft/bin/minecraft.jar
* at $HOME/.minecraft/bin/minecraft.jar
* The current working directory
* The program dir / textures
"""
programdir = os.path.dirname(__file__)
path = os.path.join(programdir, filename)
if os.path.exists(path):
return open(path, mode)
if sys.platform == "darwin":
path = os.path.join("/Applications/Minecraft", filename)
if os.path.exists(path):
return open(path, mode)
# Find minecraft.jar.
jarpaths = []
if "APPDATA" in os.environ:
jarpaths.append( os.path.join(os.environ['APPDATA'], ".minecraft",
"bin", "minecraft.jar"))
if "HOME" in os.environ:
jarpaths.append(os.path.join(os.environ['HOME'], "Library",
"Application Support", "minecraft","bin","minecraft.jar"))
jarpaths.append(os.path.join(os.environ['HOME'], ".minecraft", "bin",
"minecraft.jar"))
for jarpath in jarpaths:
if os.path.exists(jarpath):
jar = zipfile.ZipFile(jarpath)
try:
return jar.open(filename)
except KeyError:
pass
path = filename
if os.path.exists(path):
return open(path, mode)
path = os.path.join(programdir, "textures", filename)
if os.path.exists(path):
return open(path, mode)
raise IOError("Could not find the file {0}. Is Minecraft installed? If so, I couldn't find the minecraft.jar file.".format(filename))
def _load_image(filename):
"""Returns an image object"""
fileobj = _find_file(filename)
buffer = StringIO(fileobj.read())
return Image.open(buffer)
def _get_terrain_image():
return _load_image("terrain.png")
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_block(top, side):
"""From a top texture and a side texture, build a block image.
top and side should be 16x16 image objects. Returns a 24x24 image
"""
img = Image.new("RGBA", (24,24), (38,92,255,0))
top = _transform_image(top)
if not side:
img.paste(top, (0,0), top)
return img
side = _transform_image_side(side)
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)
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)
img.paste(side, (0,6), side)
img.paste(otherside, (12,6), otherside)
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)))
return img
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 is not appropriate for all block types, only block types that are
proper cubes"""
# Top textures of all block types. The number here is the index in the
# texture array (terrain_images), which comes from terrain.png's cells, left to right top to
# bottom.
topids = [-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,-1,-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,-1,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?
]
# And side textures of all block types
sideids = [-1,1,3,2,16,4,15,17,205,205,237,237,18,19,32,33,
34,20,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,-1,-1,65,4,25,101,98,24,43,-1,86,1,1,-1,
-1,-1,-1,16,-1,-1,-1,-1,-1,51,51,-1,-1,1,66,67,
66,69,72,-1,74
]
# This maps block id to the texture that goes on the side of the block
allimages = []
for toptextureid, sidetextureid in zip(topids, sideids):
if toptextureid == -1 or sidetextureid == -1:
allimages.append(None)
continue
toptexture = terrain_images[toptextureid]
sidetexture = terrain_images[sidetextureid]
img = _build_block(toptexture, sidetexture)
allimages.append((img.convert("RGB"), img.split()[3]))
# Future block types:
while len(allimages) < 256:
allimages.append(None)
return allimages
blockmap = _build_blockimages()
def load_water():
"""Evidentially, the water and lava textures are not loaded from any files
in the jar (that I can tell). They must be generated on the fly. While
terrain.png does have some water and lava cells, not all texture packs
include them. So I load them here from a couple pngs included.
This mutates the blockmap global list with the new water and lava blocks.
Block 9, standing water, is given a block with only the top face showing.
Block 8, flowing water, is given a full 3 sided cube."""
watertexture = _load_image("water.png")
w1 = _build_block(watertexture, None)
blockmap[9] = w1.convert("RGB"), w1
w2 = _build_block(watertexture, watertexture)
blockmap[8] = w2.convert("RGB"), w2
lavatexture = _load_image("lava.png")
lavablock = _build_block(lavatexture, lavatexture)
blockmap[10] = lavablock.convert("RGB"), lavablock
blockmap[11] = blockmap[10]
load_water()