/* This file is part of Mitsuba, a physically based rendering system. Copyright (c) 2007-2012 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/fresolver.h> #include <mitsuba/core/fstream.h> #include <mitsuba/core/mstream.h> #include <mitsuba/core/plugin.h> #include <mitsuba/core/sched.h> #include <mitsuba/render/texture.h> #include <mitsuba/render/mipmap.h> #include <mitsuba/hw/renderer.h> #include <mitsuba/hw/gputexture.h> #include <mitsuba/hw/gpuprogram.h> #include <boost/algorithm/string.hpp> MTS_NAMESPACE_BEGIN /*!\plugin{bitmap}{Bitmap texture} * \order{1} * \parameters{ * \parameter{filename}{\String}{ * Filename of the bitmap to be loaded * } * \parameter{wrapMode, wrapModeU, wrapModeV}{\String}{ * Behavior of texture lookups outside of the $[0,1]$ $uv$ range.\vspace{-1mm} * \begin{enumerate}[(i)] * \item \code{repeat}: Repeat the texture indefinitely\vspace{-1mm} * \item \code{mirror}: Mirror the texture along its boundaries\vspace{-1mm} * \item \code{clamp}: Clamp $uv$ coordinates to $[0,1]$ before a lookup\vspace{-1mm} * \item \code{zero}: Switch to a zero-valued texture \vspace{-1mm} * \item \code{one}: Switch to a one-valued texture \vspace{-1mm} * \end{enumerate} * Default: \code{repeat}. The parameter \code{wrapMode} is a shortcut for * setting both \code{wrapModeU} and \code{wrapModeV} at the same time. * } * \parameter{gamma}{\Float}{ * Optional parameter to override the gamma value of the source bitmap, * where 1 indicates a linear color space and the special value -1 * corresponds to sRGB. \default{automatically detect based on the * image type and metadata} * } * \parameter{filterType}{\String}{ * Specifies the texture filturing that should be used for lookups\vspace{-1mm} * \begin{enumerate}[(i)] * \item \code{ewa}: Elliptically weighted average (a.k.a. * anisotropic filtering). This produces the best quality\vspace{-1mm} * \item \code{trilinear}: Simple trilinear (isotropic) filtering.\vspace{-1mm} * \item \code{nearest}: No filtering, do nearest neighbor lookups.\vspace{-1mm} * \end{enumerate} * Default: \code{ewa}. * } * \parameter{maxAnisotropy}{\Float}{ * Specific to \code{ewa} filtering, this parameter limits the * anisotropy (and thus the computational cost) of filtured texture lookups. The * default of 20 is a good compromise. * } * \parameter{cache}{\Boolean}{ * Preserve generated MIP map data in a cache file? This will cause a file named * \emph{filename}\code{.mip} to be created. * \default{automatic---use caching for textures larger than 1M pixels.} * } * \parameter{uoffset, voffset}{\Float}{ * Numerical offset that should be applied to UV values before a lookup * } * \parameter{uscale, vscale}{\Float}{ * Multiplicative factors that should be applied to UV values before a lookup * } * } * This plugin provides a bitmap-backed texture source that supports \emph{filtered} * texture lookups on\footnote{Some of these may not be available depending on how * Mitsuba was compiled.} JPEG, PNG, OpenEXR, RGBE, TGA, and BMP files. Filtered * lookups are useful to avoid aliasing when rendering textures that contain high * frequencies (see the next page for an example). * * The plugin operates as follows: when loading a bitmap file, it is first converted * into a linear color space. Following this, a MIP map is constructed that is necessary * to perform filtered lookups during rendering. A \emph{MIP map} is a hierarchy of * progressively lower resolution versions of the input image, where the resolution of * adjacent levels differs by a factor of two. Mitsuba creates this hierarchy using * Lanczos resampling to obtain very high quality results. * Note that textures may have an arbitrary resolution and are not limited to powers of two. * Three different filtering modes are supported: * * \begin{enumerate}[(i)] * \item Nearest neighbor lookups effectively disable filtering and always query the * highest-resolution version of the texture without any kind of interpolation. This is * fast and requires little memory (no MIP map is created), but results in visible aliasing. * Only a single pixel value is accessed. * * \item The trilinear filter performs bilinear interpolation on two adjacent MIP levels * and blends the results. Because it cannot do anisotropic (i.e. slanted) lookups in texture space, * it must compromise either on the side of blurring or aliasing. The implementation in Mitsuba * chooses blurring over aliasing (though note that (\textbf{b}) is an extreme case). * Only 8 pixel values are accessed. * * \item The EWA filter performs anisotropicically filtered lookups on two adjacent MIP map levels * and blends them. This produces the best quality, but at the expense of computation time. * Generally, 20-40 pixel values must be read for a single EWA texture lookup. To limit * the number of pixel accesses, the \code{maxAnisotropy} parameter can be used to bound * the amount of anisotropy that a texture lookup is allowed to have. * \end{enumerate} * \renderings{ * \rendering{Nearest-neighbor filter. Note the aliasing}{tex_bitmap_nearest} * \rendering{Trilinear filter. Note the blurring}{tex_bitmap_trilinear} * \vspace{-5mm} * } * \renderings{ * \setcounter{subfigure}{2} * \rendering{EWA filter}{tex_bitmap_ewa} * \rendering{Ground truth (512 samples per pixel)}{tex_bitmap_gt} * \caption{A somewhat contrived comparison of the different filters when rendering a high-frequency * checkerboard pattern using four samples per pixel. The EWA method (the default) * pre-filters the texture anisotropically to limit blurring and aliasing, but has a * higher computational cost than the other filters.} * } * \paragraph{Caching and memory requirements:} * When a texture is read, Mitsuba internally converts it into an uncompressed linear format * using a half precision (\code{float16})-based representation. This is convenient for * rendering but means that textures require copious amounts of memory (in particular, the * size of the occupied memory region might be orders of magnitude greater than that of the * original input file). * * For instance, a basic 10 megapixel image requires as much as 76 MiB of memory! Loading, * color space transformation, and MIP map construction require up to several seconds in this case. * To reduce these overheads, Mitsuba 0.4.0 introduced MIP map caches. When a large * texture is loaded for the first time, a MIP map cache file with the name \emph{filename}\code{.mip} * is generated. This is essentially a verbatim copy of the in-memory representation created * during normal rendering. Storing this information as a separate file has two advantages: * * \begin{enumerate}[(i)] * \item MIP maps do not have to be regenerated in subsequent Mitsuba runs, * which substantially reduces scene loading times. * \item Because the texture storage is entirely disk-backed and can be \emph{memory-mapped}, * Mitsuba is able to work with truly massive textures that would otherwise exhaust the main system memory. * \end{enumerate} * * The texture caches are automatically regenerated when the input texture is modified. * Of course, the cache files can be cumbersome when they are not needed anymore. On Linux * or Mac OS, they can safely be deleted by executing the following command within a scene directory. * * \begin{shell} * $\code{\$}$ find . -name "*.mip" -delete * \end{shell} */ class BitmapTexture : public Texture2D { public: /* Store texture data using half precision, but perform computations in single/double precision based on compilation flags. The following generates efficient implementations for both luminance and RGB data */ typedef TSpectrum<Float, 1> Color1; typedef TSpectrum<Float, 3> Color3; typedef TSpectrum<half, 1> Color1h; typedef TSpectrum<half, 3> Color3h; typedef TMIPMap<Color1, Color1h> MIPMap1; typedef TMIPMap<Color3, Color3h> MIPMap3; BitmapTexture(const Properties &props) : Texture2D(props) { uint64_t timestamp = 0; bool tryReuseCache = false; fs::path cacheFile; ref<Bitmap> bitmap; if (props.hasProperty("bitmap")) { /* Support initialization via raw data passed from another plugin */ bitmap = reinterpret_cast<Bitmap *>(props.getData("bitmap").ptr); } else { m_filename = Thread::getThread()->getFileResolver()->resolve( props.getString("filename")); Log(EInfo, "Loading texture \"%s\"", m_filename.filename().string().c_str()); if (!fs::exists(m_filename)) Log(EError, "Texture file \"%s\" could not be found!", m_filename.c_str()); boost::system::error_code ec; timestamp = (uint64_t) fs::last_write_time(m_filename, ec); if (ec.value()) Log(EError, "Could not determine modification time of \"%s\"!", m_filename.c_str()); cacheFile = m_filename; cacheFile.replace_extension(".mip"); tryReuseCache = fs::exists(cacheFile) && props.getBoolean("cache", true); } std::string filterType = boost::to_lower_copy(props.getString("filterType", "ewa")); std::string wrapMode = props.getString("wrapMode", "repeat"); m_wrapModeU = parseWrapMode(props.getString("wrapModeU", wrapMode)); m_wrapModeV = parseWrapMode(props.getString("wrapModeV", wrapMode)); m_gamma = props.getFloat("gamma", 0); if (filterType == "ewa") m_filterType = EEWA; else if (filterType == "trilinear") m_filterType = ETrilinear; else if (filterType == "nearest") m_filterType = ENearest; else Log(EError, "Unknown filter type '%s' -- must be " "'ewa', 'trilinear', or 'nearest'!", filterType.c_str()); m_maxAnisotropy = props.getFloat("maxAnisotropy", 20); if (m_filterType != EEWA) m_maxAnisotropy = 1.0f; if (tryReuseCache && MIPMap3::validateCacheFile(cacheFile, timestamp, Bitmap::ERGB, m_wrapModeU, m_wrapModeV, m_filterType, m_gamma)) { /* Reuse an existing MIP map cache file */ m_mipmap3 = new MIPMap3(cacheFile, m_maxAnisotropy); } else if (tryReuseCache && MIPMap1::validateCacheFile(cacheFile, timestamp, Bitmap::ELuminance, m_wrapModeU, m_wrapModeV, m_filterType, m_gamma)) { /* Reuse an existing MIP map cache file */ m_mipmap1 = new MIPMap1(cacheFile, m_maxAnisotropy); } else { if (bitmap == NULL) { /* Load the input image if necessary */ ref<Timer> timer = new Timer(); ref<FileStream> fs = new FileStream(m_filename, FileStream::EReadOnly); bitmap = new Bitmap(Bitmap::EAuto, fs); if (m_gamma != 0) bitmap->setGamma(m_gamma); Log(EDebug, "Loaded \"%s\" in %i ms", m_filename.filename().string().c_str(), timer->getMilliseconds()); } Bitmap::EPixelFormat pixelFormat; switch (bitmap->getPixelFormat()) { case Bitmap::ELuminance: case Bitmap::ELuminanceAlpha: pixelFormat = Bitmap::ELuminance; break; case Bitmap::ERGB: case Bitmap::ERGBA: pixelFormat = Bitmap::ERGB; break; default: Log(EError, "The input image has an unsupported pixel format!"); return; } /* (Re)generate the MIP map hierarchy; downsample using a 2-lobed Lanczos reconstruction filter */ Properties rfilterProps("lanczos"); rfilterProps.setInteger("lobes", 2); ref<ReconstructionFilter> rfilter = static_cast<ReconstructionFilter *> ( PluginManager::getInstance()->createObject( MTS_CLASS(ReconstructionFilter), rfilterProps)); rfilter->configure(); /* Potentially create a new MIP map cache file */ bool createCache = !cacheFile.empty() && props.getBoolean("cache", bitmap->getSize().x * bitmap->getSize().y > 1024*1024); if (pixelFormat == Bitmap::ELuminance) m_mipmap1 = new MIPMap1(bitmap, pixelFormat, Bitmap::EFloat, rfilter, m_wrapModeU, m_wrapModeV, m_filterType, m_maxAnisotropy, createCache ? cacheFile : fs::path(), timestamp); else m_mipmap3 = new MIPMap3(bitmap, pixelFormat, Bitmap::EFloat, rfilter, m_wrapModeU, m_wrapModeV, m_filterType, m_maxAnisotropy, createCache ? cacheFile : fs::path(), timestamp); } } inline ReconstructionFilter::EBoundaryCondition parseWrapMode(const std::string &wrapMode) { if (wrapMode == "repeat") return ReconstructionFilter::ERepeat; else if (wrapMode == "clamp") return ReconstructionFilter::EClamp; else if (wrapMode == "mirror") return ReconstructionFilter::EMirror; else if (wrapMode == "zero" || wrapMode == "black") return ReconstructionFilter::EZero; else if (wrapMode == "one" || wrapMode == "white") return ReconstructionFilter::EOne; else Log(EError, "Unknown wrap mode '%s' -- must be " "'repeat', 'clamp', 'black', or 'white'!", wrapMode.c_str()); return ReconstructionFilter::EZero; // make gcc happy } BitmapTexture(Stream *stream, InstanceManager *manager) : Texture2D(stream, manager) { m_filename = stream->readString(); Log(EDebug, "Unserializing texture \"%s\"", m_filename.filename().string().c_str()); m_filterType = (EMIPFilterType) stream->readUInt(); m_wrapModeU = (ReconstructionFilter::EBoundaryCondition) stream->readUInt(); m_wrapModeV = (ReconstructionFilter::EBoundaryCondition) stream->readUInt(); m_gamma = stream->readFloat(); m_maxAnisotropy = stream->readFloat(); size_t size = stream->readSize(); ref<MemoryStream> mStream = new MemoryStream(size); stream->copyTo(mStream, size); mStream->seek(0); ref<Bitmap> bitmap = new Bitmap(Bitmap::EAuto, mStream); if (m_gamma != 0) bitmap->setGamma(m_gamma); /* Downsample using a 2-lobed Lanczos reconstruction filter */ Properties rfilterProps("lanczos"); rfilterProps.setInteger("lobes", 2); ref<ReconstructionFilter> rfilter = static_cast<ReconstructionFilter *> ( PluginManager::getInstance()->createObject( MTS_CLASS(ReconstructionFilter), rfilterProps)); rfilter->configure(); Bitmap::EPixelFormat pixelFormat; switch (bitmap->getPixelFormat()) { case Bitmap::ELuminance: case Bitmap::ELuminanceAlpha: pixelFormat = Bitmap::ELuminance; break; case Bitmap::ERGB: case Bitmap::ERGBA: pixelFormat = Bitmap::ERGB; break; default: Log(EError, "The input image has an unsupported pixel format!"); return; } if (pixelFormat == Bitmap::ELuminance) m_mipmap1 = new MIPMap1(bitmap, pixelFormat, Bitmap::EFloat, rfilter, m_wrapModeU, m_wrapModeV, m_filterType, m_maxAnisotropy, fs::path(), 0); else m_mipmap3 = new MIPMap3(bitmap, pixelFormat, Bitmap::EFloat, rfilter, m_wrapModeU, m_wrapModeV, m_filterType, m_maxAnisotropy, fs::path(), 0); } void serialize(Stream *stream, InstanceManager *manager) const { Texture2D::serialize(stream, manager); stream->writeString(m_filename.string()); stream->writeUInt(m_filterType); stream->writeUInt(m_wrapModeU); stream->writeUInt(m_wrapModeV); stream->writeFloat(m_gamma); stream->writeFloat(m_maxAnisotropy); if (!m_filename.empty() && fs::exists(m_filename)) { /* We still have access to the original image -- use that, since it is probably much smaller than the in-memory representation */ ref<Stream> is = new FileStream(m_filename, FileStream::EReadOnly); stream->writeSize(is->getSize()); is->copyTo(stream); } else { /* No access to the original image anymore. Create an EXR image from the top MIP map level and serialize that */ ref<MemoryStream> mStream = new MemoryStream(); ref<Bitmap> bitmap = m_mipmap1.get() ? m_mipmap1->toBitmap() : m_mipmap3->toBitmap(); bitmap->write(Bitmap::EOpenEXR, mStream); stream->writeSize(mStream->getSize()); stream->write(mStream->getData(), mStream->getSize()); } } Spectrum eval(const Point2 &uv) const { /* There are no ray differentials to do any kind of prefiltering. Evaluate the full-resolution texture */ Spectrum result; if (m_mipmap3.get()) { Color3 value; if (m_mipmap3->getFilterType() != ENearest) value = m_mipmap3->evalBilinear(0, uv); else value = m_mipmap3->evalBox(0, uv); result.fromLinearRGB(value[0], value[1], value[2]); } else { Color1 value; if (m_mipmap1->getFilterType() != ENearest) value = m_mipmap1->evalBilinear(0, uv); else value = m_mipmap1->evalBox(0, uv); result = Spectrum(value[0]); } stats::filteredLookups.incrementBase(); return result; } Spectrum eval(const Point2 &uv, const Vector2 &d0, const Vector2 &d1) const { stats::filteredLookups.incrementBase(); ++stats::filteredLookups; Spectrum result; if (m_mipmap3.get()) { Color3 value = m_mipmap3->eval(uv, d0, d1); result.fromLinearRGB(value[0], value[1], value[2]); } else { Color1 value = m_mipmap1->eval(uv, d0, d1); result = Spectrum(value[0]); } return result; } Spectrum getAverage() const { Spectrum result; if (m_mipmap3.get()) { Color3 value = m_mipmap3->getAverage(); result.fromLinearRGB(value[0], value[1], value[2]); } else { Color1 value = m_mipmap1->getAverage(); result = Spectrum(value[0]); } return result; } Spectrum getMaximum() const { Spectrum result; if (m_mipmap3.get()) { Color3 value = m_mipmap3->getMaximum(); result.fromLinearRGB(value[0], value[1], value[2]); } else { Color1 value = m_mipmap1->getMaximum(); result = Spectrum(value[0]); } return result; } Spectrum getMinimum() const { Spectrum result; if (m_mipmap3.get()) { Color3 value = m_mipmap3->getMinimum(); result.fromLinearRGB(value[0], value[1], value[2]); } else { Color1 value = m_mipmap1->getMinimum(); result = Spectrum(value[0]); } return result; } bool isConstant() const { return false; } bool usesRayDifferentials() const { return true; } Vector3i getResolution() const { if (m_mipmap3.get()) { return Vector3i( m_mipmap3->getWidth(), m_mipmap3->getHeight(), 1 ); } else { return Vector3i( m_mipmap1->getWidth(), m_mipmap1->getHeight(), 1 ); } } std::string toString() const { std::ostringstream oss; oss << "BitmapTexture[" << endl << " filename = \"" << m_filename.string() << "\"," << endl; if (m_mipmap3.get()) oss << " mipmap = " << indent(m_mipmap3.toString()) << endl; else oss << " mipmap = " << indent(m_mipmap1.toString()) << endl; oss << "]"; return oss.str(); } Shader *createShader(Renderer *renderer) const; MTS_DECLARE_CLASS() protected: ref<MIPMap1> m_mipmap1; ref<MIPMap3> m_mipmap3; EMIPFilterType m_filterType; ReconstructionFilter::EBoundaryCondition m_wrapModeU; ReconstructionFilter::EBoundaryCondition m_wrapModeV; Float m_gamma, m_maxAnisotropy; fs::path m_filename; }; // ================ Hardware shader implementation ================ class BitmapTextureShader : public Shader { public: BitmapTextureShader(Renderer *renderer, const std::string &filename, const BitmapTexture::MIPMap1* mipmap1, const BitmapTexture::MIPMap3* mipmap3, const Point2 &uvOffset, const Vector2 &uvScale, ReconstructionFilter::EBoundaryCondition wrapModeU, ReconstructionFilter::EBoundaryCondition wrapModeV, Float maxAnisotropy) : Shader(renderer, ETextureShader), m_uvOffset(uvOffset), m_uvScale(uvScale) { ref<Bitmap> bitmap = mipmap1 ? mipmap1->toBitmap() : mipmap3->toBitmap(); m_gpuTexture = renderer->createGPUTexture(filename, bitmap); switch (wrapModeU) { case ReconstructionFilter::EClamp: m_gpuTexture->setWrapType(GPUTexture::EClampToEdge); break; case ReconstructionFilter::EMirror: m_gpuTexture->setWrapType(GPUTexture::EMirror); break; case ReconstructionFilter::ERepeat: m_gpuTexture->setWrapType(GPUTexture::ERepeat); break; case ReconstructionFilter::EZero: m_gpuTexture->setWrapType(GPUTexture::EClampToBorder); m_gpuTexture->setBorderColor(Color3(0.0f)); break; case ReconstructionFilter::EOne: m_gpuTexture->setWrapType(GPUTexture::EClampToBorder); m_gpuTexture->setBorderColor(Color3(1.0f)); break; default: Log(EError, "Unknown wrap mode!"); } switch (mipmap1 ? mipmap1->getFilterType() : mipmap3->getFilterType()) { case ENearest: m_gpuTexture->setFilterType(GPUTexture::ENearest); break; default: m_gpuTexture->setFilterType(GPUTexture::EMipMapLinear); break; } m_gpuTexture->setMaxAnisotropy(maxAnisotropy); m_gpuTexture->setMaxAnisotropy(maxAnisotropy); m_gpuTexture->initAndRelease(); } void cleanup(Renderer *renderer) { m_gpuTexture->cleanup(); } void generateCode(std::ostringstream &oss, const std::string &evalName, const std::vector<std::string> &depNames) const { oss << "uniform sampler2D " << evalName << "_texture;" << endl << "uniform vec2 " << evalName << "_uvOffset;" << endl << "uniform vec2 " << evalName << "_uvScale;" << endl << endl << "vec3 " << evalName << "(vec2 uv) {" << endl << " return texture2D(" << evalName << "_texture, vec2(" << endl << " uv.x * " << evalName << "_uvScale.x + " << evalName << "_uvOffset.x," << endl << " uv.y * " << evalName << "_uvScale.y + " << evalName << "_uvOffset.y)).rgb;" << endl << "}" << endl; } void resolve(const GPUProgram *program, const std::string &evalName, std::vector<int> ¶meterIDs) const { parameterIDs.push_back(program->getParameterID(evalName + "_texture", false)); parameterIDs.push_back(program->getParameterID(evalName + "_uvOffset", false)); parameterIDs.push_back(program->getParameterID(evalName + "_uvScale", false)); } void bind(GPUProgram *program, const std::vector<int> ¶meterIDs, int &textureUnitOffset) const { m_gpuTexture->bind(textureUnitOffset++); program->setParameter(parameterIDs[0], m_gpuTexture.get()); program->setParameter(parameterIDs[1], m_uvOffset); program->setParameter(parameterIDs[2], m_uvScale); } void unbind() const { m_gpuTexture->unbind(); } MTS_DECLARE_CLASS() private: ref<GPUTexture> m_gpuTexture; Point2 m_uvOffset; Vector2 m_uvScale; }; Shader *BitmapTexture::createShader(Renderer *renderer) const { return new BitmapTextureShader(renderer, m_filename.filename().string(), m_mipmap1.get(), m_mipmap3.get(), m_uvOffset, m_uvScale, m_wrapModeU, m_wrapModeV, m_maxAnisotropy); } MTS_IMPLEMENT_CLASS_S(BitmapTexture, false, Texture2D) MTS_IMPLEMENT_CLASS(BitmapTextureShader, false, Shader) MTS_EXPORT_PLUGIN(BitmapTexture, "Bitmap texture"); MTS_NAMESPACE_END