/* Copyright (c) 2012-2014 Jesper Öqvist <jesper@llbit.se> * * This file is part of Chunky. * * Chunky is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Chunky 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. * You should have received a copy of the GNU General Public License * along with Chunky. If not, see <http://www.gnu.org/licenses/>. */ package se.llbit.chunky.renderer.scene; import java.util.Random; import org.apache.commons.math3.util.FastMath; import se.llbit.chunky.renderer.Refreshable; import se.llbit.chunky.resources.Texture; import se.llbit.json.JsonObject; import se.llbit.math.QuickMath; import se.llbit.math.Ray; import se.llbit.math.Vector3; import se.llbit.util.JsonSerializable; /** * Sun model for ray tracing. * * @author Jesper Öqvist <jesper@llbit.se> */ public class Sun implements JsonSerializable { /** * Default sun intensity */ public static final double DEFAULT_INTENSITY = 1.25; /** * Maximum sun intensity */ public static final double MAX_INTENSITY = 50; /** * Minimum sun intensity */ public static final double MIN_INTENSITY = 0.1; private static final double xZenithChroma[][] = {{0.00166, -0.00375, 0.00209, 0}, {-0.02903, 0.06377, -0.03203, 0.00394}, {0.11693, -0.21196, 0.06052, 0.25886},}; private static final double yZenithChroma[][] = {{0.00275, -0.00610, 0.00317, 0}, {-0.04214, 0.08970, -0.04153, 0.00516}, {0.15346, -0.26756, 0.06670, 0.26688},}; private static final double mdx[][] = {{-0.0193, -0.2592}, {-0.0665, 0.0008}, {-0.0004, 0.2125}, {-0.0641, -0.8989}, {-0.0033, 0.0452}}; private static final double mdy[][] = {{-0.0167, -0.2608}, {-0.0950, 0.0092}, {-0.0079, 0.2102}, {-0.0441, -1.6537}, {-0.0109, 0.0529}}; private static final double mdY[][] = {{0.1787, -1.4630}, {-0.3554, 0.4275}, {-0.0227, 5.3251}, {0.1206, -2.5771}, {-0.0670, 0.3703}}; private static double turb = 2.5; private static double turb2 = turb * turb; private static Vector3 A = new Vector3(); private static Vector3 B = new Vector3(); private static Vector3 C = new Vector3(); private static Vector3 D = new Vector3(); private static Vector3 E = new Vector3(); /** * Sun texture */ public static Texture texture = new Texture(); static { A.x = mdx[0][0] * turb + mdx[0][1]; B.x = mdx[1][0] * turb + mdx[1][1]; C.x = mdx[2][0] * turb + mdx[2][1]; D.x = mdx[3][0] * turb + mdx[3][1]; E.x = mdx[4][0] * turb + mdx[4][1]; A.y = mdy[0][0] * turb + mdy[0][1]; B.y = mdy[1][0] * turb + mdy[1][1]; C.y = mdy[2][0] * turb + mdy[2][1]; D.y = mdy[3][0] * turb + mdy[3][1]; E.y = mdy[4][0] * turb + mdy[4][1]; A.z = mdY[0][0] * turb + mdY[0][1]; B.z = mdY[1][0] * turb + mdY[1][1]; C.z = mdY[2][0] * turb + mdY[2][1]; D.z = mdY[3][0] * turb + mdY[3][1]; E.z = mdY[4][0] * turb + mdY[4][1]; } private double zenith_Y; private double zenith_x; private double zenith_y; private double f0_Y; private double f0_x; private double f0_y; private final Refreshable scene; /** * Sun radius */ public static final double RADIUS = .03; public static final double RADIUS_COS = FastMath.cos(RADIUS); public static final double RADIUS_SIN = FastMath.sin(RADIUS); private static final double AMBIENT = .3; private double intensity = DEFAULT_INTENSITY; private double azimuth = Math.PI / 2.5; private double altitude = Math.PI / 3; // Support vectors. private final Vector3 su = new Vector3(); private final Vector3 sv = new Vector3(); /** * Location of the sun in the sky. */ private final Vector3 sw = new Vector3(); protected final Vector3 emittance = new Vector3(1, 1, 1); // final to ensure that we don't do a lot of redundant re-allocation private final Vector3 color = new Vector3(1, 1, 1); /** * Calculate skylight for ray using Preetham day sky model. */ public void calcSkyLight(Ray ray, double horizonOffset) { double cosTheta = ray.d.y; cosTheta += horizonOffset * (1 - cosTheta); if (cosTheta < 0) cosTheta = 0; double cosGamma = ray.d.dot(sw); double gamma = FastMath.acos(cosGamma); double cos2Gamma = cosGamma * cosGamma; double x = zenith_x * perezF(cosTheta, gamma, cos2Gamma, A.x, B.x, C.x, D.x, E.x) * f0_x; double y = zenith_y * perezF(cosTheta, gamma, cos2Gamma, A.y, B.y, C.y, D.y, E.y) * f0_y; double z = zenith_Y * perezF(cosTheta, gamma, cos2Gamma, A.z, B.z, C.z, D.z, E.z) * f0_Y; if (y <= Ray.EPSILON) { ray.color.set(0, 0, 0, 1); } else { double f = (z / y); double x2 = x * f; double y2 = z; double z2 = (1 - x - y) * f; // CIE to RGB M^-1 matrix from http://www.brucelindbloom.com/Eqn_RGB_XYZ_Matrix.html ray.color.set(2.3706743 * x2 - 0.9000405 * y2 - 0.4706338 * z2, -0.513885 * x2 + 1.4253036 * y2 + 0.0885814 * z2, 0.0052982 * x2 - 0.0146949 * y2 + 1.0093968 * z2, 1); ray.color.scale(0.045); } } private double chroma(double turb, double turb2, double sunTheta, double[][] matrix) { double t1 = sunTheta; double t2 = t1 * t1; double t3 = t1 * t2; return turb2 * (matrix[0][0] * t3 + matrix[0][1] * t2 + matrix[0][2] * t1 + matrix[0][3]) + turb * (matrix[1][0] * t3 + matrix[1][1] * t2 + matrix[1][2] * t1 + matrix[1][3]) + (matrix[2][0] * t3 + matrix[2][1] * t2 + matrix[2][2] * t1 + matrix[2][3]); } private static double perezF(double cosTheta, double gamma, double cos2Gamma, double A, double B, double C, double D, double E) { return (1 + A * FastMath.exp(B / cosTheta)) * (1 + C * FastMath.exp(D * gamma) + E * cos2Gamma); } /** * Create new sun model. */ public Sun(Refreshable sceneDescription) { this.scene = sceneDescription; initSun(); } /** * Set equal to other sun model. */ public void set(Sun other) { azimuth = other.azimuth; altitude = other.altitude; color.set(other.color); intensity = other.intensity; initSun(); } private void initSun() { double theta = azimuth; double phi = altitude; double r = QuickMath.abs(FastMath.cos(phi)); sw.set(FastMath.cos(theta) * r, FastMath.sin(phi), FastMath.sin(theta) * r); if (QuickMath.abs(sw.x) > .1) { su.set(0, 1, 0); } else { su.set(1, 0, 0); } sv.cross(sw, su); sv.normalize(); su.cross(sv, sw); emittance.set(color); emittance.scale(FastMath.pow(intensity, Scene.DEFAULT_GAMMA)); updateSkylightValues(); } /** * Angle of the sun around the horizon, measured from north. */ public void setAzimuth(double value) { azimuth = QuickMath.modulo(value, Math.PI * 2); initSun(); scene.refresh(); } /** * Sun altitude from the horizon. */ public void setAltitude(double value) { altitude = QuickMath.clamp(value, 0, Math.PI / 2); initSun(); scene.refresh(); } /** * @return Zenith angle */ public double getAltitude() { return altitude; } /** * @return Azimuth */ public double getAzimuth() { return azimuth; } /** * Check if the ray intersects the sun> * * @return <code>true</code> if the ray intersects the sun model */ public boolean intersect(Ray ray) { if (ray.d.dot(sw) < .5) { return false; } double WIDTH = RADIUS * 4; double WIDTH2 = WIDTH * 2; double a; a = Math.PI / 2 - FastMath.acos(ray.d.dot(su)) + WIDTH; if (a >= 0 && a < WIDTH2) { double b = Math.PI / 2 - FastMath.acos(ray.d.dot(sv)) + WIDTH; if (b >= 0 && b < WIDTH2) { texture.getColor(a / WIDTH2, b / WIDTH2, ray.color); ray.color.x *= emittance.x * 10; ray.color.y *= emittance.y * 10; ray.color.z *= emittance.z * 10; return true; } } return false; } /** * Calculate flat shading for ray. */ public void flatShading(Ray ray) { double shading = ray.n.x * sw.x + ray.n.y * sw.y + ray.n.z * sw.z; shading = QuickMath.max(AMBIENT, shading); ray.color.x *= emittance.x * shading; ray.color.y *= emittance.y * shading; ray.color.z *= emittance.z * shading; } public void setColor(Vector3 newColor) { this.color.set(newColor); initSun(); scene.refresh(); } private void updateSkylightValues() { double sunTheta = Math.PI / 2 - altitude; double cosTheta = FastMath.cos(sunTheta); double cos2Theta = cosTheta * cosTheta; double chi = (4.0 / 9.0 - turb / 120.0) * (Math.PI - 2 * sunTheta); zenith_Y = (4.0453 * turb - 4.9710) * Math.tan(chi) - 0.2155 * turb + 2.4192; zenith_Y = (zenith_Y < 0) ? -zenith_Y : zenith_Y; zenith_x = chroma(turb, turb2, sunTheta, xZenithChroma); zenith_y = chroma(turb, turb2, sunTheta, yZenithChroma); f0_x = 1 / perezF(1, sunTheta, cos2Theta, A.x, B.x, C.x, D.x, E.x); f0_y = 1 / perezF(1, sunTheta, cos2Theta, A.y, B.y, C.y, D.y, E.y); f0_Y = 1 / perezF(1, sunTheta, cos2Theta, A.z, B.z, C.z, D.z, E.z); } /** * Set the sun intensity */ public void setIntensity(double value) { intensity = value; initSun(); scene.refresh(); } /** * @return The sun intensity */ public double getIntensity() { return intensity; } /** * Point ray in random direction within sun solid angle */ public void getRandomSunDirection(Ray reflected, Random random) { double x1 = random.nextDouble(); double x2 = random.nextDouble(); double cos_a = 1 - x1 + x1 * RADIUS_COS; double sin_a = FastMath.sqrt(1 - cos_a * cos_a); double phi = 2 * Math.PI * x2; Vector3 u = new Vector3(su); Vector3 v = new Vector3(sv); Vector3 w = new Vector3(sw); u.scale(FastMath.cos(phi) * sin_a); v.scale(FastMath.sin(phi) * sin_a); w.scale(cos_a); reflected.d.add(u, v); reflected.d.add(w); reflected.d.normalize(); } @Override public JsonObject toJson() { JsonObject sun = new JsonObject(); sun.add("altitude", altitude); sun.add("azimuth", azimuth); sun.add("intensity", intensity); JsonObject colorObj = new JsonObject(); colorObj.add("red", color.x); colorObj.add("green", color.y); colorObj.add("blue", color.z); sun.add("color", colorObj); return sun; } public void importFromJson(JsonObject json) { azimuth = json.get("azimuth").doubleValue(azimuth); altitude = json.get("altitude").doubleValue(altitude); intensity = json.get("intensity").doubleValue(intensity); if (json.get("color").isObject()) { JsonObject colorObj = json.get("color").object(); color.x = colorObj.get("red").doubleValue(1); color.y = colorObj.get("green").doubleValue(1); color.z = colorObj.get("blue").doubleValue(1); } initSun(); } /** * @return sun color */ public Vector3 getColor() { return new Vector3(color); } }