/*
* Copyright 2012 Benjamin Glatzel <benjamin.glatzel@me.com>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.terasology.logic.world;
import java.util.Collection;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.spout.api.geo.cuboid.Chunk;
import org.terasology.math.Diamond3iIterator;
import org.terasology.math.Region3i;
import org.terasology.math.Side;
import org.terasology.math.Vector3i;
import org.terasology.teraspout.TeraBlock;
import org.terasology.teraspout.TeraChunk;
import com.google.common.collect.Lists;
/**
* @author Immortius
*/
public class LightPropagator {
private Logger logger = Logger.getLogger(getClass().getName());
private WorldView worldView;
public LightPropagator(WorldView worldView) {
this.worldView = worldView;
}
/**
* Propagates light out of the central chunk of the world view, "connecting" it to the surrounding chunks
* <p/>
* This expects the light propagator to be set up with a 3x3 world view offset so the center chunk is accessed as(0,0,0)
*/
public void propagateOutOfTargetChunk() {
int maxX = Chunk.BLOCKS.SIZE - 1;
int maxZ = Chunk.BLOCKS.SIZE - 1;
// Iterate over the blocks on the horizontal sides
for (int y = 0; y < Chunk.BLOCKS.SIZE; y++) {
for (int x = 0; x < Chunk.BLOCKS.SIZE; x++) {
propagateSunlightFrom(x, y, 0, Side.FRONT);
propagateSunlightFrom(x, y, maxZ, Side.BACK);
propagateLightFrom(x, y, 0, Side.FRONT);
propagateLightFrom(x, y, maxZ, Side.BACK);
}
for (int z = 0; z < Chunk.BLOCKS.SIZE; z++) {
propagateSunlightFrom(0, y, z, Side.LEFT);
propagateSunlightFrom(maxX, y, z, Side.RIGHT);
propagateLightFrom(0, y, z, Side.LEFT);
propagateLightFrom(maxX, y, z, Side.RIGHT);
}
}
}
/**
* Updates the lighting for a block change
*
* @param pos The position of the block
* @param type The new block type
* @param oldType The old block type
* @return The region affected by the light update
*/
public Region3i update(Vector3i pos, TeraBlock type, TeraBlock oldType) {
return update(pos.x, pos.y, pos.z, type, oldType);
}
/**
* Updates the lighting for a block change
*
* @param x
* @param y
* @param z
* @param type The new block type
* @param oldType The old block type
* @return The region affected by the light update
*/
public Region3i update(int x, int y, int z, TeraBlock type, TeraBlock oldType) {
return Region3i.createEncompassing(updateSunlight(x, y, z, type, oldType), updateLight(x, y, z, type, oldType));
}
private Region3i updateSunlight(int x, int y, int z, TeraBlock type, TeraBlock oldType) {
if (type.isTranslucent() == oldType.isTranslucent()) {
return Region3i.EMPTY;
}
if (type.isTranslucent()) {
byte light = pullSunlight(x, y, z);
worldView.setSunlight(x, y, z, light);
if (light > 1) {
return pushSunlight(x, y, z, light);
}
} else {
return clearSunlight(x, y, z);
}
return Region3i.EMPTY;
}
private Region3i updateLight(int x, int y, int z, TeraBlock type, TeraBlock oldType) {
byte currentLight = worldView.getLight(x, y, z);
byte lum = type.getLuminance();
// Newly transparent and we're not brighter than before, so pull in surrounding light and then push it out
if (type.isTranslucent() && !oldType.isTranslucent() && lum >= currentLight) {
byte newLight = pullLight(x, y, z, lum);
worldView.setLight(x, y, z, newLight);
return pushLight(x, y, z, newLight);
// Brighter than before, so push our light out
} else if (lum > currentLight) {
worldView.setLight(x, y, z, lum);
return pushLight(x, y, z, lum);
// Dimmer than before, and how lit the block was came from luminance before, reduce light levels
} else if (lum < currentLight && oldType.getLuminance() == currentLight) {
clearLight(x, y, z, currentLight);
return Region3i.createFromCenterExtents(new Vector3i(x, y, z), currentLight - 1);
}
return Region3i.EMPTY;
}
private byte pullSunlight(int x, int y, int z) {
byte light = 0;
if (y == Chunk.BLOCKS.SIZE - 1) {
light = TeraChunk.MAX_LIGHT;
} else {
light = (byte) Math.max(light, worldView.getSunlight(x, y + 1, z));
light = (byte) Math.max(light, worldView.getSunlight(x, y - 1, z) - 1);
for (Side side : Side.horizontalSides()) {
Vector3i adjPos = side.getVector3i();
light = (byte) Math.max(light, worldView.getSunlight(x + adjPos.x, y + adjPos.y, z + adjPos.z) - 1);
}
}
return light;
}
private byte pullLight(int x, int y, int z, byte newLight) {
for (Side side : Side.values()) {
Vector3i adjDir = side.getVector3i();
byte adjLight = (byte) ((worldView.getLight(x + adjDir.x, y + adjDir.y, z + adjDir.z)) - 1);
newLight = (adjLight > newLight) ? adjLight : newLight;
}
return newLight;
}
private Region3i pushSunlight(int x, int y, int z, byte lightLevel) {
Collection<Vector3i> currentWave = Lists.newArrayList();
Collection<Vector3i> nextWave = Lists.newArrayList();
nextWave.add(new Vector3i(x, y, z));
// First drop MAX_LIGHT until it is blocked
if (lightLevel == TeraChunk.MAX_LIGHT && worldView.getSunlight(x, y - 1, z) < TeraChunk.MAX_LIGHT) {
for (int columnY = y - 1; columnY >= 0; columnY--) {
TeraBlock block = worldView.getBlock(x, columnY, z);
if (sunlightRetainsFullStrengthIn(block)) {
worldView.setSunlight(x, columnY, z, lightLevel);
nextWave.add(new Vector3i(x, columnY, z));
} else {
break;
}
}
}
// Spread the sunlight
Region3i affectedRegion = Region3i.createFromMinAndSize(new Vector3i(x, y, z), Vector3i.one());
while (lightLevel > 1 && !nextWave.isEmpty()) {
Collection<Vector3i> temp = currentWave;
currentWave = nextWave;
nextWave = temp;
nextWave.clear();
// Only move sunlight up if it is below max light
if (lightLevel < TeraChunk.MAX_LIGHT) {
for (Vector3i pos : currentWave) {
// Move sunlight up
if (pos.y < Chunk.BLOCKS.SIZE - 2) {
Vector3i adjPos = new Vector3i(pos.x, pos.y + 1, pos.z);
TeraBlock block = worldView.getBlock(adjPos);
if (block.isTranslucent()) {
byte adjLight = worldView.getSunlight(adjPos);
if (adjLight < lightLevel - 1) {
worldView.setSunlight(adjPos, (byte) (lightLevel - 1));
nextWave.add(adjPos);
affectedRegion = affectedRegion.expandToContain(adjPos);
}
}
}
}
}
for (Vector3i pos : currentWave) {
// Move sunlight down
if (pos.y > 0) {
Vector3i adjPos = new Vector3i(pos.x, pos.y - 1, pos.z);
TeraBlock block = worldView.getBlock(adjPos);
if (block.isTranslucent()) {
byte adjLight = worldView.getSunlight(adjPos);
if (adjLight < lightLevel - 1) {
worldView.setSunlight(adjPos, (byte) (lightLevel - 1));
nextWave.add(adjPos);
affectedRegion = affectedRegion.expandToContain(adjPos);
}
}
}
}
// Move sunlight sideways
for (Vector3i pos : currentWave) {
for (Side side : Side.horizontalSides()) {
Vector3i adjPos = new Vector3i(pos);
adjPos.add(side.getVector3i());
try {
TeraBlock block = worldView.getBlock(adjPos);
if (block.isTranslucent()) {
byte adjLight = worldView.getSunlight(adjPos);
if (adjLight < lightLevel - 1) {
worldView.setSunlight(adjPos, (byte) (lightLevel - 1));
nextWave.add(adjPos);
affectedRegion = affectedRegion.expandToContain(adjPos);
}
}
} catch (ArrayIndexOutOfBoundsException e) {
logger.log(Level.SEVERE, String.format("Pushing Light %s %d %s", new Vector3i(x, y, z), lightLevel, worldView.getChunkRegion()), e);
}
}
}
lightLevel--;
}
return affectedRegion;
}
private Region3i pushLight(int x, int y, int z, byte lightLevel) {
Collection<Vector3i> currentWave = Lists.newArrayList();
Collection<Vector3i> nextWave = Lists.newArrayList();
nextWave.add(new Vector3i(x, y, z));
Region3i affectedRegion = Region3i.createFromMinAndSize(new Vector3i(x, y, z), Vector3i.one());
while (lightLevel > 1 && !nextWave.isEmpty()) {
Collection<Vector3i> temp = currentWave;
currentWave = nextWave;
nextWave = temp;
nextWave.clear();
for (Vector3i pos : currentWave) {
for (Side side : Side.values()) {
Vector3i adjPos = new Vector3i(pos);
adjPos.add(side.getVector3i());
if (adjPos.y < 0 || adjPos.y >= Chunk.BLOCKS.SIZE) {
continue;
}
TeraBlock block = worldView.getBlock(adjPos);
if (block.isTranslucent()) {
byte adjLight = worldView.getLight(adjPos);
if (adjLight < lightLevel - 1) {
worldView.setLight(adjPos, (byte) (lightLevel - 1));
nextWave.add(adjPos);
affectedRegion = affectedRegion.expandToContain(adjPos);
}
}
}
}
lightLevel--;
}
return affectedRegion;
}
private Region3i clearSunlight(int x, int y, int z) {
byte oldSunlight = worldView.getSunlight(x, y, z);
if (oldSunlight == TeraChunk.MAX_LIGHT) {
//logger.log(Level.INFO, "Full Recalculating sunlight");
worldView.setSunlight(x, y, z, (byte) 0);
fullRecalculateSunlightAround(x, y, z);
return Region3i.createFromMinAndSize(new Vector3i(x - TeraChunk.MAX_LIGHT + 1, 0, z - TeraChunk.MAX_LIGHT + 1), new Vector3i(2 * TeraChunk.MAX_LIGHT - 1, Chunk.BLOCKS.SIZE, 2 * TeraChunk.MAX_LIGHT - 1));
} else if (oldSunlight > 1) {
//logger.log(Level.INFO, "Local Recalculating sunlight");
localRecalculateSunlightAround(x, y, z, oldSunlight);
return Region3i.createFromCenterExtents(new Vector3i(x, y, z), oldSunlight - 1);
} else if (oldSunlight > 0) {
worldView.setSunlight(x, y, z, (byte) 0);
return Region3i.createFromCenterExtents(new Vector3i(x, y, z), 0);
}
return Region3i.EMPTY;
}
private void clearLight(int x, int y, int z, int oldLightLevel) {
List<Vector3i> lightSources = Lists.newArrayList();
// Clear old light, recording light sources
for (Vector3i pos : Diamond3iIterator.iterate(new Vector3i(x, y, z), oldLightLevel)) {
byte lum = worldView.getBlock(pos).getLuminance();
worldView.setLight(pos, lum);
if (lum > 1) {
lightSources.add(pos);
}
}
// Apply light sources
for (Vector3i pos : lightSources) {
byte lightLevel = worldView.getLight(pos);
if (lightLevel > 1) {
pushLight(pos.x, pos.y, pos.z, lightLevel);
}
}
// Draw in light from surrounding area
for (Vector3i pos : Diamond3iIterator.iterateAtDistance(new Vector3i(x, y, z), oldLightLevel + 1)) {
byte lightLevel = worldView.getLight(pos);
if (lightLevel > 1) {
pushLight(pos.x, pos.y, pos.z, lightLevel);
}
}
}
private void localRecalculateSunlightAround(int x, int y, int z, int oldLightLevel) {
// Clear old light, recording light sources
for (Vector3i pos : Diamond3iIterator.iterate(new Vector3i(x, y, z), oldLightLevel)) {
worldView.setSunlight(pos, (byte) 0);
}
// Draw in light from surrounding area
for (Vector3i pos : Diamond3iIterator.iterateAtDistance(new Vector3i(x, y, z), oldLightLevel + 1)) {
byte lightLevel = worldView.getSunlight(pos);
if (lightLevel > 1) {
pushSunlight(pos.x, pos.y, pos.z, lightLevel);
}
}
}
private void fullRecalculateSunlightAround(int blockX, int blockY, int blockZ) {
int top = Math.min(Chunk.BLOCKS.SIZE - 2, blockY + TeraChunk.MAX_LIGHT - 2);
Region3i region = Region3i.createFromMinMax(new Vector3i(blockX - TeraChunk.MAX_LIGHT + 1, 0, blockZ - TeraChunk.MAX_LIGHT + 1), new Vector3i(blockX + TeraChunk.MAX_LIGHT - 1, top, blockZ + TeraChunk.MAX_LIGHT - 1));
short[] tops = new short[region.size().x * region.size().z];
// Tunnel light down
for (int x = 0; x < region.size().x; x++) {
for (int z = 0; z < region.size().z; z++) {
int y = top;
byte aboveLight = worldView.getSunlight(x + region.min().x, y + 1, z + region.min().z);
if (aboveLight == TeraChunk.MAX_LIGHT) {
for (; y >= 0; y--) {
TeraBlock block = worldView.getBlock(x + region.min().x, y, z + region.min().z);
if (sunlightRetainsFullStrengthIn(block)) {
worldView.setSunlight(x + region.min().x, y, z + region.min().z, TeraChunk.MAX_LIGHT);
} else {
break;
}
}
}
tops[x + region.size().x * z] = (short) y;
for (; y >= 0; y--) {
worldView.setSunlight(x + region.min().x, y, z + region.min().z, (byte) 0);
}
}
}
// Spread internal to the changed column
for (int x = 0; x < region.size().x; x++) {
for (int z = 0; z < region.size().z; z++) {
// Pull light down
if (tops[x + region.size().x * z] > 0) {
propagateSunlightFrom(region.min().x + x, tops[x + region.size().x * z] + 1, region.min().z + z, Side.BOTTOM);
}
for (int y = tops[x + region.size().x * z]; y >= 0; y--) {
if (x <= 0 || tops[(x - 1) + region.size().x * z] < y) {
propagateSunlightFrom(region.min().x + x - 1, y, region.min().z + z, Side.RIGHT);
}
if (x >= region.size().x - 1 || tops[(x + 1) + region.size().x * z] < y) {
propagateSunlightFrom(region.min().x + x + 1, y, region.min().z + z, Side.LEFT);
}
if (z <= 0 || tops[x + region.size().x * (z - 1)] < y) {
propagateSunlightFrom(region.min().x + x, y, region.min().z + z - 1, Side.BACK);
}
if (z >= region.size().z - 1 || tops[x + region.size().x * (z + 1)] < y) {
propagateSunlightFrom(region.min().x + x, y, region.min().z + z + 1, Side.FRONT);
}
}
}
}
}
private void propagateSunlightFrom(int blockX, int blockY, int blockZ, Side side) {
byte lightLevel = worldView.getSunlight(blockX, blockY, blockZ);
Vector3i adjSide = new Vector3i(blockX, blockY, blockZ);
adjSide.add(side.getVector3i());
if (lightLevel > 1 && worldView.getSunlight(adjSide) < lightLevel - 1 && worldView.getBlock(adjSide).isTranslucent()) {
worldView.setSunlight(adjSide, (byte) (lightLevel - 1));
pushSunlight(adjSide.x, adjSide.y, adjSide.z, (byte) (lightLevel - 1));
}
}
private void propagateLightFrom(int blockX, int blockY, int blockZ, Side side) {
byte lightLevel = worldView.getLight(blockX, blockY, blockZ);
Vector3i adjSide = new Vector3i(blockX, blockY, blockZ);
adjSide.add(side.getVector3i());
if (lightLevel > 1 && worldView.getLight(adjSide) < lightLevel - 1 && worldView.getBlock(adjSide).isTranslucent()) {
worldView.setLight(adjSide, (byte) (lightLevel - 1));
pushLight(adjSide.x, adjSide.y, adjSide.z, (byte) (lightLevel - 1));
}
}
private boolean sunlightRetainsFullStrengthIn(TeraBlock block) {
return block.isTranslucent() && !block.isLiquid();
}
}