uses multiprocessing to speed up rendering. Caches chunks
This commit is contained in:
126
chunk.py
126
chunk.py
@@ -1,6 +1,8 @@
|
|||||||
import numpy
|
import numpy
|
||||||
from PIL import Image
|
from PIL import Image, ImageDraw
|
||||||
from itertools import izip, count
|
from itertools import izip, count
|
||||||
|
import os.path
|
||||||
|
import hashlib
|
||||||
|
|
||||||
import nbt
|
import nbt
|
||||||
import textures
|
import textures
|
||||||
@@ -38,14 +40,87 @@ def get_skylight_array(level):
|
|||||||
"""
|
"""
|
||||||
return numpy.frombuffer(level['SkyLight'], dtype=numpy.uint8).reshape((16,16,64))
|
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])
|
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):
|
def render_and_save(chunkfile, cave=False):
|
||||||
level = get_lvldata(chunkfile)
|
a = ChunkRenderer(chunkfile)
|
||||||
blocks = get_blockarray(level)
|
return a.render_and_save(cave)
|
||||||
|
|
||||||
|
class ChunkRenderer(object):
|
||||||
|
def __init__(self, chunkfile):
|
||||||
|
if not os.path.exists(chunkfile):
|
||||||
|
raise ValueError("Could not find chunkfile")
|
||||||
|
self.chunkfile = chunkfile
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
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]
|
||||||
|
|
||||||
|
|
||||||
|
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(),
|
||||||
|
)
|
||||||
|
|
||||||
|
dest_path = os.path.join(destdir, dest_filename)
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
# Render the chunk
|
||||||
|
img = self.chunk_render(cave=cave)
|
||||||
|
# Save it
|
||||||
|
img.save(dest_path)
|
||||||
|
# Return its location
|
||||||
|
return dest_path
|
||||||
|
|
||||||
|
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:
|
if cave:
|
||||||
skylight = get_skylight_array(level)
|
skylight = get_skylight_array(self.level)
|
||||||
# Cave mode. Actually go through and 0 out all blocks that are not in a
|
# Cave mode. Actually go through and 0 out all blocks that are not in a
|
||||||
# cave, so that it only renders caves.
|
# cave, so that it only renders caves.
|
||||||
|
|
||||||
@@ -63,8 +138,6 @@ def chunk_render(chunkfile, img=None, xoff=0, yoff=0, cave=False):
|
|||||||
blocks = blocks.copy()
|
blocks = blocks.copy()
|
||||||
blocks[skylight_expanded != 0] = 21
|
blocks[skylight_expanded != 0] = 21
|
||||||
|
|
||||||
# Don't render
|
|
||||||
|
|
||||||
|
|
||||||
# Each block is 24x24
|
# 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 X axis adds 12px to x and subtracts 6px from y in the image
|
||||||
@@ -82,7 +155,6 @@ def chunk_render(chunkfile, img=None, xoff=0, yoff=0, cave=False):
|
|||||||
imgy = yoff - x*6 + y*6 + 128*12 + 16*12//2
|
imgy = yoff - x*6 + y*6 + 128*12 + 16*12//2
|
||||||
for z in xrange(128):
|
for z in xrange(128):
|
||||||
try:
|
try:
|
||||||
|
|
||||||
blockid = blocks[x,y,z]
|
blockid = blocks[x,y,z]
|
||||||
t = textures.blockmap[blockid]
|
t = textures.blockmap[blockid]
|
||||||
if not t:
|
if not t:
|
||||||
@@ -133,10 +205,46 @@ def chunk_render(chunkfile, img=None, xoff=0, yoff=0, cave=False):
|
|||||||
# we're not on the edge
|
# we're not on the edge
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
# 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])
|
img.paste(t[0], (imgx, imgy), t[1])
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
# Do this no mater how the above block exits
|
# Do this no mater how the above block exits
|
||||||
imgy -= 12
|
imgy -= 12
|
||||||
|
|
||||||
return img
|
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()
|
||||||
|
|||||||
22
world.py
22
world.py
@@ -3,6 +3,7 @@ import string
|
|||||||
import os
|
import os
|
||||||
import os.path
|
import os.path
|
||||||
import time
|
import time
|
||||||
|
import multiprocessing
|
||||||
|
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
@@ -42,7 +43,7 @@ def find_chunkfiles(worlddir):
|
|||||||
os.path.join(dirpath, f)))
|
os.path.join(dirpath, f)))
|
||||||
return all_chunks
|
return all_chunks
|
||||||
|
|
||||||
def render_world(worlddir):
|
def render_world(worlddir, cavemode=False):
|
||||||
print "Scanning chunks..."
|
print "Scanning chunks..."
|
||||||
all_chunks = find_chunkfiles(worlddir)
|
all_chunks = find_chunkfiles(worlddir)
|
||||||
|
|
||||||
@@ -109,6 +110,14 @@ def render_world(worlddir):
|
|||||||
print "Sorting chunks..."
|
print "Sorting chunks..."
|
||||||
all_chunks.sort(key=lambda x: x[1]-x[0])
|
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!"
|
print "Processing chunks!"
|
||||||
processed = 0
|
processed = 0
|
||||||
starttime = time.time()
|
starttime = time.time()
|
||||||
@@ -128,8 +137,13 @@ def render_world(worlddir):
|
|||||||
print "It's in column {0} row {1}".format(column, row)
|
print "It's in column {0} row {1}".format(column, row)
|
||||||
|
|
||||||
# Read it and render
|
# Read it and render
|
||||||
chunk.chunk_render(chunkfile, worldimg, imgx, imgy, cave=True)
|
result = resultsmap[(chunkx, chunky)]
|
||||||
# chunk chunk chunk chunk
|
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
|
processed += 1
|
||||||
|
|
||||||
@@ -137,4 +151,6 @@ def render_world(worlddir):
|
|||||||
(time.time()-starttime)/processed)
|
(time.time()-starttime)/processed)
|
||||||
|
|
||||||
print "All done!"
|
print "All done!"
|
||||||
|
print "Took {0} minutes".format((time.time()-starttime)/60)
|
||||||
return worldimg
|
return worldimg
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user