/* This file is part of Mitsuba, a physically based rendering system. Copyright (c) 2007-2010 by Wenzel Jakob and others. Mitsuba is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License Version 3 as published by the Free Software Foundation. Mitsuba 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 this program. If not, see <http://www.gnu.org/licenses/>. */ #include <mitsuba/render/scene.h> #include <mitsuba/core/util.h> #include <mitsuba/core/bitmap.h> #include <mitsuba/core/fstream.h> #define SAMPLE_UNIFORMLY 1 MTS_NAMESPACE_BEGIN /*!\plugin{sky}{Skylight luminaire} * \parameters{ * \parameter{turbidity}{\Float}{ * This parameter determines the amount of scattering particles (or * `haze') in the atmosphere. Smaller values ($\sim 2$) produce a * clear blue sky, larger values ($\sim 8$) lead to an overcast sky, * and a very high values ($\sim 20$) cause a color shift towards * orange and red. \default{3} * } * \parameter{day}{\Integer}{Solar day used to compute the sun's position. * Must be in the range between 1 and 365. \default{180}} * \parameter{time}{\Float}{Fractional time used to compute the sun's * position. A time of 4:15 PM corresponds to 16.25. \default{15.00}} * \parameter{latitude, longitude}{\Float}{ * These two parameters specify the oberver's latitude and longitude * in degrees, which are required to compute the sun's position. * \default{35.6894, 139.6917 --- Tokyo, Japan} * } * \parameter{standardMeridian}{\Integer}{Denotes the * standard meridian of the time zone for finding * the sun's position \default{135 --- Japan standard time} * } * \parameter{sunDirection}{\Vector}{Allows to manually * override the sun direction in world space. When this value * is provided, parameters pertaining to the computation * of the sun direction (\code{day, time, latitude, longitude,} * and \code{standardMeridian}) are unnecessary. \default{none} * } * \parameter{extend}{\Boolean}{ * Extend luminaire below the horizon? \default{\code{false}} * } * \parameter{resolution}{\Integer}{Specifies the resolution of the precomputed * image that is used to represent the sky environment map * \default{256}} * \parameter{scale}{\Float}{ * This parameter can be used to scale the the amount of illumination * emitted by the sky luminaire, for instance to change its units. To * switch from photometric ($\nicefrac{W}{m^2\cdot sr}$) * to arbitrary but convenient units in the $[0,1]$ range, set * this parameter to \code{1e-5}.\default{1}. * } * } * * \renderings{ * \tinyrendering{6AM}{preetham_06} * \tinyrendering{8AM}{preetham_08} * \tinyrendering{10AM}{preetham_10} * \tinyrendering{12PM}{preetham_12} * \tinyrendering{2PM}{preetham_14} * \tinyrendering{4PM}{preetham_16} * \tinyrendering{6PM}{preetham_18} * \tinyrendering{8PM}{preetham_20}\hfill * \vspace{-3mm} * \caption{Time series with the default settings (visualized by * projecting the sky onto a disk)} * } * * This plugin implements the physically-based skylight model proposed by * Preetham et al. \cite{Preetham1999Practical}. It can be used for realistic * daylight renderings of scenes under clear and overcast skies, assuming * that the sky is observed from a position either on or close to the surface * of the earth. * * Numerous parameters allow changing the both the position on Earth, as * well as the time of observation. These are used to compute the sun * direction which, together with \code{turbidity}, constitutes the main * parameter of the model. If desired, the sun direction can also be * specified manually. * * \renderings{ * \tinyrendering{2}{preetham_turb_2} * \tinyrendering{3}{preetham_turb_3} * \tinyrendering{4}{preetham_turb_4} * \tinyrendering{5}{preetham_turb_5} * \tinyrendering{6}{preetham_turb_6} * \tinyrendering{7}{preetham_turb_7} * \tinyrendering{8}{preetham_turb_8} * \tinyrendering{9}{preetham_turb_9} * \vspace{-3mm} * \caption{Sky light for different turbidity values (fixed time \& location)} * } * * \emph{Turbidity}, the other important parameter, specifies the amount of * atmospheric extinction due to larger particles ($t_l$), as opposed to * molecules ($t_m$). Lower values correspond to a clear sky, and higher values * produce illumination resembling that of a hazy, overcast sky. Formally, * the turbidity is defined as the ratio between the combined extinction * cross-section and the cross-section only due to molecules, i.e. * $T=\frac{t_m+t_l}{t_m}$. Values between 1 and 30 are possible, though * the model will be most accurate for values between 2 and 6, to which * it was fit using numerical optimization. * The default coordinate system of the luminaire associates the up * direction with the $+Y$ axis. The east direction is associated with $+X$ * and the north direction is equal to $+Z$. To change this coordinate * system, rotations can be applied using the \code{toWorld} parameter. * * By default, the luminaire will not emit any light below the * horizon, which means that these regions will be black when they * are observed directly. By setting the \code{extend} parameter to * \code{true}, the emitted radiance at the horizon will be extended to * the entire bottom hemisphere. Note that this will significantly * increase the amount of illumination present in the scene. * * For performance reasons, the implementation precomputes an environment * map of the entire sky that is then forwarded to the \pluginref{envmap} * plugin. The resolution of this environment map can affect the quality * of the result. Due to the smoothness of the sky illumination, * \code{resolution} values of around 256 (the default) are usually * more than sufficient. * * Note that while the model encompasses sunrise and sunset configurations, * it does not extend to the night sky, where illumination from stars, galaxies, * and the moon dominate. The model also currently does not handle cloudy skies. * The implementation in Mitsuba is based on code by Preetham et al. It was * ported by Tom Kazimiers. */ class SkyLuminaire : public Luminaire { public: /** * Creates a new Sky luminaire. The defaults values origate * from the sample code of Preetham et al. and create an over * head sun on a clear day. */ SkyLuminaire(const Properties &props) : Luminaire(props) { /* Transformation from the luminaire's local coordinates to * world coordiantes */ m_luminaireToWorld = props.getTransform("toWorld", Transform()); m_worldToLuminaire = m_luminaireToWorld.inverse(); m_scale = props.getFloat("scale", Float(1.0)); m_turbidity = props.getFloat("turbidity", Float(3.0)); if (m_turbidity < 1 || m_turbidity > 30) Log(EError, "The turbidity parameter must be in the range [1,30]!"); m_extend = props.getBoolean("extend", false); m_resolution = props.getInteger("resolution", 256); /* configure position of sun */ if (props.hasProperty("sunDirection")) { if (props.hasProperty("latitude") || props.hasProperty("longitude") || props.hasProperty("standardMeridian") || props.hasProperty("day") || props.hasProperty("time")) Log(EError, "Both the 'sunDirection' parameter and time/location " "information were provided -- only one of them can be specified at a time!"); configureSunPosition( props.getVector("sunDirection")); } else { Float lat = props.getFloat("latitude", 35.6894f); Float lon = props.getFloat("longitude", 139.6917f); int stdMrd = props.getInteger("standardMeridian", 135); int day = props.getInteger("day", 180); if (day < 1 || day > 365) Log(EError, "The day parameter must be in the range [1, 365]!"); Float time = props.getFloat("time", 15.00f); if (time < 0 || time > 24) Log(EError, "The time parameter must be in the range [0, 24]!"); configureSunPosition(lat, lon, stdMrd, day, time); } configure(); } SkyLuminaire(Stream *stream, InstanceManager *manager) : Luminaire(stream, manager) { m_scale = stream->readFloat(); m_turbidity = stream->readFloat(); m_thetaS = stream->readFloat(); m_phiS = stream->readFloat(); m_extend = stream->readBool(); m_resolution = stream->readInt(); configure(); } void serialize(Stream *stream, InstanceManager *manager) const { Luminaire::serialize(stream, manager); stream->writeFloat(m_scale); stream->writeFloat(m_turbidity); stream->writeFloat(m_thetaS); stream->writeFloat(m_phiS); stream->writeBool(m_extend); stream->writeInt(m_resolution); } /** * Precalculates some inernal varibles. It needs a valid sun position, * hence the configureSunPos... method must have been called previously. */ void configure() { Float theta2 = m_thetaS * m_thetaS; Float theta3 = theta2 * m_thetaS; Float turb2 = m_turbidity * m_turbidity; /* calculate zenith chromaticity */ m_zenithX = (+0.00165f*theta3 - 0.00374f*theta2 + 0.00208f*m_thetaS + 0.0f) * turb2 + (-0.02902f*theta3 + 0.06377f*theta2 - 0.03202f*m_thetaS + 0.00394f) * m_turbidity + (+0.11693f*theta3 - 0.21196f*theta2 + 0.06052f*m_thetaS + 0.25885f); m_zenithY = (+0.00275f*theta3 - 0.00610f*theta2 + 0.00316f*m_thetaS + 0.0f) * turb2 + (-0.04214f*theta3 + 0.08970f*theta2 - 0.04153f*m_thetaS + 0.00515f) * m_turbidity + (+0.15346f*theta3 - 0.26756f*theta2 + 0.06669f*m_thetaS + 0.26688f); /* calculate zenith luminance */ Float chi = (4.0f/9.0f - m_turbidity / 120.0f) * (M_PI - 2 * m_thetaS); m_zenithL = (4.0453f * m_turbidity - 4.9710f) * std::tan(chi) - 0.2155f * m_turbidity + 2.4192f; cout << toString() << endl; /* Evaluate quadratic polynomials to find the Perez sky * model coefficients for the x, y and luminance components */ m_perezL[0] = 0.17872f * m_turbidity - 1.46303f; m_perezL[1] = -0.35540f * m_turbidity + 0.42749f; m_perezL[2] = -0.02266f * m_turbidity + 5.32505f; m_perezL[3] = 0.12064f * m_turbidity - 2.57705f; m_perezL[4] = -0.06696f * m_turbidity + 0.37027f; m_perezX[0] = -0.01925f * m_turbidity - 0.25922f; m_perezX[1] = -0.06651f * m_turbidity + 0.00081f; m_perezX[2] = -0.00041f * m_turbidity + 0.21247f; m_perezX[3] = -0.06409f * m_turbidity - 0.89887f; m_perezX[4] = -0.00325f * m_turbidity + 0.04517f; m_perezY[0] = -0.01669f * m_turbidity - 0.26078f; m_perezY[1] = -0.09495f * m_turbidity + 0.00921f; m_perezY[2] = -0.00792f * m_turbidity + 0.21023f; m_perezY[3] = -0.04405f * m_turbidity - 1.65369f; m_perezY[4] = -0.01092f * m_turbidity + 0.05291f; int thetaBins = m_resolution, phiBins = m_resolution*2; ref<Bitmap> bitmap = new Bitmap(phiBins, thetaBins, 128); bitmap->clear(); Point2 factor(M_PI / thetaBins, (2*M_PI) / phiBins); for (int i=0; i<thetaBins; ++i) { Float theta = (i+.5f)*factor.x; for (int j=0; j<phiBins; ++j) { Float phi = (j+.5f)*factor.y; Spectrum s = getSkySpectralRadiance(theta, phi) * m_scale; Float r, g, b; s.toLinearRGB(r, g, b); bitmap->getFloatData()[(j+i*phiBins)*4 + 0] = r; bitmap->getFloatData()[(j+i*phiBins)*4 + 1] = g; bitmap->getFloatData()[(j+i*phiBins)*4 + 2] = b; bitmap->getFloatData()[(j+i*phiBins)*4 + 3] = 1; } } } Vector toSphere(Float theta, Float phi) const { /* Spherical-to-cartesian coordinate mapping with theta=0 => Y=1 */ Float cosTheta = std::cos(theta), sinTheta = std::sin(theta), cosPhi = std::cos(phi), sinPhi = std::sin(phi); return m_luminaireToWorld(Vector( sinTheta * sinPhi, cosTheta, -sinTheta*cosPhi)); } Point2 fromSphere(const Vector &d) const { Float theta = std::acos(std::max((Float) -1.0f, std::min((Float) 1.0f, d.y))); Float phi = std::atan2(d.x,-d.z); if (phi < 0) phi += 2*M_PI; return Point2(theta, phi); } /** * Configures the position of the sun. This calculation is based on * your position on the world and time of day. * From IES Lighting Handbook pg 361. */ void configureSunPosition(Float lat, Float lon, int stdMrd, int day, Float time) { const Float solarTime = time + (0.170f * std::sin(4.0f * M_PI * (day - 80.0f) / 373.0f) - 0.129f * std::sin(2.0f * M_PI * (day - 8.0f) / 355.0f)) + (stdMrd - lon) / 15.0f; const Float solarDeclination = (0.4093f * std::sin(2 * M_PI * (day - 81.0f) / 368.0f)); lat = degToRad(lat); const Float solarAltitude = std::asin(std::sin(lat) * std::sin(solarDeclination) - std::cos(lat) * std::cos(solarDeclination) * std::cos(M_PI * solarTime / 12.0f)); const Float opp = -std::cos(solarDeclination) * std::sin(M_PI * solarTime / 12.0f); const Float adj = -(std::cos(lat) * std::sin(solarDeclination) + std::sin(lat) * std::cos(solarDeclination) * std::cos(M_PI * solarTime / 12.0f)); const Float solarAzimuth = std::atan2(opp, adj); m_phiS = -solarAzimuth; m_thetaS = M_PI / 2.0f - solarAltitude; } void configureSunPosition(const Vector& sunDir) { Point2 sunPos = fromSphere(normalize(m_luminaireToWorld(sunDir))); m_thetaS = sunPos.x; m_phiS = sunPos.y; } void preprocess(const Scene *scene) { /* Get the scene's bounding sphere and slightly enlarge it */ m_bsphere = scene->getBSphere(); m_bsphere.radius *= 1.01f; } Spectrum getPower() const { /* TODO */ return m_average * (M_PI * 4 * M_PI * m_bsphere.radius * m_bsphere.radius); } inline Spectrum Le(const Vector &direction) const { /* Compute sky light radiance for direction */ Vector d = normalize(m_worldToLuminaire(direction)); const Point2 sphCoords = fromSphere(d); return getSkySpectralRadiance(sphCoords.x, sphCoords.y) * m_scale; } inline Spectrum Le(const Ray &ray) const { return Le(normalize(ray.d)); } Spectrum Le(const LuminaireSamplingRecord &lRec) const { return Le(-lRec.d); } inline void sample(const Point &p, LuminaireSamplingRecord &lRec, const Point2 &sample) const { lRec.d = sampleDirection(sample, lRec.pdf, lRec.value); lRec.sRec.p = p - lRec.d * (2 * m_bsphere.radius); } void sample(const Intersection &its, LuminaireSamplingRecord &lRec, const Point2 &sample) const { SkyLuminaire::sample(its.p, lRec, sample); } inline Float pdf(const Point &p, const LuminaireSamplingRecord &lRec, bool delta) const { #if defined(SAMPLE_UNIFORMLY) return 1.0f / (4 * M_PI); #endif } Float pdf(const Intersection &its, const LuminaireSamplingRecord &lRec, bool delta) const { return SkyLuminaire::pdf(its.p, lRec, delta); } /** * This is the tricky bit - we want to sample a ray that * has uniform density over the set of all rays passing * through the scene. * For more detail, see "Using low-discrepancy sequences and * the Crofton formula to compute surface areas of geometric models" * by Li, X. and Wang, W. and Martin, R.R. and Bowyer, A. * (Computer-Aided Design vol 35, #9, pp. 771--782) */ void sampleEmission(EmissionRecord &eRec, const Point2 &sample1, const Point2 &sample2) const { Assert(eRec.type == EmissionRecord::ENormal); /* Chord model - generate the ray passing through two uniformly distributed points on a sphere containing the scene */ Vector d = squareToSphere(sample1); eRec.sRec.p = m_bsphere.center + d * m_bsphere.radius; eRec.sRec.n = Normal(-d); Point p2 = m_bsphere.center + squareToSphere(sample2) * m_bsphere.radius; eRec.d = p2 - eRec.sRec.p; Float length = eRec.d.length(); if (length == 0) { eRec.value = Spectrum(0.0f); eRec.pdfArea = eRec.pdfDir = 1.0f; return; } eRec.d /= length; eRec.pdfArea = 1.0f / (4 * M_PI * m_bsphere.radius * m_bsphere.radius); eRec.pdfDir = INV_PI * dot(eRec.sRec.n, eRec.d); eRec.value = Le(-eRec.d); } void sampleEmissionArea(EmissionRecord &eRec, const Point2 &sample) const { if (eRec.type == EmissionRecord::ENormal) { Vector d = squareToSphere(sample); eRec.sRec.p = m_bsphere.center + d * m_bsphere.radius; eRec.sRec.n = Normal(-d); eRec.pdfArea = 1.0f / (4 * M_PI * m_bsphere.radius * m_bsphere.radius); eRec.value = Spectrum(M_PI); } else { /* Preview mode, which is more suitable for VPL-based rendering: approximate the infinitely far-away source with set of diffuse point sources */ const Float radius = m_bsphere.radius * 1.5f; Vector d = squareToSphere(sample); eRec.sRec.p = m_bsphere.center + d * radius; eRec.sRec.n = Normal(-d); eRec.pdfArea = 1.0f / (4 * M_PI * radius * radius); eRec.value = Le(d) * M_PI; } } Spectrum sampleEmissionDirection(EmissionRecord &eRec, const Point2 &sample) const { Float radius = m_bsphere.radius; if (eRec.type == EmissionRecord::EPreview) radius *= 1.5f; Point p2 = m_bsphere.center + squareToSphere(sample) * radius; eRec.d = p2 - eRec.sRec.p; Float length = eRec.d.length(); if (length == 0.0f) { eRec.pdfDir = 1.0f; return Spectrum(0.0f); } eRec.d /= length; eRec.pdfDir = INV_PI * dot(eRec.sRec.n, eRec.d); if (eRec.type == EmissionRecord::ENormal) return Le(-eRec.d) * INV_PI; else return Spectrum(INV_PI); } Spectrum fDirection(const EmissionRecord &eRec) const { if (eRec.type == EmissionRecord::ENormal) return Le(-eRec.d) * INV_PI; else return Spectrum(INV_PI); } Spectrum fArea(const EmissionRecord &eRec) const { Assert(eRec.type == EmissionRecord::ENormal); return Spectrum(M_PI); } Spectrum f(const EmissionRecord &eRec) const { if (eRec.type == EmissionRecord::ENormal) return Le(-eRec.d) * INV_PI; else return Spectrum(INV_PI); } void pdfEmission(EmissionRecord &eRec, bool delta) const { } std::string toString() const { std::ostringstream oss; oss << "SkyLuminaire[" << endl << " turbidity = " << m_turbidity << "," << endl << " sunPos = [theta: " << m_thetaS << ", phi: "<< m_phiS << "]," << endl << " zenithL = " << m_zenithL << "," << endl << " scale = " << m_scale << endl << "]"; return oss.str(); } bool isBackgroundLuminaire() const { return true; } Vector sampleDirection(Point2 sample, Float &pdf, Spectrum &value) const { #if defined(SAMPLE_UNIFORMLY) pdf = 1.0f / (4*M_PI); Vector d = squareToSphere(sample); value = Le(-d); return d; #endif } private: /** * Calculates the angle between two spherical cooridnates. All * angles in radians, theta angles measured from up/zenith direction. */ inline Float getAngleBetween(const Float thetav, const Float phiv, const Float theta, const Float phi) const { const Float cospsi = std::sin(thetav) * std::sin(theta) * std::cos(phi - phiv) + std::cos(thetav) * std::cos(theta); if (cospsi > 1.0f) return 0.0f; if (cospsi < -1.0f) return M_PI; return std::acos(cospsi); } /** * Calculates the distribution of the sky radiance with two * Perez functions: * * Perez(Theta, Gamma) * d = --------------------- * Perez(0, ThetaSun) * * From IES Lighting Handbook pg 361 */ inline Float getDistribution(const Float *lam, const Float theta, const Float gamma) const { const Float cosGamma = std::cos(gamma); const Float num = ((1 + lam[0] * std::exp(lam[1] / std::cos(theta))) * (1 + lam[2] * std::exp(lam[3] * gamma) + lam[4] * cosGamma * cosGamma)); const Float cosTheta = std::cos(m_thetaS); const Float den = ( (1 + lam[0] * std::exp(lam[1] /* / cos 0 */)) * (1 + lam[2] * std::exp(lam[3] * m_thetaS) + lam[4] * cosTheta * cosTheta)); return num / den; } /** * Calculates the spectral radiance of the sky in the specified directiono. */ Spectrum getSkySpectralRadiance(Float theta, Float phi) const { if (!m_extend && std::cos(theta) <= 0) return Spectrum(0.0f); /* Clip directions that are extremely close to grazing (for numerical * stability) or entirely below the horizon. This effectively extends * the horizon luminance value to the bottom hemisphere */ theta = std::min(theta, (M_PI * 0.5f) - 0.001f); /* get angle between sun (zenith is 0, 0) and point (theta, phi) */ const Float gamma = getAngleBetween(theta, phi, m_thetaS, m_phiS); /* Compute xyY values by calculating the distribution for the point * point of interest and multiplying it with the the components * zenith value. */ const Float x = m_zenithX * getDistribution(m_perezX, theta, gamma); const Float y = m_zenithY * getDistribution(m_perezY, theta, gamma); const Float Y = m_zenithL * getDistribution(m_perezL, theta, gamma); /* Convert xyY to XYZ */ const Float yFrac = Y / y; const Float X = yFrac * x; /* It seems the following is necassary to stay always above zero */ const Float z = std::max((Float) 0.0f, 1.0f - x - y); const Float Z = yFrac * z; /* Create spectrum from XYZ values */ Spectrum dstSpect; dstSpect.fromXYZ(X, Y, Z, Spectrum::EIlluminant); /* The produced spectrum might contain out-of-gamut colors. * The common solution is to clamp resulting values to zero. */ dstSpect.clampNegative(); return dstSpect; } MTS_DECLARE_CLASS() protected: Spectrum m_average; BSphere m_bsphere; int m_resolution; Float m_scale; /* The turbidity of the sky ranges normally from 1 to 30. For clear skies values in range [2,6] are useful. */ Float m_turbidity; /* Position of the sun in spherical coordinates */ Float m_thetaS, m_phiS; /* Radiance at the zenith, in xyY */ Float m_zenithL, m_zenithX, m_zenithY; /* The distribution coefficints are called A, B, C, D and E by Preetham. They exist for x, y and Y (here called L). The following attributes save the precalculated version of each */ Float m_perezL[5], m_perezX[5], m_perezY[5]; /* Extend to the bottom hemisphere? */ bool m_extend; }; MTS_IMPLEMENT_CLASS_S(SkyLuminaire, false, Luminaire) MTS_EXPORT_PLUGIN(SkyLuminaire, "Sky luminaire"); MTS_NAMESPACE_END