/* * 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 illarion.client.world; import illarion.common.util.PoolThreadFactory; import org.illarion.engine.graphic.Color; import org.illarion.engine.graphic.ImmutableColor; import org.jetbrains.annotations.Contract; import javax.annotation.Nonnull; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; /** * The purpose of this class is to calculate and maintain the current ambient light. * * @author Martin Karing <nitram@illarion.org> */ final class AmbientLight { @Nonnull private static final GradientColorKey[] SUN_RISE_GRADIENT = { new GradientColorKey(Color.BLACK, 0.0), new GradientColorKey(new ImmutableColor(80, 70, 0), 0.1), new GradientColorKey(new ImmutableColor(255, 210, 170), 0.3), new GradientColorKey(Color.WHITE, 1.0) }; @Nonnull private static final Color STARLIGHT_COLOR = new ImmutableColor(0.15f, 0.15f, 0.3f); @Nonnull private final Color ambientLight1; @Nonnull private final Color ambientLight2; @Nonnull private final ScheduledExecutorService calculationExecutor; private boolean ambientLightToggle; private double overcast; AmbientLight() { ambientLight1 = new Color(Color.BLACK); ambientLight2 = new Color(Color.BLACK); ambientLightToggle = false; calculationExecutor = Executors.newSingleThreadScheduledExecutor( new PoolThreadFactory("AmbientLightCalculation", true)); calculationExecutor.scheduleAtFixedRate(this::calculate, 0, 500, TimeUnit.MILLISECONDS); } /** * Get the phase of the sun based on the day of the year. * * @param dayOfYear the day of the year. * @return {@code 1.0} for the longest day, {@code -1.0} for the shortest day. */ private static double getPhaseOfTheSun(double dayOfYear) { int daysInYear = 365; /* The amount of days in one year. */ int longDay = 182; /* The longest day in the year. */ double usedDay = dayOfYear - longDay; if (usedDay < 0) { usedDay += daysInYear; } return Math.cos((usedDay / daysInYear) * Math.PI * 2.0); } /** * Get the time in seconds the sun is shining at the specified day. * * @param phaseOfSun the phase of the sun * @return the time in seconds of sunshine */ private static double getDaylightSpan(double phaseOfSun) { if ((phaseOfSun < -1) || (phaseOfSun > 1)) { throw new IllegalArgumentException("The phase of the sun is out of bounds: " + phaseOfSun); } double timeVariation = 3.85; /* The variation of the normal length day to the shortest and the longest day. */ return (12.0 + (timeVariation * phaseOfSun)) * 60.0 * 60.0; } /** * Get the time in seconds the sun requires to rise or go down. * * @param phaseOfSun the current phase of the sun * @return the time for the raise or the down in seconds */ private static double getSunRiseSetTime(double phaseOfSun) { if ((phaseOfSun < -1) || (phaseOfSun > 1)) { throw new IllegalArgumentException("The phase of the sun is out of bounds: " + phaseOfSun); } double defaultSunRaiseTime = 2.0; /* Mean time for a sun rise/set in hours. */ double sunRaiseVariation = -1.0; /* The variation of the time for the sun rise/set in hours */ return (defaultSunRaiseTime + (sunRaiseVariation * phaseOfSun)) * 60.0 * 60.0; } @Nonnull private static Color getColorFromGradient(double key, @Nonnull GradientColorKey... gradientKeys) { if (gradientKeys.length < 2) { throw new IllegalArgumentException("Supplied gradient is no gradient. Too few values."); } assert gradientKeys[0] != null; if (key <= gradientKeys[0].getKey()) { return gradientKeys[0].getColor(); } if (key >= gradientKeys[gradientKeys.length - 1].getKey()) { return gradientKeys[gradientKeys.length - 1].getColor(); } for (int i = 0; i < (gradientKeys.length - 1); i++) { double startKey = gradientKeys[i].getKey(); double stopKey = gradientKeys[i + 1].getKey(); if ((key >= startKey) && (key < stopKey)) { /* Found the gradient range it is in. */ double processWithinRange = getProgressInRange(startKey, stopKey, key); Color startColor = gradientKeys[i].getColor(); Color stopColor = gradientKeys[i + 1].getColor(); return new ImmutableColor( getInterpolated(startColor.getRedf(), stopColor.getRedf(), processWithinRange), getInterpolated(startColor.getGreenf(), stopColor.getGreenf(), processWithinRange), getInterpolated(startColor.getBluef(), stopColor.getBluef(), processWithinRange), getInterpolated(startColor.getAlphaf(), stopColor.getAlphaf(), processWithinRange)); } } throw new IllegalStateException("Feature at gradient calculation. This poit must not ever be reached."); } @Contract(pure = true) private static double getProgressInRange(double start, double stop, double currentValue) { return (currentValue - start) * (1.0 / (stop - start)); } @Contract(pure = true) private static float getInterpolated(float start, float stop, double process) { return (float) (((stop - start) * process) + start); } public void setOvercast(double newValue) { if ((newValue < 0) || (newValue > 1)) { throw new IllegalArgumentException("Overcast value is out of range."); } overcast = newValue; } private void calculate() { if (!World.getClock().isSet()) { return; } Color usedColor = getFreeColorStorage(); usedColor.setColor(Color.BLACK); double dayInYear = World.getClock().getTotalDayInYear(); double secondOfDay = World.getClock().getTotalHour() * 60.0 * 60.0; double middleOfDay = 12.0 * 60.0 * 60.0; /* The time in a day where the run is highest. */ usedColor.setColor(calculateSunlight(dayInYear, secondOfDay, middleOfDay)); usedColor.setRedf(Math.max(usedColor.getRedf(), STARLIGHT_COLOR.getRedf())); usedColor.setGreenf(Math.max(usedColor.getGreenf(), STARLIGHT_COLOR.getGreenf())); usedColor.setBluef(Math.max(usedColor.getBluef(), STARLIGHT_COLOR.getBluef())); if (overcast > 1.0e-8) { /* Calculate the darkening effect of the overcast. */ /* We need HSB for that. */ float[] hsb = java.awt.Color.RGBtoHSB(usedColor.getRed(), usedColor.getGreen(), usedColor.getBlue(), null); hsb[1] = Math.max(0, hsb[1] - (float) (0.3f * overcast)); /* Clouds make everything gray */ hsb[2] = Math.max(0.1f, hsb[2] - (float) (0.3f * overcast)); /* Clouds make it darker. */ int rgb = java.awt.Color.HSBtoRGB(hsb[0], hsb[1], hsb[2]); usedColor.setRed((0xFF0000 & rgb) >> 16); usedColor.setGreen((0xFF00 & rgb) >> 8); usedColor.setBlue(0xFF & rgb); } /* Activate the new color */ ambientLightToggle = !ambientLightToggle; } /** * Calculate the color the sun is shining with. * * @param dayInYear the day within the year * @param secondOfDay the second within the current day * @param middleOfDay the second of the day that marks the middle * @return the color of the sun */ @Nonnull private static Color calculateSunlight(double dayInYear, double secondOfDay, double middleOfDay) { double phaseOfTheSun = getPhaseOfTheSun(dayInYear); double daylightTimeSpan = getDaylightSpan(phaseOfTheSun); double sunRiseSetSpan = getSunRiseSetTime(phaseOfTheSun); /* Light of the sun. */ if (secondOfDay < middleOfDay) { /* Before noon. So there may be a sunrise at hand. */ double middleOfSunrise = middleOfDay - (daylightTimeSpan / 2.0); double beginOfSunrise = middleOfSunrise - (sunRiseSetSpan / 2.0); double endOfSunrise = middleOfSunrise + (sunRiseSetSpan / 2.0); if ((secondOfDay > beginOfSunrise) && (secondOfDay < endOfSunrise)) { /* We are currently within the time span of a sunrise. */ double riseProgress = getProgressInRange(beginOfSunrise, endOfSunrise, secondOfDay); return getColorFromGradient(riseProgress, SUN_RISE_GRADIENT); } else { return (secondOfDay >= endOfSunrise) ? Color.WHITE : Color.BLACK; } } else { /* Before noon. So there may be a sunset at hand. */ double middleOfSunset = middleOfDay + (daylightTimeSpan / 2.0); double beginOfSunset = middleOfSunset - (sunRiseSetSpan / 2.0); double endOfSunset = middleOfSunset + (sunRiseSetSpan / 2.0); if ((secondOfDay > beginOfSunset) && (secondOfDay < endOfSunset)) { /* We are currently within the time span of a sunset. */ double setProgress = getProgressInRange(beginOfSunset, endOfSunset, secondOfDay); /* Follow the gradient inverse. */ return getColorFromGradient(1.0 - setProgress, SUN_RISE_GRADIENT); } else { return (secondOfDay >= endOfSunset) ? Color.BLACK : Color.WHITE; } } } public void shutdown() { calculationExecutor.shutdown(); while (!calculationExecutor.isTerminated()) { try { calculationExecutor.awaitTermination(1, TimeUnit.SECONDS); } catch (InterruptedException e) { // ignored. } } } @Nonnull @Contract(pure = true) private Color getFreeColorStorage() { return ambientLightToggle ? ambientLight1 : ambientLight2; } @Nonnull @Contract(pure = true) public Color getCurrentAmbientLight() { return ambientLightToggle ? ambientLight2 : ambientLight1; } private static class GradientColorKey { @Nonnull private final ImmutableColor color; private final double key; GradientColorKey(@Nonnull ImmutableColor color, double key) { this.color = color; this.key = key; } @Nonnull public ImmutableColor getColor() { return color; } public double getKey() { return key; } } }