mitsuba/src/luminaires/sky.cpp

612 lines
22 KiB
C++

/*
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