mitsuba/src/textures/bitmap.cpp

609 lines
23 KiB
C++

/*
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> &parameterIDs) 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> &parameterIDs,
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