got rid of exrtexture, renamed ldrtexture to bitmap and extended it to handle EXRs.

metadata
Wenzel Jakob 2011-07-03 17:10:12 +02:00
parent 38a908306e
commit c4eaf13ec8
11 changed files with 176 additions and 235 deletions

View File

@ -10,12 +10,16 @@ An simple scene with a single mesh and the default lighting and camera setup mig
something like this: something like this:
\begin{xml} \begin{xml}
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<scene> <scene version=$\MtsVer$>
<shape type="obj"> <shape type="obj">
<string name="filename" value="dragon.obj"/> <string name="filename" value="dragon.obj"/>
</shape> </shape>
</scene> </scene>
\end{xml} \end{xml}
The scene version attribute denotes the release of Mitsuba that was used to
create the scene. This information allows Mitsuba to always correctly process the
file irregardless of any potential future changes in the scene description language.
This example already contains the most important things to know about format: you can have This example already contains the most important things to know about format: you can have
\emph{objects} (such as the objects instantiated by the \code{scene} or \code{shape} tags), which are allowed to be nested within \emph{objects} (such as the objects instantiated by the \code{scene} or \code{shape} tags), which are allowed to be nested within
each other. Each object optionally accepts \emph{properties} (such as the \code{string} tag), each other. Each object optionally accepts \emph{properties} (such as the \code{string} tag),
@ -29,7 +33,7 @@ the certainly case for the plugin named \code{obj} (it contains a WaveFront OBJ
Similarly, you could write Similarly, you could write
\begin{xml} \begin{xml}
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<scene> <scene version=$\MtsVer$>
<shape type="sphere"> <shape type="sphere">
<float name="radius" value="10"/> <float name="radius" value="10"/>
</shape> </shape>
@ -43,7 +47,7 @@ The most common scene setup is to declare an integrator, some geometry, a camera
and one or more luminaires. Here is a more complex example: and one or more luminaires. Here is a more complex example:
\begin{xml} \begin{xml}
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<scene> <scene version=$\MtsVer$>
<integrator type="path"> <!-- Path trace an 8-bounce GI solution --> <integrator type="path"> <!-- Path trace an 8-bounce GI solution -->
<integer name="maxDepth" value="8"/> <integer name="maxDepth" value="8"/>
</integrator> </integrator>
@ -202,8 +206,8 @@ Quite often, you will find yourself using an object (such as a material) in many
to declare it over and over again, which wastes memory, you can make use of references. Here is an example to declare it over and over again, which wastes memory, you can make use of references. Here is an example
of how this works: of how this works:
\begin{xml} \begin{xml}
<scene> <scene version=$\MtsVer$>
<texture type="ldrtexture" id="myImage"> <texture type="bitmap" id="myImage">
<string name="filename" value="textures/myImage.jpg"/> <string name="filename" value="textures/myImage.jpg"/>
</texture> </texture>

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

View File

@ -44,13 +44,14 @@
\newcommand{\renderings}[1]{ \newcommand{\renderings}[1]{
\begin{figure}[h!] \begin{figure}[h!]
\setcounter{subfigure}{0}
\centering \centering
\hfill \hfill
#1 #1
\end{figure} \end{figure}
} }
\newcommand{\rendering}[2]{\subfigure[#1]{\fbox{\includegraphics[width=0.4\textwidth]{images/#2}}}\hfill} \newcommand{\rendering}[2]{\subfigure[#1]{\fbox{\includegraphics[width=0.47\textwidth]{images/#2}}}\hfill}
\newcommand{\medrendering}[2]{ \subfigure[#1]{\fbox{\includegraphics[width=0.3\textwidth]{images/#2}}}\hfill} \newcommand{\medrendering}[2]{ \subfigure[#1]{\fbox{\includegraphics[width=0.3\textwidth]{images/#2}}}\hfill}
\newcommand{\smallrendering}[2]{ \subfigure[#1]{\fbox{\includegraphics[width=0.2\textwidth]{images/#2}}}\hfill} \newcommand{\smallrendering}[2]{ \subfigure[#1]{\fbox{\includegraphics[width=0.2\textwidth]{images/#2}}}\hfill}

View File

@ -73,7 +73,7 @@ either be performed by nesting BSDFs within shapes, or they can
be named and then later referenced by their name. be named and then later referenced by their name.
The following fragment shows an example of both kinds of usages: The following fragment shows an example of both kinds of usages:
\begin{xml} \begin{xml}
<scene> <scene version=$\MtsVer$>
<!-- Creating a named BSDF for later use --> <!-- Creating a named BSDF for later use -->
<bsdf type=".. BSDF type .." id="myNamedMaterial"> <bsdf type=".. BSDF type .." id="myNamedMaterial">
<!-- BSDF parameters go here --> <!-- BSDF parameters go here -->

View File

@ -53,7 +53,7 @@ MTS_NAMESPACE_BEGIN
* \end{xml} * \end{xml}
* \begin{xml}[caption=Lambertian material with a texture map, label=lst:lambertian-textured] * \begin{xml}[caption=Lambertian material with a texture map, label=lst:lambertian-textured]
* <bsdf type="lambertian"> * <bsdf type="lambertian">
* <texture type="ldrtexture" name="reflectance"> * <texture type="bitmap" name="reflectance">
* <string name="filename" value="wood.jpg"/> * <string name="filename" value="wood.jpg"/>
* </texture> * </texture>
* </bsdf> * </bsdf>

View File

@ -57,7 +57,7 @@ MTS_NAMESPACE_BEGIN
* *
* \begin{xml}[caption=Lambertian material with a texture map, label=lst:lambertian-textured] * \begin{xml}[caption=Lambertian material with a texture map, label=lst:lambertian-textured]
* <bsdf type="lambertian"> * <bsdf type="lambertian">
* <texture type="ldrtexture" name="reflectance"> * <texture type="bitmap" name="reflectance">
* <string name="filename" value="wood.jpg"/> * <string name="filename" value="wood.jpg"/>
* </texture> * </texture>
* </bsdf> * </bsdf>

View File

@ -30,7 +30,7 @@ MTS_NAMESPACE_BEGIN
* used to model the surface roughness. * used to model the surface roughness.
* \begin{enumerate}[(i)] * \begin{enumerate}[(i)]
* \item \code{beckmann}: Physically-based distribution derived from * \item \code{beckmann}: Physically-based distribution derived from
* Gaussian random surfaces. This is the default choice. * Gaussian random surfaces. This is the default.
* \item \code{phong}: Classical $\cos^p\theta$ distribution. * \item \code{phong}: Classical $\cos^p\theta$ distribution.
* The Phong exponent $p$ is obtained using a transformation that * The Phong exponent $p$ is obtained using a transformation that
* produces roughness similar to a Beckmann distribution of the same * produces roughness similar to a Beckmann distribution of the same
@ -55,35 +55,62 @@ MTS_NAMESPACE_BEGIN
* \parameter{extIOR}{\Float}{Exterior index of refraction \default{1.0}} * \parameter{extIOR}{\Float}{Exterior index of refraction \default{1.0}}
* \parameter{specular\showbreak Reflectance}{\Spectrum\Or\Texture}{Optional * \parameter{specular\showbreak Reflectance}{\Spectrum\Or\Texture}{Optional
* factor used to modulate the reflectance component\default{1.0}} * factor used to modulate the reflectance component\default{1.0}}
* \parameter{specular\showbreak Transmittance}{\Spectrum\Or\Texture}{Optional * \lastparameter{specular\showbreak Transmittance}{\Spectrum\Or\Texture}{Optional
* factor used to modulate the transmittance component\default{1.0}} * factor used to modulate the transmittance component\default{1.0}}
* } * }
* *
* \renderings{
* \medrendering{Beckmann, $\alpha$=0.2}{bsdf_dielectric_glass}
* \medrendering{Beckmann, $\alpha$=0.3}{bsdf_dielectric_glass}
* \medrendering{Beckmann, $\alpha$=0.4}{bsdf_dielectric_glass}
* }
* *
* This plugin implements a realistic microfacet scattering model for rendering * This plugin implements a realistic microfacet scattering model for rendering
* rough interfaces between dielectric materials, such as a transition from air to ground glass. * rough interfaces between dielectric materials, such as a transition from air to
* Microfacet theory describes surfaces as an arrangement of unresolved and ideally specular * ground glass. Microfacet theory describes rough surfaces as an arrangement of
* facets, whose normals are given by a specially chosen \emph{microfacet * unresolved and ideally specular facets, whose normal directions are given by
* distribution}. By accounting for shadowing and masking effects between * a specially chosen \emph{microfacet distribution}. By accounting for shadowing
* these facets, it is possible to reproduce the off-specular reflections * and masking effects between these facets, it is possible to reproduce the
* peaks observed in real-world measurements of such materials. * off-specular reflections peaks observed in real-world measurements of such
* materials.
* \renderings{
* \rendering{Rough glass (Beckmann, $\alpha$=0.1)}{bsdf_roughdielectric_beckmann_0_1.jpg}
* \rendering{Ground glass (GGX, $\alpha$=0.304, \lstref{roughdielectric-roughglass})}{bsdf_roughdielectric_ggx_0_304.jpg}
* }
* *
* This plugin is essentially the ``roughened'' equivalent of the plugin * This plugin is essentially the ``roughened'' equivalent of the plugin
* \pluginref{dielectric}. Its implementation is based on the paper * \pluginref{dielectric}. As the roughness value is decreased, it increasingly
* ``Microfacet Models for Refraction through Rough Surfaces'' * approximates that model. Its implementation is based on the paper
* \cite{Walter07Microfacet}. The model supports several types of microfacet * ``Microfacet Models for Refraction through Rough Surfaces'' by Walter et
* distributions and a texturable roughness. The default settings are set * al. \cite{Walter07Microfacet}. The model supports several types of microfacet
* distributions and has a texturable roughness parameter.
* The default settings are set
* to a borosilicate glass BK7/air interface with a light amount of rougness * to a borosilicate glass BK7/air interface with a light amount of rougness
* modeled using a Beckmann distribution. * modeled using a Beckmann distribution.
* *
* When using this plugin, it is crucial that the scene contains * When using this plugin, it is crucial that the scene contains
* meaningful and mutally compatible index of refraction change -- see * meaningful and mutally compatible index of refraction change---see
* \figref{glass-explanation} for an example. * \figref{glass-explanation} for an example. Also, please note that
* the importance sampling implementation of this model is close, but
* not perfect a perfect match to the underlying scattering distribution,
* particularly for high roughness values and when the \texttt{GGX}
* model is used. Hence, such renderings may converge slowly.
*
* \begin{xml}[caption=Ground glass, label=lst:roughdielectric-roughglass]
* <bsdf type="roughdielectric">
* <string name="distribution" value="ggx"/>
* <float name="alpha" value="0.304"/>
* <float name="intIOR" value="1.5046"/>
* <float name="extIOR" value="1.0"/>
* </bsdf>
* \end{xml}
*
* \begin{xml}[caption=Textured rougness, label=lst:roughdielectric-textured]
* <bsdf type="roughdielectric">
* <string name="distribution" value="beckmann"/>
* <float name="intIOR" value="1.5046"/>
* <float name="extIOR" value="1.0"/>
*
* <texture type="bitmap" name="alpha">
* <string name="filename" value="roughness.exr"/>
* </texture>
* </bsdf>
* \end{xml}
*/ */
class RoughDielectric : public BSDF { class RoughDielectric : public BSDF {
public: public:
@ -384,7 +411,8 @@ public:
} }
/* Evaluate the roughness */ /* Evaluate the roughness */
const Float alpha = m_alpha->getValue(bRec.its).average(); const Float alpha =
std::max(m_alpha->getValue(bRec.its).average(), (Float) 1e-4f);
/* Microsurface normal distribution */ /* Microsurface normal distribution */
const Float D = evalD(H, alpha); const Float D = evalD(H, alpha);
@ -467,7 +495,8 @@ public:
} }
/* Evaluate the roughness */ /* Evaluate the roughness */
Float alpha = m_alpha->getValue(bRec.its).average(); Float alpha =
std::max(m_alpha->getValue(bRec.its).average(), (Float) 1e-4f);
/* Suggestion by Bruce Walter: sample using a slightly different /* Suggestion by Bruce Walter: sample using a slightly different
value of alpha. This in practice limits the weights to value of alpha. This in practice limits the weights to
@ -549,7 +578,8 @@ public:
} }
/* Evaluate the roughness */ /* Evaluate the roughness */
Float alpha = m_alpha->getValue(bRec.its).average(); Float alpha =
std::max(m_alpha->getValue(bRec.its).average(), (Float) 1e-4f);
/* Suggestion by Bruce Walter: sample using a slightly different /* Suggestion by Bruce Walter: sample using a slightly different
value of alpha. This in practice limits the weights to value of alpha. This in practice limits the weights to
@ -671,7 +701,8 @@ public:
} }
/* Evaluate the roughness */ /* Evaluate the roughness */
Float alpha = m_alpha->getValue(bRec.its).average(); Float alpha =
std::max(m_alpha->getValue(bRec.its).average(), (Float) 1e-4f);
/* Suggestion by Bruce Walter: sample using a slightly different /* Suggestion by Bruce Walter: sample using a slightly different
value of alpha. This in practice limits the weights to value of alpha. This in practice limits the weights to

View File

@ -1,7 +1,6 @@
Import('env', 'plugins') Import('env', 'plugins')
plugins += env.SharedLibrary('exrtexture', ['exrtexture.cpp']) plugins += env.SharedLibrary('bitmap', ['bitmap.cpp'])
plugins += env.SharedLibrary('ldrtexture', ['ldrtexture.cpp'])
plugins += env.SharedLibrary('gridtexture', ['gridtexture.cpp']) plugins += env.SharedLibrary('gridtexture', ['gridtexture.cpp'])
plugins += env.SharedLibrary('checkerboard', ['checkerboard.cpp']) plugins += env.SharedLibrary('checkerboard', ['checkerboard.cpp'])
plugins += env.SharedLibrary('vertexcolors', ['vertexcolors.cpp']) plugins += env.SharedLibrary('vertexcolors', ['vertexcolors.cpp'])

View File

@ -31,14 +31,17 @@
MTS_NAMESPACE_BEGIN MTS_NAMESPACE_BEGIN
/** /**
* Gamma-corrected bitmap texture using the JPG, PNG, TGA or BMP * Gamma-corrected bitmap texture using the EXR, JPG, PNG, TGA or BMP
* file formats.
*/ */
class LDRTexture : public Texture2D { class BitmapTexture : public Texture2D {
public: public:
LDRTexture(const Properties &props) : Texture2D(props) { BitmapTexture(const Properties &props) : Texture2D(props) {
m_filename = Thread::getThread()->getFileResolver()->resolve( m_filename = Thread::getThread()->getFileResolver()->resolve(
props.getString("filename")); props.getString("filename"));
m_gamma = props.getFloat("gamma", -1); /* -1 means sRGB */
/* -1 means sRGB. Gamma is ignored when loading EXR files */
m_gamma = props.getFloat("gamma", -1);
Log(EInfo, "Loading texture \"%s\"", m_filename.leaf().c_str()); Log(EInfo, "Loading texture \"%s\"", m_filename.leaf().c_str());
ref<FileStream> fs = new FileStream(m_filename, FileStream::EReadOnly); ref<FileStream> fs = new FileStream(m_filename, FileStream::EReadOnly);
@ -71,7 +74,9 @@ public:
m_maxAnisotropy = props.getFloat("maxAnisotropy", 8); m_maxAnisotropy = props.getFloat("maxAnisotropy", 8);
if (extension == ".jpg" || extension == ".jpeg") if (extension == ".exr")
m_format = Bitmap::EEXR;
else if (extension == ".jpg" || extension == ".jpeg")
m_format = Bitmap::EJPEG; m_format = Bitmap::EJPEG;
else if (extension == ".png") else if (extension == ".png")
m_format = Bitmap::EPNG; m_format = Bitmap::EPNG;
@ -86,7 +91,7 @@ public:
initializeFrom(bitmap); initializeFrom(bitmap);
} }
LDRTexture(Stream *stream, InstanceManager *manager) BitmapTexture(Stream *stream, InstanceManager *manager)
: Texture2D(stream, manager) { : Texture2D(stream, manager) {
m_filename = stream->readString(); m_filename = stream->readString();
Log(EInfo, "Unserializing texture \"%s\"", m_filename.leaf().c_str()); Log(EInfo, "Unserializing texture \"%s\"", m_filename.leaf().c_str());
@ -121,7 +126,13 @@ public:
} }
void initializeFrom(Bitmap *bitmap) { void initializeFrom(Bitmap *bitmap) {
ref<Bitmap> corrected = new Bitmap(bitmap->getWidth(), bitmap->getHeight(), 128); ref<Bitmap> corrected;
m_bpp = bitmap->getBitsPerPixel();
if (bitmap->getBitsPerPixel() == 128) {
/* Nothing needs to be done */
corrected = bitmap;
} else {
corrected = new Bitmap(bitmap->getWidth(), bitmap->getHeight(), 128);
float tbl[256]; float tbl[256];
if (m_gamma == -1) { if (m_gamma == -1) {
@ -200,6 +211,7 @@ public:
} else { } else {
Log(EError, "%i bpp images are currently not supported!", bitmap->getBitsPerPixel()); Log(EError, "%i bpp images are currently not supported!", bitmap->getBitsPerPixel());
} }
}
m_mipmap = MIPMap::fromBitmap(corrected, m_filterType, m_mipmap = MIPMap::fromBitmap(corrected, m_filterType,
m_wrapMode, m_maxAnisotropy); m_wrapMode, m_maxAnisotropy);
@ -258,10 +270,16 @@ public:
std::string toString() const { std::string toString() const {
std::ostringstream oss; std::ostringstream oss;
oss << "LDRTexture[" << endl oss << "BitmapTexture[" << endl
<< " filename = \"" << m_filename << "\"," << endl << " filename = \"" << m_filename << "\"," << endl
<< " gamma = " << m_gamma << endl << " bpp = " << m_bpp;
<< "]"; if (m_bpp < 128) {
oss << "," << endl
<< " gamma = " << m_gamma << endl;
} else {
oss << endl;
}
oss << "]";
return oss.str(); return oss.str();
} }
@ -278,13 +296,14 @@ protected:
Float m_gamma; Float m_gamma;
MIPMap::EWrapMode m_wrapMode; MIPMap::EWrapMode m_wrapMode;
Float m_maxAnisotropy; Float m_maxAnisotropy;
int m_bpp;
}; };
// ================ Hardware shader implementation ================ // ================ Hardware shader implementation ================
class LDRTextureShader : public Shader { class BitmapTextureShader : public Shader {
public: public:
LDRTextureShader(Renderer *renderer, std::string filename, ref<Bitmap> bitmap, BitmapTextureShader(Renderer *renderer, std::string filename, ref<Bitmap> bitmap,
const Point2 &uvOffset, const Vector2 &uvScale, MIPMap::EWrapMode wrapMode, const Point2 &uvOffset, const Vector2 &uvScale, MIPMap::EWrapMode wrapMode,
Float maxAnisotropy) Float maxAnisotropy)
: Shader(renderer, ETextureShader), m_uvOffset(uvOffset), m_uvScale(uvScale) { : Shader(renderer, ETextureShader), m_uvOffset(uvOffset), m_uvScale(uvScale) {
@ -342,14 +361,14 @@ private:
Vector2 m_uvScale; Vector2 m_uvScale;
}; };
Shader *LDRTexture::createShader(Renderer *renderer) const { Shader *BitmapTexture::createShader(Renderer *renderer) const {
return new LDRTextureShader(renderer, m_filename.leaf(), return new BitmapTextureShader(renderer, m_filename.leaf(),
m_mipmap->getLDRBitmap(), m_uvOffset, m_uvScale, m_mipmap->getLDRBitmap(), m_uvOffset, m_uvScale,
m_wrapMode, (m_filterType == MIPMap::EEWA) m_wrapMode, (m_filterType == MIPMap::EEWA)
? m_maxAnisotropy : 1.0f); ? m_maxAnisotropy : 1.0f);
} }
MTS_IMPLEMENT_CLASS_S(LDRTexture, false, Texture2D) MTS_IMPLEMENT_CLASS_S(BitmapTexture, false, Texture2D)
MTS_IMPLEMENT_CLASS(LDRTextureShader, false, Shader) MTS_IMPLEMENT_CLASS(BitmapTextureShader, false, Shader)
MTS_EXPORT_PLUGIN(LDRTexture, "LDR texture (JPG/PNG/TGA/BMP)"); MTS_EXPORT_PLUGIN(BitmapTexture, "Bitmap texture (EXR/JPG/PNG/TGA/BMP)");
MTS_NAMESPACE_END MTS_NAMESPACE_END

View File

@ -1,113 +0,0 @@
/*
This file is part of Mitsuba, a physically based rendering system.
Copyright (c) 2007-2011 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/core/bitmap.h>
#include <mitsuba/core/mstream.h>
#include <mitsuba/core/fstream.h>
#include <mitsuba/core/fresolver.h>
#include <mitsuba/core/properties.h>
#include <mitsuba/render/texture.h>
#include <mitsuba/render/mipmap.h>
MTS_NAMESPACE_BEGIN
/**
* Simple linear (i.e. not gamma corrected) bitmap texture
* using the EXR file format
*/
class EXRTexture : public Texture2D {
public:
EXRTexture(const Properties &props) : Texture2D(props) {
m_filename = Thread::getThread()->getFileResolver()->resolve(
props.getString("filename"));
Log(EInfo, "Loading texture \"%s\"", m_filename.leaf().c_str());
ref<FileStream> fs = new FileStream(m_filename, FileStream::EReadOnly);
ref<Bitmap> bitmap = new Bitmap(Bitmap::EEXR, fs);
m_mipmap = MIPMap::fromBitmap(bitmap);
m_average = m_mipmap->triangle(m_mipmap->getLevels()-1, 0, 0);
m_maximum = m_mipmap->getMaximum();
}
EXRTexture(Stream *stream, InstanceManager *manager)
: Texture2D(stream, manager) {
m_filename = stream->readString();
Log(EInfo, "Unserializing texture \"%s\"", m_filename.leaf().c_str());
size_t size = stream->readSize();
ref<MemoryStream> mStream = new MemoryStream(size);
stream->copyTo(mStream, size);
mStream->setPos(0);
ref<Bitmap> bitmap = new Bitmap(Bitmap::EEXR, mStream);
m_mipmap = MIPMap::fromBitmap(bitmap);
m_average = m_mipmap->triangle(m_mipmap->getLevels()-1, 0, 0);
m_maximum = m_mipmap->getMaximum();
}
void serialize(Stream *stream, InstanceManager *manager) const {
Texture2D::serialize(stream, manager);
stream->writeString(m_filename.file_string());
ref<Stream> is = new FileStream(m_filename, FileStream::EReadOnly);
stream->writeSize(is->getSize());
is->copyTo(stream);
}
Spectrum getValue(const Point2 &uv) const {
return m_mipmap->triangle(0, uv.x, uv.y);
}
Spectrum getValue(const Point2 &uv, Float dudx,
Float dudy, Float dvdx, Float dvdy) const {
return m_mipmap->getValue(uv.x, uv.y, dudx, dudy, dvdx, dvdy);
}
Spectrum getMaximum() const {
return m_maximum;
}
Spectrum getAverage() const {
return m_average;
}
bool usesRayDifferentials() const {
return true;
}
Vector3i getResolution() const {
return Vector3i(
m_mipmap->getWidth(),
m_mipmap->getHeight(),
1
);
}
std::string toString() const {
std::ostringstream oss;
oss << "EXRTexture[filename=\"" << m_filename.file_string() << "\"]";
return oss.str();
}
MTS_DECLARE_CLASS()
protected:
ref<MIPMap> m_mipmap;
fs::path m_filename;
Spectrum m_average, m_maximum;
};
MTS_IMPLEMENT_CLASS_S(EXRTexture, false, Texture2D)
MTS_EXPORT_PLUGIN(EXRTexture, "HDR texture (EXR)");
MTS_NAMESPACE_END