mitsuba/include/mitsuba/render/mipmap.h

862 lines
29 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/>.
*/
#pragma once
#if !defined(__MITSUBA_RENDER_MIPMAP_H_)
#define __MITSUBA_RENDER_MIPMAP_H_
#include <mitsuba/core/bitmap.h>
#include <mitsuba/core/rfilter.h>
#include <mitsuba/core/barray.h>
#include <mitsuba/core/mmap.h>
#include <mitsuba/core/timer.h>
#include <mitsuba/core/statistics.h>
#include <boost/filesystem/fstream.hpp>
MTS_NAMESPACE_BEGIN
/// Use a blocked array to store MIP map data (slightly faster)
#define MTS_MIPMAP_BLOCKED 1
/// Look-up table size for a tabulated Gaussian filter
#define MTS_MIPMAP_LUT_SIZE 64
/// MIP map cache file version
#define MTS_MIPMAP_CACHE_VERSION 0x01
/// Make sure that the actual cache contents start on a cache line
#define MTS_MIPMAP_CACHE_ALIGNMENT 64
/* Some statistics counters */
namespace stats {
extern MTS_EXPORT_RENDER StatsCounter avgEWASamples;
extern MTS_EXPORT_RENDER StatsCounter clampedAnisotropy;
extern MTS_EXPORT_RENDER StatsCounter mipStorage;
extern MTS_EXPORT_RENDER StatsCounter filteredLookups;
};
/// Specifies the desired antialiasing filter
enum EMIPFilterType {
/// No filtering, nearest neighbor lookups
ENearest = 0,
/// No filtering, only bilinear interpolation
EBilinear = 1,
/// Basic trilinear filtering
ETrilinear = 2,
/// Elliptically weighted average
EEWA = 3
};
/**
* \brief MIP map class with support for elliptically weighted averages
*
* This class stores a precomputed collection of images that provide
* a hierarchy of resolution levels of an input image. These are used
* to prefilter texture lookups at render time, reducing aliasing
* artifacts. The implementation here supports non-power-of two images,
* different data types and quantizations thereof, as well as the
* computation of elliptically weighted averages that account for the
* anisotropy of texture lookups in UV space.
*
* Generating good mip maps is costly, and therefore this class provides
* the means to cache them on disk if desired.
*
* \tparam Value
* This class can be parameterized to yield MIP map classes for
* RGB values, color spectra, or just plain floats. This parameter
* specifies the underlying type.
*
* \tparam QuantizedValue
* If desired, the MIP map can store its contents using a quantized
* representation (e.g. half precision). In that case, this type
* denotes the associated data type.
*
* \ingroup librender
*/
template <typename Value, typename QuantizedValue> class TMIPMap : public Object {
public:
#if MTS_MIPMAP_BLOCKED == 1
/// Use a blocked array to store MIP map data
typedef BlockedArray<QuantizedValue> Array2DType;
#else
/// Use a non-blocked array to store MIP map data
typedef LinearArray<QuantizedValue> Array2DType;
#endif
/// Shortcut
typedef ReconstructionFilter::EBoundaryCondition EBoundaryCondition;
/**
* \brief Construct a new MIP map from the given bitmap
*
* \param bitmap
* An arbitrary input bitmap that will (if necessary) be
* transformed into a representation that is compatible
* with the desired MIP map pixel format.
*
* \ref pixelFormat
* A pixel format that is compatible with the
* \c Value and \c QuantizedValue types
*
* \ref componentFormat
* A component format that is compatible with the
* \c Value type.
*
* \param rfilter
* An image reconstruction filter that is used to create
* progressively lower-resolution versions of the input image.
*
* \param bcu
* Specifies how to handle texture lookups outside of the
* horizontal range [0, 1]
*
* \param bcv
* Specifies how to handle texture lookups outside of the
* vertical range [0, 1]
*
* \param filterType
* Denotes the desired filter type to use for lookups;
* the default is to use elliptically weighted averages.
*
* \param maxAnisotropy
* Denotes the highest tolerated anisotropy of the lookup
* kernel. This is necessary to bound the computational
* cost of filtered lookups.
*
* \param cacheFilename
* Optional filename of a memory-mapped cache file that is used to keep
* MIP map data out of core, and to avoid having to load and
* downsample textures over and over again in subsequent Mitsuba runs.
*
* \param maxValue
* Maximum image value. This is used to clamp out-of-range values
* that occur during the image resampling process
*
* \param intent
* When an RGB image is transformed into a spectral representation,
* this parameter specifies what conversion method should be used.
* See \ref Spectrum::EConversionIntent for further details.
*/
TMIPMap(Bitmap *bitmap_,
Bitmap::EPixelFormat pixelFormat,
Bitmap::EComponentFormat componentFormat,
const ReconstructionFilter *rfilter,
EBoundaryCondition bcu = ReconstructionFilter::ERepeat,
EBoundaryCondition bcv = ReconstructionFilter::ERepeat,
EMIPFilterType filterType = EEWA,
Float maxAnisotropy = 20.0f,
fs::path cacheFilename = fs::path(),
uint64_t timestamp = 0,
Float maxValue = 1.0f,
Spectrum::EConversionIntent intent = Spectrum::EReflectance)
: m_pixelFormat(pixelFormat), m_bcu(bcu), m_bcv(bcv), m_filterType(filterType),
m_weightLut(NULL), m_maxAnisotropy(maxAnisotropy) {
/* Keep track of time */
ref<Timer> timer = new Timer();
/* Compute the size of the MIP map cache file (for now
assuming that one should be created) */
size_t padding = sizeof(MIPMapHeader) % MTS_MIPMAP_CACHE_ALIGNMENT;
if (padding)
padding = MTS_MIPMAP_CACHE_ALIGNMENT - padding;
size_t cacheSize = sizeof(MIPMapHeader) + padding +
Array2DType::bufferSize(bitmap_->getSize());
/* 1. Determine the number of MIP levels. The following
code also handles non-power-of-2 input. */
m_levels = 1;
if (m_filterType != ENearest && m_filterType != EBilinear) {
Vector2i size = bitmap_->getSize();
while (size.x > 1 || size.y > 1) {
size.x = std::max(1, (size.x + 1) / 2);
size.y = std::max(1, (size.y + 1) / 2);
cacheSize += Array2DType::bufferSize(size);
++m_levels;
}
}
stats::mipStorage += cacheSize;
/* Potentially create a MIP map cache file */
uint8_t *mmapData = NULL, *mmapPtr = NULL;
if (!cacheFilename.empty()) {
Log(EInfo, "Generating MIP map cache file \"%s\" ..", cacheFilename.string().c_str());
try {
m_mmap = new MemoryMappedFile(cacheFilename, cacheSize);
} catch (std::runtime_error &e) {
Log(EWarn, "Unable to create MIP map cache file \"%s\" -- "
"retrying with a temporary file. Error message was: %s",
cacheFilename.string().c_str(), e.what());
m_mmap = MemoryMappedFile::createTemporary(cacheSize);
}
mmapData = mmapPtr = (uint8_t *) m_mmap->getData();
}
/* 2. Store the base image in a suitable memory layout */
m_pyramid = new Array2DType[m_levels];
m_sizeRatio = new Vector2[m_levels];
/* Allocate memory for the first MIP map level */
if (mmapPtr) {
mmapPtr += sizeof(MIPMapHeader) + padding;
m_pyramid[0].map(mmapPtr, bitmap_->getSize());
mmapPtr += m_pyramid[0].getBufferSize();
} else {
m_pyramid[0].alloc(bitmap_->getSize());
}
/* Initialize the first mip map level and extract some general
information (i.e. the minimum, maximum, and average texture value) */
ref<Bitmap> bitmap = bitmap_->expand()->convert(pixelFormat,
componentFormat, 1.0f, 1.0f, intent);
m_pyramid[0].cleanup();
m_pyramid[0].init((Value *) bitmap->getData(), m_minimum, m_maximum, m_average);
if (m_minimum.min() < 0) {
Log(EWarn, "The texture contains negative pixel values! These will be clamped!");
Value *value = (Value *) bitmap->getData();
for (size_t i=0, count=bitmap->getPixelCount(); i<count; ++i)
(*value++).clampNegative();
m_pyramid[0].init((Value *) bitmap->getData(), m_minimum, m_maximum, m_average);
}
m_sizeRatio[0] = Vector2(1, 1);
/* 3. Progressively downsample until only a 1x1 image is left */
if (m_filterType != ENearest && m_filterType != EBilinear) {
Vector2i size = bitmap_->getSize();
m_levels = 1;
while (size.x > 1 || size.y > 1) {
/* Compute the size of the next downsampled layer */
size.x = std::max(1, (size.x + 1) / 2);
size.y = std::max(1, (size.y + 1) / 2);
/* Either allocate memory or index into the memory map file */
if (mmapPtr) {
m_pyramid[m_levels].map(mmapPtr, size);
mmapPtr += m_pyramid[m_levels].getBufferSize();
} else {
m_pyramid[m_levels].alloc(size);
}
bitmap = bitmap->resample(rfilter, bcu, bcv, size, 0.0f, maxValue);
m_pyramid[m_levels].cleanup();
m_pyramid[m_levels].init((Value *) bitmap->getData());
m_sizeRatio[m_levels] = Vector2(
(Float) size.x / (Float) m_pyramid[0].getWidth(),
(Float) size.y / (Float) m_pyramid[0].getHeight());
++m_levels;
}
}
if (mmapData) {
/* If a cache file was requested, create a header that
describes the current MIP map configuration */
MIPMapHeader header;
memcpy(header.identifier, "MIP", 3);
header.version = MTS_MIPMAP_CACHE_VERSION;
header.pixelFormat = (uint8_t) m_pixelFormat;
header.levels = (uint8_t) m_levels;
header.bcu = (uint8_t) bcu;
header.bcv = (uint8_t) bcv;
header.filterType = (uint8_t) m_filterType;
header.gamma = (float) bitmap_->getGamma();
header.width = bitmap_->getWidth();
header.height = bitmap_->getHeight();
header.timestamp = timestamp;
header.minimum = m_minimum;
header.maximum = m_maximum;
header.average = m_average;
memcpy(mmapData, &header, sizeof(MIPMapHeader));
}
Log(EDebug, "Created %s of MIP maps in %i ms", memString(
getBufferSize()).c_str(), timer->getMilliseconds());
if (m_filterType == EEWA) {
m_weightLut = static_cast<Float *>(allocAligned(sizeof(Float) * MTS_MIPMAP_LUT_SIZE));
for (int i=0; i<MTS_MIPMAP_LUT_SIZE; ++i) {
Float r2 = (Float) i / (Float) (MTS_MIPMAP_LUT_SIZE-1);
m_weightLut[i] = math::fastexp(-2.0f * r2) - math::fastexp(-2.0f);
}
}
}
/**
* \brief Construct a new MIP map from a previously created cache file
*
* \param cacheFilename
* Filename of a memory-mapped cache file that is used to keep
* MIP map data out of core, and to avoid having to load and
* downsample textures over and over again in subsequent Mitsuba runs.
*
* \param maxAnisotropy
* Denotes the highest tolerated anisotropy of the lookup
* kernel. This is necessary to bound the computational
* cost of filtered lookups. This parameter is independent of the
* cache file that was previously created.
*/
TMIPMap(fs::path cacheFilename, Float maxAnisotropy = 20.0f)
: m_weightLut(NULL), m_maxAnisotropy(maxAnisotropy) {
m_mmap = new MemoryMappedFile(cacheFilename);
uint8_t *mmapPtr = (uint8_t *) m_mmap->getData();
Log(EInfo, "Mapped MIP map cache file \"%s\" into memory (%s).", cacheFilename.string().c_str(),
memString(m_mmap->getSize()).c_str());
stats::mipStorage += m_mmap->getSize();
/* Load the file header, and run some santity checks */
MIPMapHeader header;
memcpy(&header, mmapPtr, sizeof(MIPMapHeader));
Assert(header.identifier[0] == 'M' && header.identifier[1] == 'I'
&& header.identifier[2] == 'P' && header.version == MTS_MIPMAP_CACHE_VERSION);
m_pixelFormat = (Bitmap::EPixelFormat) header.pixelFormat;
m_levels = (int) header.levels;
m_bcu = (EBoundaryCondition) header.bcu;
m_bcv = (EBoundaryCondition) header.bcv;
m_filterType = (EMIPFilterType) header.filterType;
m_minimum = header.minimum;
m_maximum = header.maximum;
m_average = header.average;
/* Move the pointer to the beginning of the MIP map data */
size_t padding = sizeof(MIPMapHeader) % MTS_MIPMAP_CACHE_ALIGNMENT;
if (padding)
padding = MTS_MIPMAP_CACHE_ALIGNMENT - padding;
mmapPtr += sizeof(MIPMapHeader) + padding;
/* Map the highest resolution level */
m_pyramid = new Array2DType[m_levels];
m_sizeRatio = new Vector2[m_levels];
Vector2i size(header.width, header.height);
m_pyramid[0].map(mmapPtr, size);
mmapPtr += m_pyramid[0].getBufferSize();
m_sizeRatio[0] = Vector2(1, 1);
if (m_filterType != ENearest && m_filterType != EBilinear) {
/* Map the remainder of the image pyramid */
int level = 1;
while (size.x > 1 || size.y > 1) {
size.x = std::max(1, (size.x + 1) / 2);
size.y = std::max(1, (size.y + 1) / 2);
m_pyramid[level].map(mmapPtr, size);
m_sizeRatio[level] = Vector2(
(Float) size.x / (Float) m_pyramid[0].getWidth(),
(Float) size.y / (Float) m_pyramid[0].getHeight());
mmapPtr += m_pyramid[level++].getBufferSize();
}
Assert(level == m_levels);
}
if (m_filterType == EEWA) {
m_weightLut = static_cast<Float *>(allocAligned(sizeof(Float) * MTS_MIPMAP_LUT_SIZE));
for (int i=0; i<MTS_MIPMAP_LUT_SIZE; ++i) {
Float r2 = (Float) i / (Float) (MTS_MIPMAP_LUT_SIZE-1);
m_weightLut[i] = math::fastexp(-2.0f * r2) - math::fastexp(-2.0f);
}
}
}
/// Release all memory
~TMIPMap() {
delete[] m_pyramid;
delete[] m_sizeRatio;
if (m_weightLut)
freeAligned(m_weightLut);
}
/**
* \brief Check if a MIP map cache is up-to-date and matches the
* desired configuration
*
* \param path
* File system path of the MIP map cache
* \param timestamp
* Timestamp of the original texture file
* \param bcu
* Horizontal boundary condition used when creating the MIP map pyramid
* \param bcv
* Vertical boundary condition used when creating the MIP map pyramid
* \param gamma
* If nonzero, it is verified that the provided gamma value
* matches that of the cache file.
* \return \c true if the texture file is good for use
*/
static bool validateCacheFile(const fs::path &path, uint64_t timestamp,
Bitmap::EPixelFormat pixelFormat, EBoundaryCondition bcu,
EBoundaryCondition bcv, EMIPFilterType filterType, Float gamma) {
fs::ifstream is(path);
if (!is.good())
return false;
MIPMapHeader header;
is.read((char *) &header, sizeof(MIPMapHeader));
if (is.fail())
return false;
if (header.identifier[0] != 'M' || header.identifier[1] != 'I'
|| header.identifier[2] != 'P' || header.version != MTS_MIPMAP_CACHE_VERSION
|| header.timestamp != timestamp
|| header.bcu != (uint8_t) bcu || header.bcv != (uint8_t) bcv
|| header.pixelFormat != (uint8_t) pixelFormat
|| header.filterType != (uint8_t) filterType)
return false;
if (gamma != 0 && (float) gamma != header.gamma)
return false;
/* Sanity check on the expected file size */
size_t padding = sizeof(MIPMapHeader) % MTS_MIPMAP_CACHE_ALIGNMENT;
if (padding)
padding = MTS_MIPMAP_CACHE_ALIGNMENT - padding;
Vector2i size(header.width, header.height);
size_t expectedFileSize = sizeof(MIPMapHeader) + padding
+ Array2DType::bufferSize(size);
if (filterType != ENearest && filterType != EBilinear) {
while (size.x > 1 || size.y > 1) {
size.x = std::max(1, (size.x + 1) / 2);
size.y = std::max(1, (size.y + 1) / 2);
expectedFileSize += Array2DType::bufferSize(size);
}
}
return fs::file_size(path) == expectedFileSize;
}
/// Return the size of all buffers
size_t getBufferSize() const {
size_t size = 0;
for (int i=0; i<m_levels; ++i)
size += m_pyramid[i].getBufferSize();
return size;
}
/// Return the size of the underlying full resolution texture
inline const Vector2i &getSize() const { return m_pyramid[0].getSize(); }
/// Return the width of the represented texture
inline int getWidth() const { return getSize().x; }
/// Return the height of the represented texture
inline int getHeight() const { return getSize().y; }
/// Return the number of mip-map levels
inline int getLevels() const { return m_levels; }
/// Return the filter type that is used to pre-filter lookups
inline EMIPFilterType getFilterType() const { return m_filterType; }
/// Get the component-wise maximum at the zero level
inline const Value &getMinimum() const { return m_minimum; }
/// Get the component-wise minimum
inline const Value &getMaximum() const { return m_maximum; }
/// Get the component-wise average
inline const Value &getAverage() const { return m_average; }
/// Return the blocked array used to store a given MIP level
inline const Array2DType &getArray(int level = 0) const {
return m_pyramid[level];
}
/// Return a bitmap representation of the given level
ref<Bitmap> toBitmap(int level = 0) const {
const Array2DType &array = m_pyramid[level];
ref<Bitmap> result = new Bitmap(
m_pixelFormat,
Bitmap::componentFormat<typename QuantizedValue::Scalar>(),
array.getSize()
);
array.copyTo((QuantizedValue *) result->getData());
return result;
}
/**
* \brief Return the texture value at a texel specified using integer
* coordinates, while accounting for boundary conditions
*/
inline Value evalTexel(int level, int x, int y) const {
const Vector2i &size = m_pyramid[level].getSize();
if (x < 0 || x >= size.x) {
/* Encountered an out of bounds access -- determine what to do */
switch (m_bcu) {
case ReconstructionFilter::ERepeat:
// Assume that the input repeats in a periodic fashion
x = math::modulo(x, size.x);
break;
case ReconstructionFilter::EClamp:
// Clamp to the outermost sample position
x = math::clamp(x, 0, size.x - 1);
break;
case ReconstructionFilter::EMirror:
// Assume that the input is mirrored along the boundary
x = math::modulo(x, 2*size.x);
if (x >= size.x)
x = 2*size.x - x - 1;
break;
case ReconstructionFilter::EZero:
// Assume that the input function is zero
// outside of the defined domain
return Value(0.0f);
case ReconstructionFilter::EOne:
// Assume that the input function is equal
// to one outside of the defined domain
return Value(1.0f);
}
}
if (y < 0 || y >= size.y) {
/* Encountered an out of bounds access -- determine what to do */
switch (m_bcv) {
case ReconstructionFilter::ERepeat:
// Assume that the input repeats in a periodic fashion
y = math::modulo(y, size.y);
break;
case ReconstructionFilter::EClamp:
// Clamp to the outermost sample position
y = math::clamp(y, 0, size.y - 1);
break;
case ReconstructionFilter::EMirror:
// Assume that the input is mirrored along the boundary
y = math::modulo(y, 2*size.y);
if (y >= size.y)
y = 2*size.y - y - 1;
break;
case ReconstructionFilter::EZero:
// Assume that the input function is zero
// outside of the defined domain
return Value(0.0f);
case ReconstructionFilter::EOne:
// Assume that the input function is equal
// to one outside of the defined domain
return Value(1.0f);
}
}
return Value(m_pyramid[level](x, y));
}
/// Evaluate the texture at the given resolution using a box filter
inline Value evalBox(int level, const Point2 &uv) const {
const Vector2i &size = m_pyramid[level].getSize();
return evalTexel(level, math::floorToInt(uv.x*size.x), math::floorToInt(uv.y*size.y));
}
/**
* \brief Evaluate the texture using fractional texture coordinates and
* bilinear interpolation. The desired MIP level must be specified
*/
inline Value evalBilinear(int level, const Point2 &uv) const {
if (EXPECT_NOT_TAKEN(!std::isfinite(uv.x) || !std::isfinite(uv.y))) {
Log(EWarn, "evalBilinear(): encountered a NaN!");
return Value(0.0f);
} else if (EXPECT_NOT_TAKEN(level >= m_levels)) {
/* The lookup is larger than the entire texture */
return evalBox(m_levels-1, uv);
}
/* Convert to fractional pixel coordinates on the specified level */
const Vector2i &size = m_pyramid[level].getSize();
Float u = uv.x * size.x - 0.5f, v = uv.y * size.y - 0.5f;
int xPos = math::floorToInt(u), yPos = math::floorToInt(v);
Float dx1 = u - xPos, dx2 = 1.0f - dx1,
dy1 = v - yPos, dy2 = 1.0f - dy1;
return evalTexel(level, xPos, yPos) * dx2 * dy2
+ evalTexel(level, xPos, yPos + 1) * dx2 * dy1
+ evalTexel(level, xPos + 1, yPos) * dx1 * dy2
+ evalTexel(level, xPos + 1, yPos + 1) * dx1 * dy1;
}
/**
* \brief Evaluate the gradient of the texture at the given MIP level
*/
inline void evalGradientBilinear(int level, const Point2 &uv, Value *gradient) const {
if (EXPECT_NOT_TAKEN(!std::isfinite(uv.x) || !std::isfinite(uv.y))) {
Log(EWarn, "evalGradientBilinear(): encountered a NaN!");
gradient[0] = gradient[1] = Value(0.0f);
return;
} else if (EXPECT_NOT_TAKEN(level >= m_levels)) {
evalGradientBilinear(m_levels-1, uv, gradient);
return;
}
/* Convert to fractional pixel coordinates on the specified level */
const Vector2i &size = m_pyramid[level].getSize();
Float u = uv.x * size.x - 0.5f, v = uv.y * size.y - 0.5f;
int xPos = math::floorToInt(u), yPos = math::floorToInt(v);
Float dx = u - xPos, dy = v - yPos;
const Value p00 = evalTexel(level, xPos, yPos);
const Value p10 = evalTexel(level, xPos+1, yPos);
const Value p01 = evalTexel(level, xPos, yPos+1);
const Value p11 = evalTexel(level, xPos+1, yPos+1);
Value tmp = p01 + p10 - p11;
gradient[0] = (p10 + p00*(dy-1) - tmp*dy) * static_cast<Float> (size.x);
gradient[1] = (p01 + p00*(dx-1) - tmp*dx) * static_cast<Float> (size.y);
}
/// \brief Perform a filtered texture lookup using the configured method
Value eval(const Point2 &uv, const Vector2 &d0, const Vector2 &d1) const {
if (m_filterType == ENearest)
return evalBox(0, uv);
else if (m_filterType == EBilinear)
return evalBilinear(0, uv);
/* Convert into texel coordinates */
const Vector2i &size = m_pyramid[0].getSize();
Float du0 = d0.x * size.x, dv0 = d0.y * size.y,
du1 = d1.x * size.x, dv1 = d1.y * size.y;
/* Turn the texture-space Jacobian into the coefficients of an
implicitly defined ellipse. */
Float A = dv0*dv0 + dv1*dv1,
B = -2.0f * (du0*dv0 + du1*dv1),
C = du0*du0 + du1*du1,
F = A*C - B*B*0.25f;
/* Compute the major and minor radii */
Float root = math::hypot2(A-C, B),
Aprime = 0.5f * (A + C - root),
Cprime = 0.5f * (A + C + root),
majorRadius = Aprime != 0 ? std::sqrt(F / Aprime) : 0,
minorRadius = Cprime != 0 ? std::sqrt(F / Cprime) : 0;
if (m_filterType == ETrilinear || !(minorRadius > 0) || !(majorRadius > 0) || F < 0) {
/* Determine a suitable mip map level, while preferring
blurring over aliasing */
Float level = math::log2(std::max(majorRadius, Epsilon));
int ilevel = math::floorToInt(level);
if (ilevel < 0) {
/* Bilinear interpolation (lookup is smaller than 1 pixel) */
return evalBilinear(0, uv);
} else {
/* Trilinear interpolation between two mipmap levels */
Float a = level - ilevel;
return evalBilinear(ilevel, uv) * (1.0f - a)
+ evalBilinear(ilevel+1, uv) * a;
}
} else {
/* Artificially enlarge ellipses that are too skinny
to avoid having to compute excessively many samples */
if (minorRadius * m_maxAnisotropy < majorRadius) {
/* Enlarge the minor radius, which will cause extra blurring */
minorRadius = majorRadius / m_maxAnisotropy;
/* We need to find the coefficients of the adjusted ellipse, which
unfortunately involves expensive trig and arctrig functions.
Fortunately, this is somewhat of a corner case and won't
happen overly often in practice. */
Float theta = 0.5f * std::atan(B / (A-C)), sinTheta, cosTheta;
math::sincos(theta, &sinTheta, &cosTheta);
Float a2 = majorRadius*majorRadius,
b2 = minorRadius*minorRadius,
sinTheta2 = sinTheta*sinTheta,
cosTheta2 = cosTheta*cosTheta,
sin2Theta = 2*sinTheta*cosTheta;
A = a2*cosTheta2 + b2*sinTheta2;
B = (a2-b2) * sin2Theta;
C = a2*sinTheta2 + b2*cosTheta2;
F = a2*b2;
++stats::clampedAnisotropy;
}
stats::clampedAnisotropy.incrementBase();
/* Switch to normalized coefficients */
Float scale = 1.0f / F;
A *= scale; B *= scale; C *= scale;
/* Determine a suitable MIP map level, such that the filter
covers a reasonable amount of pixels */
Float level = std::max((Float) 0.0f, math::log2(minorRadius));
int ilevel = (int) level;
Float a = level - ilevel;
/* Switch to bilinear interpolation, be wary of round-off errors */
if (majorRadius < 1 || !(A > 0 && C > 0))
return evalBilinear(ilevel, uv);
else
return evalEWA(ilevel, uv, A, B, C) * (1.0f-a) +
evalEWA(ilevel+1, uv, A, B, C) * a;
}
}
/// Return a human-readable string representation
std::string toString() const {
std::ostringstream oss;
oss << "TMIPMap[" << endl
<< " pixelFormat = " << m_pixelFormat << "," << endl
<< " size = " << memString(getBufferSize()) << "," << endl
<< " levels = " << m_levels << "," << endl
<< " cached = " << (m_mmap.get() ? "yes" : "no") << "," << endl
<< " filterType = ";
switch (m_filterType) {
case ENearest: oss << "nearest," << endl; break;
case EBilinear: oss << "bilinear," << endl; break;
case ETrilinear: oss << "trilinear," << endl; break;
case EEWA: oss << "ewa," << endl; break;
}
oss << " bc = [" << m_bcu << ", " << m_bcv << "]," << endl
<< " minimum = " << m_minimum.toString() << "," << endl
<< " maximum = " << m_maximum.toString() << "," << endl
<< " average = " << m_average.toString() << endl
<< "]";
return oss.str();
}
MTS_DECLARE_CLASS()
protected:
/// Header file for MIP map cache files
struct MIPMapHeader {
char identifier[3];
uint8_t version;
uint8_t pixelFormat;
uint8_t levels;
uint8_t bcu:4;
uint8_t bcv:4;
uint8_t filterType;
float gamma;
int width;
int height;
uint64_t timestamp;
Value minimum;
Value maximum;
Value average;
};
/// Calculate the elliptically weighted average of a sample and associated Jacobian
Value evalEWA(int level, const Point2 &uv, Float A, Float B, Float C) const {
Assert(A > 0);
if (EXPECT_NOT_TAKEN(!std::isfinite(A+B+C+uv.x+uv.y))) {
Log(EWarn, "evalEWA(): encountered a NaN!");
return Value(0.0f);
} else if (EXPECT_NOT_TAKEN(level >= m_levels)) {
/* The lookup is larger than the entire texture */
return evalBox(m_levels-1, uv);
}
/* Convert to fractional pixel coordinates on the specified level */
const Vector2i &size = m_pyramid[level].getSize();
Float u = uv.x * size.x - 0.5f;
Float v = uv.y * size.y - 0.5f;
/* Do the same to the ellipse coefficients */
const Vector2 &ratio = m_sizeRatio[level];
A /= ratio.x * ratio.x;
B /= ratio.x * ratio.y;
C /= ratio.y * ratio.y;
/* Compute the ellipse's bounding box in texture space */
Float invDet = 1.0f / (-B*B + 4.0f*A*C),
deltaU = 2.0f * std::sqrt(C * invDet),
deltaV = 2.0f * std::sqrt(A * invDet);
int u0 = math::ceilToInt(u - deltaU), u1 = math::floorToInt(u + deltaU);
int v0 = math::ceilToInt(v - deltaV), v1 = math::floorToInt(v + deltaV);
/* Scale the coefficients by the size of the Gaussian lookup table */
Float As = A * MTS_MIPMAP_LUT_SIZE,
Bs = B * MTS_MIPMAP_LUT_SIZE,
Cs = C * MTS_MIPMAP_LUT_SIZE;
Value result(0.0f);
Float denominator = 0.0f;
Float ddq = 2*As, uu0 = (Float) u0 - u;
int nSamples = 0;
for (int vt = v0; vt <= v1; ++vt) {
const Float vv = (Float) vt - v;
Float q = As*uu0*uu0 + (Bs*uu0 + Cs*vv)*vv;
Float dq = As*(2*uu0 + 1) + Bs*vv;
for (int ut = u0; ut <= u1; ++ut) {
if (q < (Float) MTS_MIPMAP_LUT_SIZE) {
uint32_t qi = (uint32_t) q;
if (qi < MTS_MIPMAP_LUT_SIZE) {
const Float weight = m_weightLut[(int) q];
result += evalTexel(level, ut, vt) * weight;
denominator += weight;
++nSamples;
}
}
q += dq;
dq += ddq;
}
}
if (denominator == 0) {
/* The filter did not cover any samples..
Revert to bilinear interpolation */
return evalBilinear(level, uv);
} else {
stats::avgEWASamples += nSamples;
stats::avgEWASamples.incrementBase();
}
return result / denominator;
}
private:
ref<MemoryMappedFile> m_mmap;
Bitmap::EPixelFormat m_pixelFormat;
EBoundaryCondition m_bcu, m_bcv;
EMIPFilterType m_filterType;
Float *m_weightLut;
Float m_maxAnisotropy;
Vector2 *m_sizeRatio;
Array2DType *m_pyramid;
int m_levels;
Value m_minimum;
Value m_maximum;
Value m_average;
};
template <typename Value, typename QuantizedValue>
Class *TMIPMap<Value, QuantizedValue>::m_theClass
= new Class("MIPMap", false, "Object");
template <typename Value, typename QuantizedValue>
const Class *TMIPMap<Value, QuantizedValue>::getClass() const {
return m_theClass;
}
MTS_NAMESPACE_END
#endif /* __MITSUBA_RENDER_MIPMAP_H_ */