/* This file is part of Mitsuba, a physically based rendering system. Copyright (c) 2007-2014 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 . */ #include #include #include #include #include #include #include "../medium/materials.h" MTS_NAMESPACE_BEGIN /*!\plugin{hk}{Hanrahan-Krueger BSDF} * \icon{bsdf_hk} * * \parameters{ * \parameter{material}{\String}{Name of a material preset, see * \tblref{medium-coefficients}. \default{\texttt{skin1}}} * \parameter{sigmaS}{\Spectrum\Or\Texture}{Specifies the scattering coefficient * of the internal layer. \default{based on \code{material}}} * \parameter{sigmaA}{\Spectrum\Or\Texture}{Specifies the absorption coefficient * of the internal layer. \default{based on \code{material}}} * \parameter{sigmaT \& albedo}{\Spectrum\Or\Texture}{ * Optional: Alternatively, the scattering and absorption coefficients may also be * specified using the extinction coefficient \code{sigmaT} and the * single-scattering albedo. Note that only one of the parameter passing * conventions can be used at a time (i.e. use either \code{sigmaS\&sigmaA} * \emph{or} \code{sigmaT\&albedo})} * \parameter{thickness}{\Float}{Denotes the thickness of the layer. * (should be specified in inverse units of \code{sigmaA} and \code{sigmaS})\default{1}} * \parameter{\Unnamed}{\Phase}{A nested phase function instance that represents * the type of scattering interactions occurring within the layer} * } * * \renderings{ * \rendering{An index-matched scattering layer with parameters $\sigma_s=2$, $\sigma_a=0.1$, thickness$=0.1$}{bsdf_hk_1} * \rendering{Example of the HK model with a dielectric coating (and the \code{ketchup} material preset, see \lstref{hk-coated})}{bsdf_hk_2} * \caption{ * \label{fig:hk-example} * Renderings using the uncoated and coated form of the Hanrahan-Krueger model. * } * \vspace{3mm} * } * * This plugin provides an implementation of the Hanrahan-Krueger BSDF * \cite{Hanrahan1993Reflection} for simulating single scattering in thin * index-matched layers filled with a random scattering medium. * In addition, the implementation also accounts for attenuated * light that passes through the medium without undergoing any scattering events. * * This BSDF requires a phase function to model scattering interactions within the * random medium. When no phase function is explicitly specified, it uses an * isotropic one ($g=0$) by default. A sample usage for instantiating the * plugin is given on the next page:\newpage * \begin{xml} * * * * * * * * * * \end{xml} * * When used in conjuction with the \pluginref{coating} plugin, it is possible * to model refraction and reflection at the layer boundaries when the indices * of refraction are mismatched. The combination of these two plugins then * reproduces the full model as it was originally proposed by Hanrahan and * Krueger \cite{Hanrahan1993Reflection}. * * Note that this model does not account for light that undergoes multiple * scattering events within the layer. This leads to energy loss, * particularly at grazing angles, which can be seen in the left-hand image of * \figref{hk-example}. * * \begin{xml}[caption=A thin dielectric layer with measured ketchup scattering parameters, label=lst:hk-coated] * * * * * * * * * * \end{xml} * * Note that when \texttt{sigmaS} = \texttt{sigmaA}$\ = 0$, or when \texttt{thickness=0}, * any geometry associated with this BSDF becomes invisible, as light will pass through * unchanged. * * The implementation in Mitsuba is based on code by Tom Kazimiers and Marios Papas. * Marios Papas has kindly verified the implementation of the coated and uncoated variants * against both a path tracer and a separate reference implementation. */ class HanrahanKrueger : public BSDF { public: HanrahanKrueger(const Properties &props) : BSDF(props) { Spectrum sigmaS, sigmaA, g; lookupMaterial(props, sigmaS, sigmaA, g, NULL); sigmaS *= Spectrum(1.0f) - g; /* Scattering coefficient of the layer */ m_sigmaS = new ConstantSpectrumTexture( props.getSpectrum("sigmaS", sigmaS)); /* Absorption coefficient of the layer */ m_sigmaA = new ConstantSpectrumTexture( props.getSpectrum("sigmaA", sigmaA)); /* Slab thickness in inverse units of sigmaS and sigmaA */ m_thickness = props.getFloat("thickness", 1); if (props.hasProperty("sigmaT")) m_sigmaT = new ConstantSpectrumTexture( props.getSpectrum("sigmaT")); if (props.hasProperty("albedo")) m_albedo = new ConstantSpectrumTexture( props.getSpectrum("albedo")); } HanrahanKrueger(Stream *stream, InstanceManager *manager) : BSDF(stream, manager) { m_phase = static_cast(manager->getInstance(stream)); m_sigmaS = static_cast(manager->getInstance(stream)); m_sigmaA = static_cast(manager->getInstance(stream)); m_thickness = stream->readFloat(); configure(); } void configure() { if (m_phase == NULL) m_phase = static_cast (PluginManager::getInstance()-> createObject(MTS_CLASS(PhaseFunction), Properties("isotropic"))); if (m_sigmaT != NULL || m_albedo != NULL) { /* Support for the alternative scattering/absorption * coefficient parameter passing convention */ if (m_sigmaT == NULL || m_albedo == NULL) SLog(EError, "Please provide *both* sigmaT & albedo!"); m_sigmaS = new SpectrumProductTexture(m_sigmaT, m_albedo); m_sigmaA = new SpectrumSubtractionTexture(m_sigmaT, m_sigmaS); m_sigmaT = NULL; m_albedo = NULL; } int extraFlags = m_sigmaS->isConstant() && m_sigmaA->isConstant() ? 0 : ESpatiallyVarying; m_components.clear(); m_components.push_back(EGlossyReflection | EFrontSide | EBackSide | EUsesSampler | extraFlags); if (m_thickness != std::numeric_limits::infinity()) { m_components.push_back(EGlossyTransmission | EFrontSide | EBackSide | EUsesSampler | extraFlags); m_components.push_back(EDeltaTransmission | EFrontSide | EBackSide | EUsesSampler | extraFlags); } m_usesRayDifferentials = m_sigmaS->usesRayDifferentials() || m_sigmaA->usesRayDifferentials(); BSDF::configure(); } Spectrum getDiffuseReflectance(const Intersection &its) const { Spectrum sigmaA = m_sigmaA->eval(its), sigmaS = m_sigmaS->eval(its), sigmaT = sigmaA + sigmaS, albedo; for (int i = 0; i < SPECTRUM_SAMPLES; i++) albedo[i] = sigmaT[i] > 0 ? (sigmaS[i]/sigmaT[i]) : (Float) 0; return albedo; /* Very approximate .. */ } Spectrum eval(const BSDFSamplingRecord &bRec, EMeasure measure) const { Spectrum sigmaA = m_sigmaA->eval(bRec.its), sigmaS = m_sigmaS->eval(bRec.its), sigmaT = sigmaA + sigmaS, tauD = sigmaT * m_thickness, result(0.0f); if (measure == EDiscrete) { /* Figure out if the specular transmission is specifically requested */ bool hasSpecularTransmission = (bRec.typeMask & EDeltaTransmission) && (bRec.component == -1 || bRec.component == 2); /* Return the attenuated light if requested */ if (hasSpecularTransmission && std::abs(1+dot(bRec.wi, bRec.wo)) < DeltaEpsilon) result = (-tauD/std::abs(Frame::cosTheta(bRec.wi))).exp(); } else if (measure == ESolidAngle) { /* Sample single scattering events */ bool hasGlossyReflection = (bRec.typeMask & EGlossyReflection) && (bRec.component == -1 || bRec.component == 0); bool hasGlossyTransmission = (bRec.typeMask & EGlossyTransmission) && (bRec.component == -1 || bRec.component == 1); Spectrum albedo; for (int i = 0; i < SPECTRUM_SAMPLES; i++) albedo[i] = sigmaT[i] > 0 ? (sigmaS[i]/sigmaT[i]) : (Float) 0; const Float cosThetaI = Frame::cosTheta(bRec.wi), cosThetaO = Frame::cosTheta(bRec.wo), dp = cosThetaI*cosThetaO; bool reflection = dp > 0, transmission = dp < 0; /* ==================================================================== */ /* Reflection component */ /* ==================================================================== */ if (hasGlossyReflection && reflection) { MediumSamplingRecord dummy; PhaseFunctionSamplingRecord pRec(dummy,bRec.wi,bRec.wo); const Float phaseVal = m_phase->eval(pRec); result = albedo * (phaseVal*cosThetaI/(cosThetaI+cosThetaO)) * (Spectrum(1.0f)-((-1.0f/std::abs(cosThetaI)-1.0f/std::abs(cosThetaO)) * tauD).exp()); } /* ==================================================================== */ /* Transmission component */ /* ==================================================================== */ if (hasGlossyTransmission && transmission && m_thickness < std::numeric_limits::infinity()) { MediumSamplingRecord dummy; PhaseFunctionSamplingRecord pRec(dummy,bRec.wi,bRec.wo); const Float phaseVal = m_phase->eval(pRec); /* Hanrahan etal 93 Single Scattering transmission term */ if (std::abs(cosThetaI + cosThetaO) < Epsilon) { /* avoid division by zero */ result += albedo * phaseVal*tauD/std::abs(cosThetaO) * ((-tauD/std::abs(cosThetaO)).exp()); } else { /* Guaranteed to be positive even if |cosThetaO| > |cosThetaI| */ result += albedo * phaseVal*std::abs(cosThetaI)/(std::abs(cosThetaI)-std::abs(cosThetaO)) * ((-tauD/std::abs(cosThetaI)).exp() - (-tauD/std::abs(cosThetaO)).exp()); } } return result * std::abs(cosThetaO); } return result; } Float pdf(const BSDFSamplingRecord &bRec, EMeasure measure) const { bool hasSingleScattering = (bRec.typeMask & EGlossy) && (bRec.component == -1 || bRec.component == 0 || bRec.component == 1); bool hasSpecularTransmission = (bRec.typeMask & EDeltaTransmission) && (bRec.component == -1 || bRec.component == 2); const Spectrum sigmaA = m_sigmaA->eval(bRec.its), sigmaS = m_sigmaS->eval(bRec.its), sigmaT = sigmaA + sigmaS, tauD = sigmaT * m_thickness; Float probSpecularTransmission = (-tauD/std::abs(Frame::cosTheta(bRec.wi))).exp().average(); if (measure == EDiscrete) { bool hasSpecularTransmission = (bRec.typeMask & EDeltaTransmission) && (bRec.component == -1 || bRec.component == 2); /* Return the attenuated light if requested */ if (hasSpecularTransmission && std::abs(1+dot(bRec.wi, bRec.wo)) < DeltaEpsilon) return hasSingleScattering ? probSpecularTransmission : 1.0f; } else if (hasSingleScattering && measure == ESolidAngle) { bool hasGlossyReflection = (bRec.typeMask & EGlossyReflection) && (bRec.component == -1 || bRec.component == 0); bool hasGlossyTransmission = (bRec.typeMask & EGlossyTransmission) && (bRec.component == -1 || bRec.component == 1); bool reflection = Frame::cosTheta(bRec.wi) * Frame::cosTheta(bRec.wo) >= 0; if ((!hasGlossyReflection && reflection) || (!hasGlossyTransmission && !reflection)) return 0.0f; /* Sampled according to the phase function lobe(s) */ MediumSamplingRecord dummy; PhaseFunctionSamplingRecord pRec(dummy, bRec.wi, bRec.wo); Float pdf = m_phase->pdf(pRec); if (hasSpecularTransmission) pdf *= 1-probSpecularTransmission; return pdf; } return 0.0f; } inline Spectrum sample(BSDFSamplingRecord &bRec, Float &_pdf, const Point2 &_sample) const { AssertEx(bRec.sampler != NULL, "The BSDFSamplingRecord needs to have a sampler!"); bool hasSpecularTransmission = (bRec.typeMask & EDeltaTransmission) && (bRec.component == -1 || bRec.component == 2); bool hasSingleScattering = (bRec.typeMask & EGlossy) && (bRec.component == -1 || bRec.component == 0 || bRec.component == 1); const Spectrum sigmaA = m_sigmaA->eval(bRec.its), sigmaS = m_sigmaS->eval(bRec.its), sigmaT = sigmaA + sigmaS, tauD = sigmaT * m_thickness; /* Probability for a specular transmission is approximated by the average (per wavelength) * probability of a photon exiting without a scattering event or an absorption event */ Float probSpecularTransmission = (-tauD/std::abs(Frame::cosTheta(bRec.wi))).exp().average(); bool choseSpecularTransmission = hasSpecularTransmission; Point2 sample(_sample); if (hasSpecularTransmission && hasSingleScattering) { if (sample.x > probSpecularTransmission) { sample.x = (sample.x - probSpecularTransmission) / (1 - probSpecularTransmission); choseSpecularTransmission = false; } } bRec.eta = 1.0f; if (choseSpecularTransmission) { /* The specular transmission component was sampled */ bRec.sampledComponent = 2; bRec.sampledType = EDeltaTransmission; bRec.wo = -bRec.wi; _pdf = hasSingleScattering ? probSpecularTransmission : 1.0f; return eval(bRec, EDiscrete) / _pdf; } else { /* The glossy transmission/scattering component should be sampled */ bool hasGlossyReflection = (bRec.typeMask & EGlossyReflection) && (bRec.component == -1 || bRec.component == 0); bool hasGlossyTransmission = (bRec.typeMask & EGlossyTransmission) && (bRec.component == -1 || bRec.component == 1); /* Sample According to the phase function lobes */ PhaseFunctionSamplingRecord pRec(MediumSamplingRecord(), bRec.wi, bRec.wo); m_phase->sample(pRec, _pdf, bRec.sampler); /* Store the sampled direction */ bRec.wo = pRec.wo; bool reflection = Frame::cosTheta(bRec.wi) * Frame::cosTheta(bRec.wo) >= 0; if ((!hasGlossyReflection && reflection) || (!hasGlossyTransmission && !reflection)) return Spectrum(0.0f); /* Notify that the scattering component was sampled */ bRec.sampledComponent = reflection ? 0 : 1; bRec.sampledType = EGlossy; _pdf *= (hasSpecularTransmission ? (1 - probSpecularTransmission) : 1.0f); /* Guard against numerical imprecisions */ if (_pdf == 0) return Spectrum(0.0f); else return eval(bRec, ESolidAngle) / _pdf; } } Spectrum sample(BSDFSamplingRecord &bRec, const Point2 &sample) const { Float pdf; return HanrahanKrueger::sample(bRec, pdf, sample); } void serialize(Stream *stream, InstanceManager *manager) const { BSDF::serialize(stream, manager); manager->serialize(stream, m_phase.get()); manager->serialize(stream, m_sigmaS.get()); manager->serialize(stream, m_sigmaA.get()); stream->writeFloat(m_thickness); } void addChild(const std::string &name, ConfigurableObject *child) { const Class *cClass = child->getClass(); if (cClass->derivesFrom(MTS_CLASS(PhaseFunction))) { Assert(m_phase == NULL); m_phase = static_cast(child); } else if (cClass->derivesFrom(MTS_CLASS(Texture))) { if (name == "sigmaS") m_sigmaS = static_cast(child); else if (name == "sigmaA") m_sigmaA = static_cast(child); else if (name == "sigmaT") m_sigmaT = static_cast(child); else if (name == "albedo") m_albedo = static_cast(child); else BSDF::addChild(name, child); } else { BSDF::addChild(name, child); } } Float getRoughness(const Intersection &its, int component) const { /* For lack of a better value, treat this material as diffuse in Manifold Exploration */ return std::numeric_limits::infinity(); } std::string toString() const { std::ostringstream oss; oss << "HanrahanKrueger[" << endl << " id = \"" << getID() << "\"," << endl << " sigmaS = " << indent(m_sigmaS->toString()) << "," << endl << " sigmaA = " << indent(m_sigmaA->toString()) << "," << endl << " phase = " << indent(m_phase->toString()) << "," << endl << " thickness = " << m_thickness << endl << "]"; return oss.str(); } Shader *createShader(Renderer *renderer) const; MTS_DECLARE_CLASS() private: ref m_phase; ref m_sigmaS; ref m_sigmaA; Float m_thickness; /* Temporary fields */ ref m_sigmaT; ref m_albedo; }; // ================ Hardware shader implementation ================ /** * This is a relatively approximate GLSL shader for the HK model. * It assumes that the layer is infinitely thick (i.e. there is no * transmission) and that the phase function is isotropic */ class HanrahanKruegerShader : public Shader { public: HanrahanKruegerShader(Renderer *renderer, const Texture *sigmaS, const Texture *sigmaA) : Shader(renderer, EBSDFShader), m_sigmaS(sigmaS), m_sigmaA(sigmaA) { m_sigmaSShader = renderer->registerShaderForResource(m_sigmaS.get()); m_sigmaAShader = renderer->registerShaderForResource(m_sigmaA.get()); } bool isComplete() const { return m_sigmaSShader.get() != NULL && m_sigmaAShader.get() != NULL; } void cleanup(Renderer *renderer) { renderer->unregisterShaderForResource(m_sigmaS.get()); renderer->unregisterShaderForResource(m_sigmaA.get()); } void putDependencies(std::vector &deps) { deps.push_back(m_sigmaSShader.get()); deps.push_back(m_sigmaAShader.get()); } void generateCode(std::ostringstream &oss, const std::string &evalName, const std::vector &depNames) const { oss << "vec3 " << evalName << "(vec2 uv, vec3 wi, vec3 wo) {" << endl << " vec3 sigmaS = " << depNames[0] << "(uv);" << endl << " vec3 sigmaA = " << depNames[1] << "(uv);" << endl << " vec3 albedo = sigmaS/(sigmaS + sigmaA);" << endl << " float cosThetaI = abs(cosTheta(wi));" << endl << " float cosThetaO = abs(cosTheta(wo));" << endl << " return albedo * (0.079577*cosThetaI*cosThetaO/(cosThetaI + cosThetaO));" << endl << "}" << endl << endl << "vec3 " << evalName << "_diffuse(vec2 uv, vec3 wi, vec3 wo) {" << endl << " vec3 sigmaS = " << depNames[0] << "(uv);" << endl << " vec3 sigmaA = " << depNames[1] << "(uv);" << endl << " vec3 albedo = sigmaS/(sigmaS + sigmaA);" << endl << " float cosThetaO = abs(cosTheta(wo));" << endl << " return albedo * 0.079577 * cosThetaO;" << endl << "}" << endl; } MTS_DECLARE_CLASS() private: ref m_sigmaS; ref m_sigmaA; ref m_sigmaSShader; ref m_sigmaAShader; }; Shader *HanrahanKrueger::createShader(Renderer *renderer) const { return new HanrahanKruegerShader(renderer, m_sigmaS.get(), m_sigmaA.get()); } MTS_IMPLEMENT_CLASS(HanrahanKruegerShader, false, Shader) MTS_IMPLEMENT_CLASS_S(HanrahanKrueger, false, BSDF) MTS_EXPORT_PLUGIN(HanrahanKrueger, "Hanrahan-Krueger BSDF"); MTS_NAMESPACE_END