diff --git a/src/mceditlib/test/time_exportimport.py b/src/mceditlib/bench/time_exportimport.py similarity index 100% rename from src/mceditlib/test/time_exportimport.py rename to src/mceditlib/bench/time_exportimport.py diff --git a/src/mceditlib/test/time_fill.py b/src/mceditlib/bench/time_fill.py similarity index 100% rename from src/mceditlib/test/time_fill.py rename to src/mceditlib/bench/time_fill.py diff --git a/src/mceditlib/test/time_getsetblocks.py b/src/mceditlib/bench/time_getsetblocks.py similarity index 100% rename from src/mceditlib/test/time_getsetblocks.py rename to src/mceditlib/bench/time_getsetblocks.py diff --git a/src/mceditlib/test/time_loadsave.py b/src/mceditlib/bench/time_loadsave.py similarity index 100% rename from src/mceditlib/test/time_loadsave.py rename to src/mceditlib/bench/time_loadsave.py diff --git a/src/mceditlib/test/time_nbt.py b/src/mceditlib/bench/time_nbt.py similarity index 100% rename from src/mceditlib/test/time_nbt.py rename to src/mceditlib/bench/time_nbt.py diff --git a/src/mceditlib/bench/time_relight_manmade.py b/src/mceditlib/bench/time_relight_manmade.py new file mode 100644 index 0000000..d8564e8 --- /dev/null +++ b/src/mceditlib/bench/time_relight_manmade.py @@ -0,0 +1,52 @@ +from timeit import timeit + +import numpy +from mceditlib.selection import BoundingBox + +from mceditlib.worldeditor import WorldEditor +from mceditlib.test import templevel +from mceditlib import relight + + +def manmade_relight(): + world = templevel.TempLevel("AnvilWorld") + dim = world.getDimension() + stationEditor = WorldEditor("test_files/station.schematic") + station = stationEditor.getDimension() + + times = 1 + boxes = [] + + for x in range(times): + for z in range(times): + origin = (x * station.bounds.width, 54, z * station.bounds.length) + boxes.append(BoundingBox(origin, station.bounds.size)) + dim.copyBlocks(station, station.bounds, origin, create=True) + + box = reduce(lambda a, b: a.union(b), boxes) + + positions = [] + for cx, cz in box.chunkPositions(): + for cy in box.sectionPositions(cx, cz): + positions.append((cx, cy, cz)) + + poses = iter(positions) + + def do_relight(): + cx, cy, cz = poses.next() + print "Relighting section %s..." % ((cx, cy, cz),) + indices = numpy.indices((16, 16, 16), numpy.uint32) + indices.shape = 3, 16*16*16 + indices += ([cx], [cy], [cz]) + x, y, z = indices + relight.updateLightsByCoord(dim, x, y, z) + + sectionCount = 5 + t = timeit(do_relight, number=sectionCount) + print "Relight manmade building: %d chunk-sections in %.02f seconds (%dms per section)" % (sectionCount, t, 1000 * t / sectionCount) + +if __name__ == '__main__': + manmade_relight() + + + diff --git a/src/mceditlib/bench/time_relight_natural.py b/src/mceditlib/bench/time_relight_natural.py new file mode 100644 index 0000000..3ab056c --- /dev/null +++ b/src/mceditlib/bench/time_relight_natural.py @@ -0,0 +1,38 @@ +import numpy +from mceditlib.worldeditor import WorldEditor +from timeit import timeit + +from mceditlib.test import templevel +from mceditlib import relight + +# run me with the source checkout as the working dir so I can find the test_files folder. + +def natural_relight(): + world = templevel.TempLevel("AnvilWorld") + dim = world.getDimension() + positions = [] + for cx, cz in dim.chunkPositions(): + chunk = dim.getChunk(cx, cz) + for cy in chunk.sectionPositions(): + positions.append((cx, cy, cz)) + + poses = iter(positions) + + def do_relight(): + cx, cy, cz = poses.next() + print "Relighting section %s..." % ((cx, cy, cz),) + indices = numpy.indices((16, 16, 16), numpy.uint32) + indices.shape = 3, 16*16*16 + indices += ([cx], [cy], [cz]) + x, y, z = indices + relight.updateLightsByCoord(dim, x, y, z) + + sectionCount = 5 + t = timeit(do_relight, number=sectionCount) + print "Relight natural terrain: %d chunk-sections in %.02f seconds (%dms per section)" % (sectionCount, t, 1000 * t / sectionCount) + +if __name__ == '__main__': + natural_relight() + + + diff --git a/src/mceditlib/test/time_storagechain.py b/src/mceditlib/bench/time_storagechain.py similarity index 100% rename from src/mceditlib/test/time_storagechain.py rename to src/mceditlib/bench/time_storagechain.py diff --git a/src/mceditlib/fakechunklevel.py b/src/mceditlib/fakechunklevel.py index 249bd63..2bd4c30 100644 --- a/src/mceditlib/fakechunklevel.py +++ b/src/mceditlib/fakechunklevel.py @@ -59,7 +59,7 @@ class FakeChunkData(object): arrays or its rootTag. Alternately, just set `chunk.dirty = True` needsLighting is deprecated; Use the updateLights= argument - of setBlocks and other editing functions, or call updateLights(level, x, y, z) to + of setBlocks and other editing functions, or call updateLightsByCoord(level, x, y, z) to explicitly update lights yourself. """ diff --git a/src/mceditlib/multi_block.py b/src/mceditlib/multi_block.py index 8d14aa6..248683e 100644 --- a/src/mceditlib/multi_block.py +++ b/src/mceditlib/multi_block.py @@ -356,7 +356,7 @@ def setBlocks(dimension, x, y, z, chunk.dirty = True if updateLights: - relight.updateLights(dimension, x, y, z) + relight.updateLightsByCoord(dimension, x, y, z) def setChunkBlocks(chunk, x, y, z, diff --git a/src/mceditlib/operations/block_fill.py b/src/mceditlib/operations/block_fill.py index 1e13ac8..2444734 100644 --- a/src/mceditlib/operations/block_fill.py +++ b/src/mceditlib/operations/block_fill.py @@ -140,7 +140,7 @@ class FillBlocksOperation(Operation): z = coords[1] + (cz << 4) x = coords[2] + (cx << 4) - mceditlib.relight.updateLights(self.dimension, x, y, z) + mceditlib.relight.updateLightsByCoord(self.dimension, x, y, z) def include(ref): return ref.Position not in self.selection diff --git a/src/mceditlib/relight/__init__.py b/src/mceditlib/relight/__init__.py new file mode 100644 index 0000000..935b988 --- /dev/null +++ b/src/mceditlib/relight/__init__.py @@ -0,0 +1,28 @@ +""" + __init__ +""" +from __future__ import absolute_import, division, print_function, unicode_literals +import logging + +log = logging.getLogger(__name__) + +def setMethod(name): + if name == "pure": + from mceditlib.relight import pure_python + setModule(pure_python) + if name == "cython": + from mceditlib.relight import with_cython + setModule(with_cython) + if name == "setBlocks": + from mceditlib.relight import with_setblocks + setModule(with_setblocks) + if name == "sections": + from mceditlib.relight import with_sections + setModule(with_sections) + +def setModule(mod): + global updateLightsByCoord, updateLightsInSelection + updateLightsByCoord = mod.updateLightsByCoord + updateLightsInSelection = mod.updateLightsInSelection + +setMethod("pure") \ No newline at end of file diff --git a/src/mceditlib/relight/pure_python.py b/src/mceditlib/relight/pure_python.py new file mode 100644 index 0000000..d232af1 --- /dev/null +++ b/src/mceditlib/relight/pure_python.py @@ -0,0 +1,98 @@ +""" + pure_python +""" +from __future__ import absolute_import, division, print_function, unicode_literals +import logging + +log = logging.getLogger(__name__) + +def updateLightsByCoord(dim, x, y, z): + for i in range(len(x)): + updateLights(dim, x[i], y[i], z[i]) + +def neighbors(x, y, z): + yield x-1, y, z + yield x+1, y, z + yield x, y-1, z + yield x, y+1, z + yield x, y, z-1 + yield x, y, z+1 + + +def updateLights(dim, x, y, z): + # import pdb; pdb.set_trace() + previousLight = dim.getBlockLight(x, y, z) + light = dim.getBlock(x, y, z).brightness + dim.setBlockLight(x, y, z, light) + + drawLight(dim, x, y, z) + + if previousLight < light: + spreadLight(dim, x, y, z) + + if previousLight > light: + fadeLight(dim, x, y, z, previousLight) + +def getOpacity(dim, x, y, z): + return max(1, dim.getBlock(x, y, z).opacity) + +def drawLight(dim, x, y, z): + opacity = getOpacity(dim, x, y, z) + + for nx, ny, nz in neighbors(x, y, z): + adjacentLight = dim.getBlockLight(nx, ny, nz) + if adjacentLight - opacity > dim.getBlockLight(x, y, z): + dim.setBlockLight(x, y, z, adjacentLight - opacity) + +def spreadLight(dim, x, y, z): + light = dim.getBlockLight(x, y, z) + if light <= 0: + return + + for nx, ny, nz in neighbors(x, y, z): + + # xxx cast to int because one of these is a numpy.uint8 and + # light - opacity rolls over to a large number. + adjacentLight = int(dim.getBlockLight(nx, ny, nz)) + adjacentOpacity = getOpacity(dim, nx, ny, nz) + newLight = light - adjacentOpacity + # If the adjacent cell already has the "correct" light value, stop. + if newLight > adjacentLight: + dim.setBlockLight(nx, ny, nz, newLight) + spreadLight(dim, nx, ny, nz) + + +def fadeLight(dim, x, y, z, previousLight): + fadedCells = findFadedCells(dim, x, y, z, previousLight) + for x, y, z in fadedCells: + dim.setBlockLight(x, y, z, dim.getBlock(x, y, z).brightness) + # dim.setBlock(x, y, z, "glass") + for x, y, z in fadedCells: + drawLight(dim, x, y, z) + for x, y, z in fadedCells: + spreadLight(dim, x, y, z) + + +def relCoords(ox, oy, oz, coords): + for x, y, z, l in coords: + yield x - ox, y - oy, z - oz + + +def findFadedCells(dim, ox, oy, oz, oPreviousLight): + foundCells = set() + toScan = [(ox, oy, oz, oPreviousLight)] + + while len(toScan): + x, y, z, previousLight = toScan.pop(0) + for nx, ny, nz in neighbors(x, y, z): + + adjacentLight = int(dim.getBlockLight(nx, ny, nz)) + adjacentOpacity = getOpacity(dim, nx, ny, nz) + if previousLight - adjacentOpacity <= 0: + continue + if previousLight - adjacentOpacity == adjacentLight: + if (nx, ny, nz) not in foundCells: + toScan.append((nx, ny, nz, adjacentLight)) + foundCells.add((nx, ny, nz)) + + return foundCells diff --git a/src/mceditlib/relight/with_cython.pyx b/src/mceditlib/relight/with_cython.pyx new file mode 100644 index 0000000..e69de29 diff --git a/src/mceditlib/relight/with_sections.py b/src/mceditlib/relight/with_sections.py new file mode 100644 index 0000000..b4bdee1 --- /dev/null +++ b/src/mceditlib/relight/with_sections.py @@ -0,0 +1,88 @@ +""" + with_sections +""" +from __future__ import absolute_import, division, print_function, unicode_literals +import logging +import numpy +from mceditlib.exceptions import ChunkNotPresent + +log = logging.getLogger(__name__) + +def updateLightsInSelection(dim, selection): + for cx, cz in selection.chunkPositions(): + for cy in selection.sectionPositions(cx, cz): + updateLightsInSection(dim, cx, cy, cz) + +def updateLightsByCoord(dim, x, y, z): + """ + + :param dim: + :param x: + :param y: + :param z: + :return: + """ + + # gross. + cx = numpy.asanyarray(x) >> 4 + cy = numpy.asanyarray(y) >> 4 + cz = numpy.asanyarray(z) >> 4 + + cx = numpy.unique(cx) + cy = numpy.unique(cy) + cz = numpy.unique(cz) + + for i in range(len(cx)): + updateLightsInSection(dim, cx[i], cy[i], cz[i]) + + +def updateLightsInSection(dim, cx, cy, cz): + try: + chunk = dim.getChunk(cx, cz) + except ChunkNotPresent: + return + + section = chunk.getSection(cy) + if section is None: + return + + light = section.BlockLight + blocks = section.Blocks + + # Reset all lights to block brightness values + # xxx if BlockLight + light[:] = dim.blocktypes.brightness[blocks] + + # Get all block opacities only once + opacity = dim.blocktypes.opacity[blocks] + + directions = ((0, -1), + (0, 1), + (1, -1), + (1, 1), + (2, -1), + (2, 1)) + + for axis, direction in directions: + leftSlices = [None, None, None] + rightSlices = [None, None, None] + if direction == 1: + leftSlices[axis] = slice(None, -1) + rightSlices[axis] = slice(1, None) + else: + leftSlices[axis] = slice(1, None) + rightSlices[axis] = slice(None, -1) + + leftLight = light[leftSlices] + rightOpacity = opacity[rightSlices] + + newRightLight = leftLight - rightOpacity + # BlockLight is unsigned. Lights that were zero or less are now huge, so clip light values + newRightLight.view('int8').clip(0, 15, newRightLight) + + changed = newRightLight != leftLight + + + + + diff --git a/src/mceditlib/relight.py b/src/mceditlib/relight/with_setblocks.py similarity index 97% rename from src/mceditlib/relight.py rename to src/mceditlib/relight/with_setblocks.py index ce293de..a00fc6a 100644 --- a/src/mceditlib/relight.py +++ b/src/mceditlib/relight/with_setblocks.py @@ -140,7 +140,12 @@ def updateHeightmap(dimension, x, y, z): ENABLE_LIGHTING = True -def updateLights(dimension, x, y, z): +def updateLightsInSelection(dimension, selection): + # xxx slow + x, y, z = numpy.array(selection.positions).T + return updateLightsByCoord(dimension, x, y, z) + +def updateLightsByCoord(dimension, x, y, z): """ :param dimension: diff --git a/src/mceditlib/test/time_relight.py b/src/mceditlib/test/time_relight.py deleted file mode 100644 index f50d499..0000000 --- a/src/mceditlib/test/time_relight.py +++ /dev/null @@ -1,37 +0,0 @@ -from mceditlib.worldeditor import WorldEditor -from timeit import timeit - -import templevel - -#import logging -#logging.basicConfig(level=logging.INFO) - -def natural_relight(): - world = templevel.TempLevel("AnvilWorld") - dim = world.getDimension() - t = timeit(lambda: dim.generateLights(dim.chunkPositions()), number=1) - print "Relight natural terrain: %d chunks in %.02f seconds (%.02fms per chunk)" % (dim.chunkCount, t, - t / world.chunkCount * 1000) - - -def manmade_relight(): - t = templevel.TempLevel("TimeRelight", createFunc=lambda f:WorldEditor(f, create=True)) - - world = t - station = WorldEditor("test_files/station.schematic") - - times = 2 - - for x in range(times): - for z in range(times): - world.copyBlocksFrom(station, station.bounds, (x * station.Width, 63, z * station.Length), create=True) - - t = timeit(lambda: world.generateLights(world.chunkPositions), number=1) - print "Relight manmade building: %d chunks in %.02f seconds (%.02fms per chunk)" % (world.chunkCount, t, t / world.chunkCount * 1000) - -if __name__ == '__main__': - natural_relight() - manmade_relight() - - - diff --git a/src/mceditlib/worldeditor.py b/src/mceditlib/worldeditor.py index 8056bce..7625586 100644 --- a/src/mceditlib/worldeditor.py +++ b/src/mceditlib/worldeditor.py @@ -849,6 +849,45 @@ class WorldEditorDimension(object): array[y & 0xf, z & 0xf, x & 0xf] = value chunk.dirty = True + def getLight(self, arrayName, x, y, z, default=0): + cx = x >> 4 + cy = y >> 4 + cz = z >> 4 + if self.containsChunk(cx, cz): + chunk = self.getChunk(cx, cz) + sec = chunk.getSection(cy) + if sec: + array = getattr(sec, arrayName) + if array is not None: + return array[y & 0xf, z & 0xf, x & 0xf] + return default + + def setLight(self, arrayName, x, y, z, value): + cx = x >> 4 + cy = y >> 4 + cz = z >> 4 + if self.containsChunk(cx, cz): + chunk = self.getChunk(cx, cz) + sec = chunk.getSection(cy, create=True) + if sec: + array = getattr(sec, arrayName) + assert array is not None + if array is not None: + array[y & 0xf, z & 0xf, x & 0xf] = value + chunk.dirty = True + + def getBlockLight(self, x, y, z, default=0): + return self.getLight("BlockLight", x, y, z, default) + + def setBlockLight(self, x, y, z, value): + return self.setLight("BlockLight", x, y, z, value) + + def getSkyLight(self, x, y, z, default=0): + return self.getLight("SkyLight", x, y, z, default) + + def setSkyLight(self, x, y, z, value): + return self.setLight("SkyLight", x, y, z, value) + def getBiomeID(self, x, z, default=0): cx = x >> 4 cz = z >> 4