/*
* This file is part of the Illarion project.
*
* Copyright © 2015 - Illarion e.V.
*
* Illarion is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Illarion 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.
*/
package org.illarion.engine.graphic;
import illarion.common.types.ServerCoordinate;
import org.jetbrains.annotations.Contract;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.Arrays;
import java.util.Objects;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* This class handles a light source and contains its rays, the location and the color of the light.
*
* @author Nop
* @author Martin Karing <nitram@illarion.org>
*/
public final class LightSource {
private final int encodedValue;
/**
* The flag of this light source was already deleted.
*/
private boolean disposed;
/**
* The brightness of the light. This acts like a general modifier on the rightness of the light that reduces
* anyway with increasing distance from the light source.
*/
private final double bright;
/**
* The color of the light itself.
*/
@Nonnull
private final Color color;
/**
* The dirty flag, this is set to true in case there are further calculations needed and to false in case all
* calculations are done.
*/
private boolean dirty;
private boolean calculating;
/**
* The intensity array stores the calculated light intensity values. These result from the pre-calculated light
* rays along with the situation on the map such as objects that block out the light.
*/
@Nonnull
private final double[][] intensity;
/**
* Invert flag. If this is set to true it results in a reduce of the light share on a tile instead of a increase.
*/
private final boolean invert;
/**
* The location of the light source on the map.
*/
@Nonnull
private ServerCoordinate location;
/**
* The reference map that is used to get the data how the light spreads on the map.
*/
private LightingMap mapSource;
/**
* The light rays that spread from the light source.
*/
private final LightRays rays;
/**
* The length of the light rays that are send out by this light source.
*/
private final int size;
@Nonnull
private final Lock calculationLock;
/**
* Constructor for a new light source at a given location with some encoded
* settings.
*
* @param location the location of the light source on the server map
* @param encoding the encoding of the light, this contains the color, the
* brightness, the size and the inversion flag
*/
public LightSource(@Nonnull ServerCoordinate location, int encoding) {
encodedValue = encoding;
int newSize = (encoding / 10000) % 10;
rays = LightRays.getRays(newSize);
intensity = new double[(newSize * 2) + 1][(newSize * 2) + 1];
color = new Color(Color.WHITE);
this.location = location;
float blue = (encoding % 10) / 9.f;
float green = ((encoding / 10) % 10) / 9.f;
float red = ((encoding / 100) % 10) / 9.f;
color.setRedf(red);
color.setGreenf(green);
color.setBluef(blue);
bright = ((encoding / 1000) % 10) / 9.f;
size = (encoding / 10000) % 10;
invert = (encoding / 100000) == 1;
dirty = true;
calculationLock = new ReentrantLock();
}
/**
* Apply shadow map to rendering target. So all calculated intensity values
* are added to the map by this function.
*/
public void apply() {
if (mapSource == null) {
throw new IllegalStateException("The light source is not properly bound to a map yet.");
}
ServerCoordinate loc = location;
for (int dX = -size; dX <= size; dX++) {
for (int dY = -size; dY <= size; dY++) {
double locIntensity = intensity[dX + size][dY + size];
if (locIntensity == 0) {
continue;
}
double factor = locIntensity * bright;
Color tempColor = new Color(color);
tempColor.multiply((float) factor);
if (invert) {
tempColor.multiply(-1.f);
}
// set the light on the map
mapSource.setLight(new ServerCoordinate(loc, dX, dY, 0), tempColor);
}
}
}
/**
* Recalculate the shadow map of the light source in case its needed.
*
* @return true in case anything was done
*/
public boolean calculateShadows() {
if (!dirty) {
return false;
}
dirty = false;
// reset array
resetShadows();
rays.apply(this);
return true;
}
/**
* Get the location of this light source.
*
* @return the location of the light source
*/
@Contract(pure = true)
@Nonnull
public ServerCoordinate getLocation() {
return location;
}
void setLocation(@Nonnull ServerCoordinate newLocation) {
if (!location.equals(newLocation)) {
location = newLocation;
refresh();
}
}
/**
* Get the length of the light rays of this light source.
*
* @return the length of the light rays of this light source
*/
@Contract(pure = true)
public int getSize() {
return size;
}
/**
* Check if this light source is dirty and needs further calculations or not.
*
* @return true in case the light source needs calculations
*/
@Contract(pure = true)
public boolean isDirty() {
return dirty;
}
/**
* Notify the light source about a relevant change of data. Light source
* will become dirty if change was within it's area of influence.
*
* @param changeLoc the location the change occurred on.
*/
public void notifyChange(@Nonnull ServerCoordinate changeLoc) {
if (location.getZ() != changeLoc.getZ()) {
return;
}
if (location.getStepDistance(changeLoc) < size) {
refresh();
}
}
/**
* Refresh the calculated shadow and light data. This forces the light
* source to recalculate all values.
*/
public void refresh() {
dirty = true;
}
/**
* Reset all recalculated values. This should be done before the light
* source object is put into the cache for later usage.
*/
private void resetShadows() {
for (double[] anIntensity : intensity) {
Arrays.fill(anIntensity, 0);
}
}
/**
* Set light intensity in shadow map and return opacity value.
*
* @param dX the X offset of the location that's intensity shall be set to the
* location of the light source
* @param dY the Y offset of the location that's intensity shall be set to the
* location of the light source
* @param newInt the intensity that shall for this location now
* @return the obscurity of the location that's light intensity was just set
*/
public int setIntensity(int dX, int dY, double newInt) {
if (mapSource == null) {
throw new IllegalStateException("The light source is not properly bound to a map yet.");
}
if (Math.abs(dX) > size) {
throw new IllegalArgumentException("The X offset for the light is out of bounds: " + dX);
}
if (Math.abs(dY) > size) {
throw new IllegalArgumentException("The Y offset for the light is out of bounds: " + dY);
}
ServerCoordinate targetCoordinates = new ServerCoordinate(location, dX, dY, 0);
if (((dX == 0) && (dY == 0)) || mapSource.acceptsLight(targetCoordinates, dX, dY)) {
intensity[dX + size][dY + size] = newInt;
}
return mapSource.blocksView(targetCoordinates);
}
/**
* Set a new map source to the light source. This needs to be the map the
* light is on. All data to calculate the shadows is taken from this map,
* also all results of the calculations are send to this map.
*
* @param newMapSource the map that contains the light source
*/
void setMapSource(@Nonnull LightingMap newMapSource) {
if ((mapSource == null) || !Objects.equals(mapSource, newMapSource)) {
mapSource = newMapSource;
dirty = true;
}
}
void dispose() {
disposed = true;
}
@Contract(pure = true)
boolean isDisposed() {
return disposed;
}
@Contract(pure = true)
public int getEncodedValue() {
return encodedValue;
}
@Override
@Contract(value = "null->false", pure = true)
public boolean equals(@Nullable Object obj) {
return (obj instanceof LightSource) && equals((LightSource) obj);
}
@Contract(value = "null->false", pure = true)
public boolean equals(@Nullable LightSource light) {
return (light != null) && (light.getEncodedValue() == getEncodedValue()) && light.location.equals(location);
}
@Override
@Contract(pure = true)
public int hashCode() {
return (int) (((23L + getEncodedValue()) * 31L) + location.hashCode());
}
@Override
@Nonnull
@Contract(pure = true)
public String toString() {
return "LightSource (" + location + ", " + color + ", brightness: " + bright + ", size: " + size +
", dirty: " + dirty + ')';
}
@Nonnull
@Contract(pure = true)
Lock getCalculationLock() {
return calculationLock;
}
public boolean isCalculating() {
return calculating;
}
public void setCalculating(boolean calculating) {
this.calculating = calculating;
}
}