Begin testing multiple implementations of relight algorithm.

Change relight API to have updateLightsByCoord, updateLightsInSelection, updateLightsInSection (incomplete)
Move time_xxx.py files from test/ into bench/
Split time_relight into manmade and natural.
Get time_relight_xxx working again.
Add get/set BlockLight/SkyLight to WorldEditor for pure python relight.
Implement pure python relight. Seems to work right.
This commit is contained in:
David Vierra 2015-06-23 14:13:33 -10:00
parent 987aa3e7c5
commit 9da7b289b2
18 changed files with 352 additions and 41 deletions

View File

@ -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()

View File

@ -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()

View File

@ -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.
"""

View File

@ -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,

View File

@ -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

View File

@ -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")

View File

@ -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

View File

View File

@ -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

View File

@ -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:

View File

@ -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()

View File

@ -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