diff --git a/CMakeLists.txt b/CMakeLists.txt index f0fe2a05f6..5082bf029b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -158,6 +158,7 @@ SET ( PBRT_CORE_SOURCE src/core/filter.cpp src/core/floatfile.cpp src/core/geometry.cpp + src/core/image.cpp src/core/imageio.cpp src/core/integrator.cpp src/core/interaction.cpp @@ -169,6 +170,7 @@ SET ( PBRT_CORE_SOURCE src/core/medium.cpp src/core/memory.cpp src/core/microfacet.cpp + src/core/mipmap.cpp src/core/parallel.cpp src/core/paramset.cpp src/core/parser.cpp @@ -183,6 +185,7 @@ SET ( PBRT_CORE_SOURCE src/core/sobolmatrices.cpp src/core/spectrum.cpp src/core/stats.cpp + src/core/texcache.cpp src/core/texture.cpp src/core/transform.cpp ) diff --git a/src/cameras/realistic.cpp b/src/cameras/realistic.cpp index ae0a1b5eba..e5ee0d815b 100644 --- a/src/cameras/realistic.cpp +++ b/src/cameras/realistic.cpp @@ -36,7 +36,7 @@ #include "sampler.h" #include "sampling.h" #include "floatfile.h" -#include "imageio.h" +#include "image.h" #include "reflection.h" #include "stats.h" #include "lowdiscrepancy.h" @@ -545,8 +545,7 @@ void RealisticCamera::RenderExitPupil(Float sx, Float sy, Point3f pFilm(sx, sy, 0); const int nSamples = 2048; - Float *image = new Float[3 * nSamples * nSamples]; - Float *imagep = image; + Image image(PixelFormat::Y32, {nSamples, nSamples}); for (int y = 0; y < nSamples; ++y) { Float fy = (Float)y / (Float)(nSamples - 1); @@ -557,27 +556,17 @@ void RealisticCamera::RenderExitPupil(Float sx, Float sy, Point3f pRear(lx, ly, LensRearZ()); - if (lx * lx + ly * ly > RearElementRadius() * RearElementRadius()) { - *imagep++ = 1; - *imagep++ = 1; - *imagep++ = 1; - } else if (TraceLensesFromFilm(Ray(pFilm, pRear - pFilm), - nullptr)) { - *imagep++ = 0.5f; - *imagep++ = 0.5f; - *imagep++ = 0.5f; - } else { - *imagep++ = 0.f; - *imagep++ = 0.f; - *imagep++ = 0.f; - } + if (lx * lx + ly * ly > RearElementRadius() * RearElementRadius()) + image.SetY({x, y}, 1.); + else if (TraceLensesFromFilm(Ray(pFilm, pRear - pFilm), + nullptr)) + image.SetY({x, y}, 0.5); + else + image.SetY({x, y}, 0.); } } - WriteImage(filename, image, - Bounds2i(Point2i(0, 0), Point2i(nSamples, nSamples)), - Point2i(nSamples, nSamples)); - delete[] image; + image.Write(filename); } Point3f RealisticCamera::SampleExitPupil(const Point2f &pFilm, diff --git a/src/core/api.cpp b/src/core/api.cpp index 5b6096ed7b..b72f84fe4f 100644 --- a/src/core/api.cpp +++ b/src/core/api.cpp @@ -39,6 +39,7 @@ #include "film.h" #include "medium.h" #include "stats.h" +#include "texcache.h" // API Additional Headers #include "accelerators/bvh.h" @@ -1384,6 +1385,12 @@ void pbrtWorldEnd() { if (scene && integrator) integrator->Render(*scene); + // FIXME: yuck + if (CachedTexelProvider::textureCache) { + delete CachedTexelProvider::textureCache; + CachedTexelProvider::textureCache = nullptr; + } + MergeWorkerThreadStats(); ReportThreadStats(); if (!PbrtOptions.quiet) { @@ -1406,8 +1413,8 @@ void pbrtWorldEnd() { activeTransformBits = AllTransformsBits; namedCoordinateSystems.erase(namedCoordinateSystems.begin(), namedCoordinateSystems.end()); - ImageTexture::ClearCache(); - ImageTexture::ClearCache(); + ImageTexture::ClearCache(); + ImageTexture::ClearCache(); } Scene *RenderOptions::MakeScene() { diff --git a/src/core/film.cpp b/src/core/film.cpp index c050a5dacc..818ea8d281 100644 --- a/src/core/film.cpp +++ b/src/core/film.cpp @@ -36,6 +36,7 @@ #include "paramset.h" #include "imageio.h" #include "stats.h" +#include "image.h" namespace pbrt { @@ -169,44 +170,35 @@ void Film::WriteImage(Float splatScale) { // Convert image to RGB and compute final pixel values LOG(INFO) << "Converting image to RGB and computing final weighted pixel values"; - std::unique_ptr rgb(new Float[3 * croppedPixelBounds.Area()]); - int offset = 0; + Image rgb32(PixelFormat::RGB32, Point2i(croppedPixelBounds.Diagonal())); for (Point2i p : croppedPixelBounds) { // Convert pixel XYZ color to RGB Pixel &pixel = GetPixel(p); - XYZToRGB(pixel.xyz, &rgb[3 * offset]); - + std::array xyz = { Float(0), Float(0), Float(0) }; // Normalize pixel with weight sum Float filterWeightSum = pixel.filterWeightSum; if (filterWeightSum != 0) { Float invWt = (Float)1 / filterWeightSum; - rgb[3 * offset] = std::max((Float)0, rgb[3 * offset] * invWt); - rgb[3 * offset + 1] = - std::max((Float)0, rgb[3 * offset + 1] * invWt); - rgb[3 * offset + 2] = - std::max((Float)0, rgb[3 * offset + 2] * invWt); + for (int c = 0; c < 3; ++c) + xyz[c] = pixel.xyz[c] * invWt; } - // Add splat value at pixel - Float splatRGB[3]; - Float splatXYZ[3] = {pixel.splatXYZ[0], pixel.splatXYZ[1], - pixel.splatXYZ[2]}; - XYZToRGB(splatXYZ, splatRGB); - rgb[3 * offset] += splatScale * splatRGB[0]; - rgb[3 * offset + 1] += splatScale * splatRGB[1]; - rgb[3 * offset + 2] += splatScale * splatRGB[2]; + for (int c = 0; c < 3; ++c) + xyz[c] += splatScale * pixel.splatXYZ[c]; // Scale pixel value by _scale_ - rgb[3 * offset] *= scale; - rgb[3 * offset + 1] *= scale; - rgb[3 * offset + 2] *= scale; - ++offset; + for (int c = 0; c < 3; ++c) + xyz[c] *= scale; + + Point2i pOffset(p.x - croppedPixelBounds.pMin.x, + p.y - croppedPixelBounds.pMin.y); + rgb32.SetSpectrum(pOffset, Spectrum::FromXYZ(&xyz[0])); } // Write RGB image LOG(INFO) << "Writing image " << filename << " with bounds " << croppedPixelBounds; - pbrt::WriteImage(filename, &rgb[0], croppedPixelBounds, fullResolution); + rgb32.Write(filename, croppedPixelBounds, fullResolution); } Film *CreateFilm(const ParamSet ¶ms, std::unique_ptr filter) { diff --git a/src/core/fp16.h b/src/core/fp16.h new file mode 100644 index 0000000000..5d18089069 --- /dev/null +++ b/src/core/fp16.h @@ -0,0 +1,194 @@ + +/* + pbrt source code is Copyright(c) 1998-2016 + Matt Pharr, Greg Humphreys, and Wenzel Jakob. + + This file is part of pbrt. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are + met: + + - Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + - Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS + IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + */ + + +#ifndef PBRT_CORE_HALF_H +#define PBRT_CORE_HALF_H + +#include "pbrt.h" +#include + +namespace pbrt { + +// TODO: define a small Half struct/class? + +#ifdef PBRT_HAVE_BINARY_CONSTANTS +static const int kHalfExponentMask = 0b0111110000000000; +static const int kHalfSignificandMask = 0b1111111111; +static const int kHalfNegativeZero = 0b1000000000000000; +static const int kHalfPositiveZero = 0; +// Exponent all 1s, significand zero +static const int kHalfNegativeInfinity = 0b1111110000000000; +static const int kHalfPositiveInfinity = 0b0111110000000000; +#else +static const int kHalfExponentMask = 0x7c00; +static const int kHalfSignificandMask = 0x3ff; +static const int kHalfNegativeZero = 0x8000; +static const int kHalfPositiveZero = 0; +// Exponent all 1s, significand zero +static const int kHalfNegativeInfinity = 0xfc00; +static const int kHalfPositiveInfinity = 0x7c00; +#endif + +inline bool HalfIsInf(uint16_t h) { + return h == kHalfPositiveInfinity || h == kHalfNegativeInfinity; +} + +// -1, 1 +inline int HalfSign(uint16_t h) { return (h >> 15) ? -1 : 1; } + +inline bool HalfIsNaN(uint16_t h) { + return ((h & kHalfExponentMask) == kHalfExponentMask && + (h & kHalfSignificandMask) != 0); +} + +inline uint16_t NextHalfUp(uint16_t v) { + if (HalfIsInf(v) && HalfSign(v) == 1) return v; + if (v == kHalfNegativeZero) v = kHalfPositiveZero; + + // Advance _v_ to next higher float + if (HalfSign(v) >= 0) + ++v; + else + --v; + return v; +} + +inline uint16_t NextHalfDown(uint16_t v) { + if (HalfIsInf(v) && HalfSign(v) == -1) return v; + if (v == kHalfPositiveZero) v = kHalfNegativeZero; + if (v > 0) + --v; + else + ++v; + return v; +} + +// TODO: these should live in maybe a new core/fp.h file (that also +// picks up a bunch of other stuff...) +// TODO: support for non-AVX systems, check CPUID stuff, etc.. + +// https://gist.github.com/rygorous/2156668 +union FP32 { + uint32_t u; + float f; + struct { + unsigned int Mantissa : 23; + unsigned int Exponent : 8; + unsigned int Sign : 1; + }; +}; + +union FP16 { + uint16_t u; + struct { + unsigned int Mantissa : 10; + unsigned int Exponent : 5; + unsigned int Sign : 1; + }; +}; + +// Same, but rounding ties to nearest even instead of towards +inf +inline uint16_t FloatToHalf(float ff) { + FP32 f; + f.f = ff; + FP32 f32infty = {255 << 23}; + FP32 f16max = {(127 + 16) << 23}; + FP32 denorm_magic = {((127 - 15) + (23 - 10) + 1) << 23}; + unsigned int sign_mask = 0x80000000u; + FP16 o = {0}; + + unsigned int sign = f.u & sign_mask; + f.u ^= sign; + + // NOTE all the integer compares in this function can be safely + // compiled into signed compares since all operands are below + // 0x80000000. Important if you want fast straight SSE2 code + // (since there's no unsigned PCMPGTD). + + if (f.u >= f16max.u) // result is Inf or NaN (all exponent bits set) + o.u = (f.u > f32infty.u) ? 0x7e00 : 0x7c00; // NaN->qNaN and Inf->Inf + else // (De)normalized number or zero + { + if (f.u < (113 << 23)) // resulting FP16 is subnormal or zero + { + // use a magic value to align our 10 mantissa bits at the bottom of + // the float. as long as FP addition is round-to-nearest-even this + // just works. + f.f += denorm_magic.f; + + // and one integer subtract of the bias later, we have our final + // float! + o.u = f.u - denorm_magic.u; + } else { + unsigned int mant_odd = (f.u >> 13) & 1; // resulting mantissa is odd + + // update exponent, rounding bias part 1 + f.u += (uint32_t(15 - 127) << 23) + 0xfff; + // rounding bias part 2 + f.u += mant_odd; + // take the bits! + o.u = f.u >> 13; + } + } + + o.u |= sign >> 16; + return o.u; +} + +inline float HalfToFloat(uint16_t hh) { + FP16 h; + h.u = hh; + static const FP32 magic = {113 << 23}; + static const unsigned int shifted_exp = 0x7c00 << 13; // exponent mask after shift + FP32 o; + + o.u = (h.u & 0x7fff) << 13; // exponent/mantissa bits + unsigned int exp = shifted_exp & o.u; // just the exponent + o.u += (127 - 15) << 23; // exponent adjust + + // handle exponent special cases + if (exp == shifted_exp) // Inf/NaN? + o.u += (128 - 16) << 23; // extra exp adjust + else if (exp == 0) // Zero/Denormal? + { + o.u += 1 << 23; // extra exp adjust + o.f -= magic.f; // renormalize + } + + o.u |= (h.u & 0x8000) << 16; // sign bit + return o.f; +} + +} // namespace pbrt + +#endif // PBRT_CORE_HALF_H diff --git a/src/core/image.cpp b/src/core/image.cpp new file mode 100644 index 0000000000..1b341a622b --- /dev/null +++ b/src/core/image.cpp @@ -0,0 +1,465 @@ + +/* + pbrt source code is Copyright(c) 1998-2016 + Matt Pharr, Greg Humphreys, and Wenzel Jakob. + + This file is part of pbrt. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are + met: + + - Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + - Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS + IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + */ + +// core/image.cpp* +#include "image.h" +#include "parallel.h" +#include "texture.h" + +namespace pbrt { + +Float LinearToSRGB[256] = { + 0.0000000000, 0.0003035270, 0.0006070540, 0.0009105810, 0.0012141080, + 0.0015176350, 0.0018211619, 0.0021246888, 0.0024282159, 0.0027317430, + 0.0030352699, 0.0033465356, 0.0036765069, 0.0040247170, 0.0043914421, + 0.0047769533, 0.0051815170, 0.0056053917, 0.0060488326, 0.0065120910, + 0.0069954102, 0.0074990317, 0.0080231922, 0.0085681248, 0.0091340570, + 0.0097212177, 0.0103298230, 0.0109600937, 0.0116122449, 0.0122864870, + 0.0129830306, 0.0137020806, 0.0144438436, 0.0152085144, 0.0159962922, + 0.0168073755, 0.0176419523, 0.0185002182, 0.0193823613, 0.0202885624, + 0.0212190095, 0.0221738834, 0.0231533647, 0.0241576303, 0.0251868572, + 0.0262412224, 0.0273208916, 0.0284260381, 0.0295568332, 0.0307134409, + 0.0318960287, 0.0331047624, 0.0343398079, 0.0356013142, 0.0368894450, + 0.0382043645, 0.0395462364, 0.0409151986, 0.0423114114, 0.0437350273, + 0.0451862030, 0.0466650836, 0.0481718220, 0.0497065634, 0.0512694679, + 0.0528606549, 0.0544802807, 0.0561284944, 0.0578054339, 0.0595112406, + 0.0612460710, 0.0630100295, 0.0648032799, 0.0666259527, 0.0684781820, + 0.0703601092, 0.0722718611, 0.0742135793, 0.0761853904, 0.0781874284, + 0.0802198276, 0.0822827145, 0.0843762159, 0.0865004659, 0.0886556059, + 0.0908417329, 0.0930589810, 0.0953074843, 0.0975873619, 0.0998987406, + 0.1022417471, 0.1046164930, 0.1070231125, 0.1094617173, 0.1119324341, + 0.1144353822, 0.1169706732, 0.1195384338, 0.1221387982, 0.1247718409, + 0.1274376959, 0.1301364899, 0.1328683347, 0.1356333494, 0.1384316236, + 0.1412633061, 0.1441284865, 0.1470272839, 0.1499598026, 0.1529261619, + 0.1559264660, 0.1589608639, 0.1620294005, 0.1651322246, 0.1682693958, + 0.1714410931, 0.1746473908, 0.1778884083, 0.1811642349, 0.1844749898, + 0.1878207624, 0.1912016720, 0.1946178079, 0.1980693042, 0.2015562356, + 0.2050787061, 0.2086368501, 0.2122307271, 0.2158605307, 0.2195262313, + 0.2232279778, 0.2269658893, 0.2307400703, 0.2345506549, 0.2383976579, + 0.2422811985, 0.2462013960, 0.2501583695, 0.2541521788, 0.2581829131, + 0.2622507215, 0.2663556635, 0.2704978585, 0.2746773660, 0.2788943350, + 0.2831487954, 0.2874408960, 0.2917706966, 0.2961383164, 0.3005438447, + 0.3049873710, 0.3094689548, 0.3139887452, 0.3185468316, 0.3231432438, + 0.3277781308, 0.3324515820, 0.3371636569, 0.3419144452, 0.3467040956, + 0.3515326977, 0.3564002514, 0.3613068759, 0.3662526906, 0.3712377846, + 0.3762622178, 0.3813261092, 0.3864295185, 0.3915725648, 0.3967553079, + 0.4019778669, 0.4072403014, 0.4125427008, 0.4178851545, 0.4232677519, + 0.4286905527, 0.4341537058, 0.4396572411, 0.4452012479, 0.4507858455, + 0.4564110637, 0.4620770514, 0.4677838385, 0.4735315442, 0.4793202281, + 0.4851499796, 0.4910208881, 0.4969330430, 0.5028865933, 0.5088814497, + 0.5149177909, 0.5209956765, 0.5271152258, 0.5332764983, 0.5394796133, + 0.5457245708, 0.5520114899, 0.5583404899, 0.5647116303, 0.5711249113, + 0.5775805116, 0.5840784907, 0.5906189084, 0.5972018838, 0.6038274169, + 0.6104956269, 0.6172066331, 0.6239604354, 0.6307572126, 0.6375969648, + 0.6444797516, 0.6514056921, 0.6583748460, 0.6653873324, 0.6724432111, + 0.6795425415, 0.6866854429, 0.6938719153, 0.7011020184, 0.7083759308, + 0.7156936526, 0.7230552435, 0.7304608822, 0.7379105687, 0.7454043627, + 0.7529423237, 0.7605246305, 0.7681512833, 0.7758223414, 0.7835379243, + 0.7912980318, 0.7991028428, 0.8069523573, 0.8148466945, 0.8227858543, + 0.8307699561, 0.8387991190, 0.8468732834, 0.8549926877, 0.8631572723, + 0.8713672161, 0.8796223402, 0.8879231811, 0.8962693810, 0.9046613574, + 0.9130986929, 0.9215820432, 0.9301108718, 0.9386858940, 0.9473065734, + 0.9559735060, 0.9646862745, 0.9734454751, 0.9822505713, 0.9911022186, + 1.0000000000}; + +bool RemapPixelCoords(Point2i *p, Point2i resolution, WrapMode wrapMode) { + switch (wrapMode) { + case WrapMode::Repeat: + (*p)[0] = Mod((*p)[0], resolution[0]); + (*p)[1] = Mod((*p)[1], resolution[1]); + return true; + case WrapMode::Clamp: + (*p)[0] = Clamp((*p)[0], 0, resolution[0] - 1); + (*p)[1] = Clamp((*p)[1], 0, resolution[1] - 1); + return true; + case WrapMode::Black: + return ((*p)[0] >= 0 && (*p)[0] < resolution[0] && (*p)[1] >= 0 && + (*p)[1] < resolution[1]); + default: + LOG(ERROR) << "Unhandled WrapMode mode"; + } +} + +Image::Image(PixelFormat format, Point2i resolution) + : format(format), resolution(resolution) { + if (Is8Bit(format)) + p8.resize(nChannels() * resolution[0] * resolution[1]); + else if (Is16Bit(format)) + p16.resize(nChannels() * resolution[0] * resolution[1]); + else if (Is32Bit(format)) + p32.resize(nChannels() * resolution[0] * resolution[1]); + else + LOG(FATAL) << "Unhandled format in Image::Image()"; +} + +Image::Image(std::vector p8c, PixelFormat format, Point2i resolution) + : format(format), resolution(resolution), p8(std::move(p8c)) { + CHECK_EQ(p8.size(), nChannels() * resolution[0] * resolution[1]); + CHECK(Is8Bit(format)); +} + +Image::Image(std::vector p16c, PixelFormat format, Point2i resolution) + : format(format), resolution(resolution), p16(std::move(p16c)) { + CHECK_EQ(p16.size(), nChannels() * resolution[0] * resolution[1]); + CHECK(Is16Bit(format)); +} + +Image::Image(std::vector p32c, PixelFormat format, Point2i resolution) + : format(format), resolution(resolution), p32(std::move(p32c)) { + CHECK_EQ(p32.size(), nChannels() * resolution[0] * resolution[1]); + CHECK(Is32Bit(format)); +} + +Image Image::ConvertToFormat(PixelFormat newFormat) const { + if (newFormat == format) return *this; + CHECK_EQ(pbrt::nChannels(newFormat), nChannels()); + + Image newImage(newFormat, resolution); + int nc = nChannels(); + for (int y = 0; y < resolution.y; ++y) + for (int x = 0; x < resolution.x; ++x) + for (int c = 0; c < nc; ++c) + newImage.SetChannel({x, y}, c, GetChannel({x, y}, c)); + return newImage; +} + +Float Image::GetChannel(Point2i p, int c, WrapMode wrapMode) const { + if (!RemapPixelCoords(&p, resolution, wrapMode)) return 0; + + // Use convert()? Some rewrite/refactor? + switch (format) { + case PixelFormat::SY8: + case PixelFormat::SRGB8: + return LinearToSRGB[p8[PixelOffset(p, c)]]; + case PixelFormat::Y8: + case PixelFormat::RGB8: + return Float(p8[PixelOffset(p, c)]) / 255.f; + case PixelFormat::Y16: + case PixelFormat::RGB16: + return HalfToFloat(p16[PixelOffset(p, c)]); + case PixelFormat::Y32: + case PixelFormat::RGB32: + return Float(p32[PixelOffset(p, c)]); + default: + LOG(FATAL) << "Unhandled PixelFormat"; + } +} + +Float Image::GetY(Point2i p, WrapMode wrapMode) const { + if (nChannels() == 1) return GetChannel(p, 0, wrapMode); + CHECK_EQ(3, nChannels()); + std::array rgb = GetRGB(p, wrapMode); + // FIXME: um, this isn't luminance as we think of it... + return (rgb[0] + rgb[1] + rgb[2]) / 3; +} + +std::array Image::GetRGB(Point2i p, WrapMode wrapMode) const { + CHECK_EQ(3, nChannels()); + + if (!RemapPixelCoords(&p, resolution, wrapMode)) + return {(Float)0, (Float)0, (Float)0}; + + std::array rgb; + switch (format) { + case PixelFormat::SRGB8: + for (int c = 0; c < 3; ++c) + rgb[c] = LinearToSRGB[p8[PixelOffset(p, c)]]; + break; + case PixelFormat::RGB8: + for (int c = 0; c < 3; ++c) + rgb[c] = Float(p8[PixelOffset(p, c)]) / 255.f; + break; + case PixelFormat::RGB16: + for (int c = 0; c < 3; ++c) + rgb[c] = HalfToFloat(p16[PixelOffset(p, c)]); + break; + case PixelFormat::RGB32: + for (int c = 0; c < 3; ++c) rgb[c] = p32[PixelOffset(p, c)]; + break; + default: + LOG(FATAL) << "Unhandled PixelFormat"; + } + + return rgb; +} + +Spectrum Image::GetSpectrum(Point2i p, SpectrumType spectrumType, + WrapMode wrapMode) const { + if (nChannels() == 1) return GetChannel(p, 0, wrapMode); + std::array rgb = GetRGB(p, wrapMode); + return Spectrum::FromRGB(&rgb[0], spectrumType); +} + +Float Image::BilerpChannel(Point2f p, int c, WrapMode wrapMode) const { + Float s = p[0] * resolution.x - 0.5f; + Float t = p[1] * resolution.y - 0.5f; + int si = std::floor(s), ti = std::floor(t); + Float ds = s - si, dt = t - ti; + return ((1 - ds) * (1 - dt) * GetChannel({si, ti}, c, wrapMode) + + (1 - ds) * dt * GetChannel({si, ti + 1}, c, wrapMode) + + ds * (1 - dt) * GetChannel({si + 1, ti}, c, wrapMode) + + ds * dt * GetChannel({si + 1, ti + 1}, c, wrapMode)); +} + +Float Image::BilerpY(Point2f p, WrapMode wrapMode) const { + if (nChannels() == 1) return BilerpChannel(p, 0, wrapMode); + CHECK_EQ(3, nChannels()); + return (BilerpChannel(p, 0, wrapMode) + BilerpChannel(p, 1, wrapMode) + + BilerpChannel(p, 2, wrapMode)) / + 3; +} + +Spectrum Image::BilerpSpectrum(Point2f p, SpectrumType spectrumType, + WrapMode wrapMode) const { + if (nChannels() == 1) return Spectrum(BilerpChannel(p, 0, wrapMode)); + std::array rgb = {BilerpChannel(p, 0, wrapMode), + BilerpChannel(p, 1, wrapMode), + BilerpChannel(p, 2, wrapMode)}; + return Spectrum::FromRGB(&rgb[0], spectrumType); +} + +void Image::SetChannel(Point2i p, int c, Float value) { + CHECK(!std::isnan(value)); + if (format == PixelFormat::SRGB8 || format == PixelFormat::SY8) + // TODO: use a LUT for sRGB 8 bit + value = GammaCorrect(value); + + switch (format) { + case PixelFormat::SY8: + case PixelFormat::SRGB8: + case PixelFormat::Y8: + case PixelFormat::RGB8: + value = Clamp((value * 255.f) + 0.5f, 0, 255); + p8[PixelOffset(p, c)] = uint8_t(value); + break; + case PixelFormat::Y16: + case PixelFormat::RGB16: + p16[PixelOffset(p, c)] = FloatToHalf(value); + break; + case PixelFormat::Y32: + case PixelFormat::RGB32: + p32[PixelOffset(p, c)] = value; + break; + default: + LOG(FATAL) << "Unhandled PixelFormat in Image::SetChannel()"; + } +} + +void Image::SetY(Point2i p, Float value) { + for (int c = 0; c < nChannels(); ++c) SetChannel(p, c, value); +} + +void Image::SetSpectrum(Point2i p, const Spectrum &s) { + if (nChannels() == 1) + SetChannel(p, 0, s.Average()); + else { + CHECK_EQ(3, nChannels()); + Float rgb[3]; + s.ToRGB(rgb); + for (int c = 0; c < 3; ++c) SetChannel(p, c, rgb[c]); + } +} + +struct ResampleWeight { + int firstTexel; + Float weight[4]; +}; + +static std::unique_ptr resampleWeights(int oldRes, + int newRes) { + CHECK_GE(newRes, oldRes); + std::unique_ptr wt(new ResampleWeight[newRes]); + Float filterwidth = 2.f; + for (int i = 0; i < newRes; ++i) { + // Compute image resampling weights for _i_th texel + Float center = (i + .5f) * oldRes / newRes; + wt[i].firstTexel = std::floor((center - filterwidth) + 0.5f); + for (int j = 0; j < 4; ++j) { + Float pos = wt[i].firstTexel + j + .5f; + wt[i].weight[j] = Lanczos((pos - center) / filterwidth); + } + + // Normalize filter weights for texel resampling + Float invSumWts = 1 / (wt[i].weight[0] + wt[i].weight[1] + + wt[i].weight[2] + wt[i].weight[3]); + for (int j = 0; j < 4; ++j) wt[i].weight[j] *= invSumWts; + } + return wt; +} + +void Image::Resize(Point2i newResolution, WrapMode wrapMode) { + CHECK_GE(newResolution.x, resolution.x); + CHECK_GE(newResolution.y, resolution.y); + + // Resample image in $s$ direction + std::unique_ptr sWeights = + resampleWeights(resolution[0], newResolution[0]); + const int nc = nChannels(); + CHECK(nc == 1 || nc == 3); + Image resampledImage(nc == 1 ? PixelFormat::Y32 : PixelFormat::RGB32, + newResolution); + + // Apply _sWeights_ to zoom in $s$ direction + ParallelFor( + [&](int t) { + for (int s = 0; s < newResolution[0]; ++s) { + // Compute texel $(s,t)$ in $s$-zoomed image + for (int c = 0; c < nc; ++c) { + Float value = 0; + for (int j = 0; j < 4; ++j) { + int origS = sWeights[s].firstTexel + j; + value += sWeights[s].weight[j] * + GetChannel({origS, t}, c, wrapMode); + } + resampledImage.SetChannel({s, t}, c, value); + } + } + }, + resolution[1], 16); + + // Resample image in $t$ direction + std::unique_ptr tWeights = + resampleWeights(resolution[1], newResolution[1]); + std::vector resampleBufs; + int nThreads = MaxThreadIndex(); + for (int i = 0; i < nThreads; ++i) + resampleBufs.push_back(new Float[nc * newResolution[1]]); + ParallelFor( + [&](int s) { + Float *workData = resampleBufs[ThreadIndex]; + memset(workData, 0, sizeof(Float) * nc * newResolution[1]); + + for (int t = 0; t < newResolution[1]; ++t) { + for (int j = 0; j < 4; ++j) { + int tSrc = tWeights[t].firstTexel + j; + for (int c = 0; c < nc; ++c) + workData[t * nc + c] += + tWeights[t].weight[j] * + resampledImage.GetChannel({s, tSrc}, c); + } + } + for (int t = 0; t < newResolution[1]; ++t) + for (int c = 0; c < nc; ++c) { + Float v = Clamp(workData[nc * t + c], 0, Infinity); + resampledImage.SetChannel({s, t}, c, v); + } + }, + newResolution[0], 32); + + resolution = newResolution; + if (Is8Bit(format)) + p8.resize(nc * newResolution[0] * newResolution[1]); + else if (Is16Bit(format)) + p16.resize(nc * newResolution[0] * newResolution[1]); + else if (Is32Bit(format)) + p32.resize(nc * newResolution[0] * newResolution[1]); + else + LOG(FATAL) << "unexpected PixelFormat"; + + for (int t = 0; t < resolution[1]; ++t) + for (int s = 0; s < resolution[0]; ++s) + for (int c = 0; c < nc; ++c) + SetChannel(Point2i(s, t), c, + resampledImage.GetChannel({s, t}, c)); +} + +void Image::FlipY() { + const int nc = nChannels(); + for (int y = 0; y < resolution.y / 2; ++y) { + for (int x = 0; x < resolution.x; ++x) { + size_t o1 = PixelOffset({x, y}), + o2 = PixelOffset({x, resolution.y - 1 - y}); + for (int c = 0; c < nc; ++c) { + if (Is8Bit(format)) + std::swap(p8[o1 + c], p8[o2 + c]); + else if (Is16Bit(format)) + std::swap(p16[o1 + c], p16[o2 + c]); + else if (Is32Bit(format)) + std::swap(p32[o1 + c], p32[o2 + c]); + else + LOG(FATAL) << "unexpected format"; + } + } + } +} + +std::vector Image::GenerateMIPMap(WrapMode wrapMode) const { + // Make a copy for level 0. + Image image = *this; + + if (!IsPowerOf2(resolution[0]) || !IsPowerOf2(resolution[1])) { + // Resample image to power-of-two resolution + image.Resize({RoundUpPow2(resolution[0]), RoundUpPow2(resolution[1])}, + wrapMode); + } + + // Initialize levels of MIPMap from image + int nLevels = + 1 + Log2Int(std::max(image.resolution[0], image.resolution[1])); + std::vector pyramid(nLevels); + + // Initialize most detailed level of MIPMap + pyramid[0] = std::move(image); + + Point2i levelResolution = pyramid[0].resolution; + const int nc = nChannels(); + for (int i = 1; i < nLevels; ++i) { + // Initialize $i$th MIPMap level from $i-1$st level + levelResolution[0] = std::max(1, levelResolution[0] / 2); + levelResolution[1] = std::max(1, levelResolution[1] / 2); + pyramid[i] = Image(pyramid[0].format, levelResolution); + + // Filter four texels from finer level of pyramid + ParallelFor( + [&](int t) { + for (int s = 0; s < levelResolution[0]; ++s) { + for (int c = 0; c < nc; ++c) { + Float texel = + .25f * + (pyramid[i - 1].GetChannel(Point2i(2 * s, 2 * t), c, + wrapMode) + + pyramid[i - 1].GetChannel( + Point2i(2 * s + 1, 2 * t), c, wrapMode) + + pyramid[i - 1].GetChannel( + Point2i(2 * s, 2 * t + 1), c, wrapMode) + + pyramid[i - 1].GetChannel( + Point2i(2 * s + 1, 2 * t + 1), c, wrapMode)); + pyramid[i].SetChannel(Point2i(s, t), c, texel); + } + } + }, + levelResolution[1], 16); + } + return pyramid; +} + +} // namespace pbrt diff --git a/src/core/image.h b/src/core/image.h new file mode 100644 index 0000000000..176d74c16e --- /dev/null +++ b/src/core/image.h @@ -0,0 +1,293 @@ + +/* + pbrt source code is Copyright(c) 1998-2016 + Matt Pharr, Greg Humphreys, and Wenzel Jakob. + + This file is part of pbrt. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are + met: + + - Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + - Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS + IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + */ + +#if defined(_MSC_VER) +#define NOMINMAX +#pragma once +#endif + +#ifndef PBRT_CORE_IMAGE_H +#define PBRT_CORE_IMAGE_H + +// core/image.h* +#include "pbrt.h" +#include "geometry.h" +#include "fp16.h" +#include "spectrum.h" +#include + +namespace pbrt { + +extern Float LinearToSRGB[256]; + +/////////////////////////////////////////////////////////////////////////// +// PixelFormat + +// TODO: Y8 -> G8 (or GREY8?) +enum class PixelFormat { SY8, Y8, RGB8, SRGB8, Y16, RGB16, Y32, RGB32 }; + +inline bool Is8Bit(PixelFormat format) { + return (format == PixelFormat::SY8 || format == PixelFormat::Y8 || + format == PixelFormat::SRGB8 || format == PixelFormat::RGB8); +} + +inline bool Is16Bit(PixelFormat format) { + return (format == PixelFormat::Y16 || format == PixelFormat::RGB16); +} + +inline bool Is32Bit(PixelFormat format) { + return (format == PixelFormat::Y32 || format == PixelFormat::RGB32); +} + +inline int nChannels(PixelFormat format) { + switch (format) { + case PixelFormat::SY8: + case PixelFormat::Y8: + case PixelFormat::Y16: + case PixelFormat::Y32: + return 1; + case PixelFormat::RGB8: + case PixelFormat::SRGB8: + case PixelFormat::RGB16: + case PixelFormat::RGB32: + return 3; + } +} + +inline int TexelBytes(PixelFormat format) { + switch (format) { + case PixelFormat::SY8: + case PixelFormat::Y8: + return 1; + case PixelFormat::RGB8: + case PixelFormat::SRGB8: + return 3; + case PixelFormat::Y16: + return 2; + case PixelFormat::RGB16: + return 6; + case PixelFormat::Y32: + return 4; + case PixelFormat::RGB32: + return 12; + default: + LOG(ERROR) << "Unhandled PixelFormat in TexelBytes()"; + } +} + +template +static T ConvertTexel(const void *ptr, PixelFormat format) { + T::unimplemented_function; +} + +template <> +Spectrum ConvertTexel(const void *ptr, PixelFormat format); + +template <> +Float ConvertTexel(const void *ptr, PixelFormat format) { + if (nChannels(format) != 1) + return ConvertTexel(ptr, format).Average(); + if (ptr == nullptr) return 0; + + // TODO: are those pointer casts ok or not? ok if char I think, not + // sure about uint8_t, strictly speaking... + switch (format) { + case PixelFormat::SY8: + return LinearToSRGB[*((uint8_t *)ptr)]; + case PixelFormat::Y8: + return Float(*((uint8_t *)ptr)) / 255.f; + case PixelFormat::Y16: + return HalfToFloat(*((uint16_t *)ptr)); + case PixelFormat::Y32: + return Float(*((float *)ptr)); + default: + LOG(FATAL) << "Unhandled PixelFormat"; + } +} + +template <> +Spectrum ConvertTexel(const void *ptr, PixelFormat format) { + if (nChannels(format) == 1) + return Spectrum(ConvertTexel(ptr, format)); + if (ptr == nullptr) return Spectrum(0); + + CHECK_EQ(3, nChannels(format)); + Float rgb[3]; + for (int c = 0; c < 3; ++c) switch (format) { + case PixelFormat::SRGB8: + rgb[c] = LinearToSRGB[((uint8_t *)ptr)[c]]; + break; + case PixelFormat::RGB8: + rgb[c] = Float(((uint8_t *)ptr)[c]) / 255.f; + break; + case PixelFormat::RGB16: + rgb[c] = HalfToFloat(((uint16_t *)ptr)[c]); + break; + case PixelFormat::RGB32: + rgb[c] = Float(((float *)ptr)[c]); + break; + default: + LOG(FATAL) << "Unhandled pixelformat"; + } + + // TODO: pass through illuminant/reflectance enum? (Or nix this whole + // idea)... + return Spectrum::FromRGB(rgb); +} + +/////////////////////////////////////////////////////////////////////////// +// WrapMode + +enum class WrapMode { Repeat, Black, Clamp }; + +// TODO: use in imagemap +inline bool ParseWrapMode(const char *w, WrapMode *wrapMode) { + if (!strcmp(w, "clamp")) { + *wrapMode = WrapMode::Clamp; + return true; + } else if (!strcmp(w, "repeat")) { + *wrapMode = WrapMode::Repeat; + return true; + } else if (!strcmp(w, "black")) { + *wrapMode = WrapMode::Black; + return true; + } + return false; +} + +inline const char *WrapModeString(WrapMode mode) { + switch (mode) { + case WrapMode::Clamp: + return "clamp"; + case WrapMode::Repeat: + return "repeat"; + case WrapMode::Black: + return "black"; + default: + LOG(FATAL) << "Unhandled wrap mode"; + return nullptr; + } +} + +bool RemapPixelCoords(Point2i *p, Point2i resolution, WrapMode wrapMode); + +// Important: coordinate system for our images has (0,0) at the +// upper left corner. +// Write code does this. + +class Image { + public: + // TODO: array slice this up... + Image() : format(PixelFormat::Y8), resolution(0, 0) {} + Image(std::vector p8, PixelFormat format, Point2i resolution); + Image(std::vector p16, PixelFormat format, Point2i resolution); + Image(std::vector p32, PixelFormat format, Point2i resolution); + Image(PixelFormat format, Point2i resolution); + + // TODO: make gamme option more flexible: sRGB vs provided gamma + // exponent... + static bool Read(const std::string &filename, Image *image, + bool gamma = true, Bounds2i *dataWindow = nullptr, + Bounds2i *displayWindow = nullptr); + bool Write(const std::string &name) const; + bool Write(const std::string &name, const Bounds2i &pixelBounds, + Point2i fullResolution) const; + + Image ConvertToFormat(PixelFormat format) const; + + // TODO? provide an iterator to iterate over all pixels and channels? + + Float GetChannel(Point2i p, int c, + WrapMode wrapMode = WrapMode::Clamp) const; + Float GetY(Point2i p, WrapMode wrapMode = WrapMode::Clamp) const; + Spectrum GetSpectrum(Point2i p, + SpectrumType spectrumType = SpectrumType::Reflectance, + WrapMode wrapMode = WrapMode::Clamp) const; + + Float BilerpChannel(Point2f p, int c, + WrapMode wrapMode = WrapMode::Clamp) const; + Float BilerpY(Point2f p, WrapMode wrapMode = WrapMode::Clamp) const; + Spectrum BilerpSpectrum( + Point2f p, SpectrumType spectrumType = SpectrumType::Reflectance, + WrapMode wrapMode = WrapMode::Clamp) const; + + void SetChannel(Point2i p, int c, Float value); + void SetY(Point2i p, Float value); + void SetSpectrum(Point2i p, const Spectrum &value); + + void Resize(Point2i newResolution, WrapMode wrap); + void FlipY(); + std::vector GenerateMIPMap(WrapMode wrapMode) const; + + int nChannels() const { return pbrt::nChannels(format); } + size_t BytesUsed() const { + return p8.size() + 2 * p16.size() + 4 * p32.size(); + } + + PixelFormat format; + Point2i resolution; + + size_t PixelOffset(Point2i p, int c = 0) const { + CHECK(c >= 0 && c < nChannels()); + CHECK(InsideExclusive(p, Bounds2i({0, 0}, resolution))); + return nChannels() * (p.y * resolution.x + p.x) + c; + } + const void *RawPointer(Point2i p) const { + if (Is8Bit(format)) return p8.data() + PixelOffset(p); + if (Is16Bit(format)) + return p16.data() + PixelOffset(p); + else { + CHECK(Is32Bit(format)); + return p32.data() + PixelOffset(p); + } + } + void *RawPointer(Point2i p) { + return const_cast(((const Image *)this)->RawPointer(p)); + } + + private: + std::array GetRGB(Point2i p, WrapMode wrapMode) const; + + bool WriteEXR(const std::string &name, const Bounds2i &pixelBounds, + Point2i fullResolution) const; + bool WritePFM(const std::string &name) const; + bool WritePNG(const std::string &name) const; + bool WriteTGA(const std::string &name) const; + + std::vector p8; + std::vector p16; + std::vector p32; +}; + +} // namespace pbrt + +#endif // PBRT_CORE_IMAGE_H diff --git a/src/core/imageio.cpp b/src/core/imageio.cpp index 896b46cd03..2933067c34 100644 --- a/src/core/imageio.cpp +++ b/src/core/imageio.cpp @@ -32,100 +32,75 @@ // core/imageio.cpp* #include "imageio.h" -#include "ext/lodepng.h" -#include "ext/targa.h" +#include "geometry.h" +#include "image.h" #include "fileutil.h" #include "spectrum.h" +#include "fp16.h" +#include "ext/lodepng.h" +#include "ext/targa.h" #include #include namespace pbrt { // ImageIO Local Declarations -static void WriteImageEXR(const std::string &name, const Float *pixels, - int xRes, int yRes, int totalXRes, int totalYRes, - int xOffset, int yOffset); -static void WriteImageTGA(const std::string &name, const uint8_t *pixels, - int xRes, int yRes, int totalXRes, int totalYRes, - int xOffset, int yOffset); -static RGBSpectrum *ReadImageTGA(const std::string &name, int *w, int *h); -static RGBSpectrum *ReadImagePNG(const std::string &name, int *w, int *h); -static bool WriteImagePFM(const std::string &filename, const Float *rgb, - int xres, int yres); -static RGBSpectrum *ReadImagePFM(const std::string &filename, int *xres, - int *yres); +static bool ReadImageEXR(const std::string &name, Image *image, + Bounds2i *dataWindow, Bounds2i *displayWindow); +static bool ReadImageTGA(const std::string &name, bool gamma, Image *image); +static bool ReadImagePNG(const std::string &name, bool gamma, Image *image); +static bool ReadImagePFM(const std::string &filename, Image *image); // ImageIO Function Definitions -std::unique_ptr ReadImage(const std::string &name, - Point2i *resolution) { +bool Image::Read(const std::string &name, Image *image, bool gamma, + Bounds2i *dataWindow, Bounds2i *displayWindow) { if (HasExtension(name, ".exr")) - return std::unique_ptr( - ReadImageEXR(name, &resolution->x, &resolution->y)); - else if (HasExtension(name, ".tga")) - return std::unique_ptr( - ReadImageTGA(name, &resolution->x, &resolution->y)); + return ReadImageEXR(name, image, dataWindow, displayWindow); + + LOG_IF(ERROR, dataWindow != nullptr || displayWindow != nullptr) << + "Data window and display window not available for non-EXR images."; + + if (HasExtension(name, ".tga")) + return ReadImageTGA(name, gamma, image); else if (HasExtension(name, ".png")) - return std::unique_ptr( - ReadImagePNG(name, &resolution->x, &resolution->y)); + return ReadImagePNG(name, gamma, image); else if (HasExtension(name, ".pfm")) - return std::unique_ptr( - ReadImagePFM(name, &resolution->x, &resolution->y)); - Error("Unable to load image stored in format \"%s\" for filename \"%s\".", - strrchr(name.c_str(), '.') ? (strrchr(name.c_str(), '.') + 1) - : "(unknown)", - name.c_str()); - return nullptr; + return ReadImagePFM(name, image); + else { + Error("%s: no support for reading images with this extension", + name.c_str()); + return false; + } } -void WriteImage(const std::string &name, const Float *rgb, - const Bounds2i &outputBounds, const Point2i &totalResolution) { - Vector2i resolution = outputBounds.Diagonal(); - if (HasExtension(name, ".exr")) { - WriteImageEXR(name, rgb, resolution.x, resolution.y, totalResolution.x, - totalResolution.y, outputBounds.pMin.x, - outputBounds.pMin.y); - } else if (HasExtension(name, ".pfm")) { - WriteImagePFM(name, rgb, resolution.x, resolution.y); - } else if (HasExtension(name, ".tga") || HasExtension(name, ".png")) { - // 8-bit formats; apply gamma - Vector2i resolution = outputBounds.Diagonal(); - std::unique_ptr rgb8( - new uint8_t[3 * resolution.x * resolution.y]); - uint8_t *dst = rgb8.get(); - for (int y = 0; y < resolution.y; ++y) { - for (int x = 0; x < resolution.x; ++x) { -#define TO_BYTE(v) (uint8_t) Clamp(255.f * GammaCorrect(v) + 0.5f, 0.f, 255.f) - dst[0] = TO_BYTE(rgb[3 * (y * resolution.x + x) + 0]); - dst[1] = TO_BYTE(rgb[3 * (y * resolution.x + x) + 1]); - dst[2] = TO_BYTE(rgb[3 * (y * resolution.x + x) + 2]); -#undef TO_BYTE - dst += 3; - } - } +bool Image::Write(const std::string &name) const { + return Write(name, Bounds2i({0, 0}, resolution), resolution); +} - if (HasExtension(name, ".tga")) - WriteImageTGA(name, rgb8.get(), resolution.x, resolution.y, - totalResolution.x, totalResolution.y, - outputBounds.pMin.x, outputBounds.pMin.y); - else { - unsigned int error = lodepng_encode24_file( - name.c_str(), rgb8.get(), resolution.x, resolution.y); - if (error != 0) - Error("Error writing PNG \"%s\": %s", name.c_str(), - lodepng_error_text(error)); - } - } else { +bool Image::Write(const std::string &name, const Bounds2i &pixelBounds, + Point2i fullResolution) const { + if (HasExtension(name, ".exr")) + return WriteEXR(name, pixelBounds, fullResolution); + else if (HasExtension(name, ".pfm")) + return WritePFM(name); + else if (HasExtension(name, ".png")) + return WritePNG(name); + else if (HasExtension(name, ".tga")) + return WriteTGA(name); + else { Error("Can't determine image file type from suffix of filename \"%s\"", name.c_str()); + return false; } } -RGBSpectrum *ReadImageEXR(const std::string &name, int *width, int *height, - Bounds2i *dataWindow, Bounds2i *displayWindow) { +static bool ReadImageEXR(const std::string &name, Image *image, + Bounds2i *dataWindow, Bounds2i *displayWindow) { using namespace Imf; using namespace Imath; try { + // TODO: handle single channel EXRs directly... RgbaInputFile file(name.c_str()); Box2i dw = file.dataWindow(); @@ -138,155 +113,257 @@ RGBSpectrum *ReadImageEXR(const std::string &name, int *width, int *height, *displayWindow = {{dispw.min.x, dispw.min.y}, {dispw.max.x + 1, dispw.max.y + 1}}; } - *width = dw.max.x - dw.min.x + 1; - *height = dw.max.y - dw.min.y + 1; - std::vector pixels(*width * *height); - file.setFrameBuffer(&pixels[0] - dw.min.x - dw.min.y * *width, 1, - *width); + int width = dw.max.x - dw.min.x + 1; + int height = dw.max.y - dw.min.y + 1; + std::vector pixels(width * height); + file.setFrameBuffer(&pixels[0] - dw.min.x - dw.min.y * width, 1, width); file.readPixels(dw.min.y, dw.max.y); - RGBSpectrum *ret = new RGBSpectrum[*width * *height]; - for (int i = 0; i < *width * *height; ++i) { - Float frgb[3] = {pixels[i].r, pixels[i].g, pixels[i].b}; - ret[i] = RGBSpectrum::FromRGB(frgb); + std::vector rgb(3 * width * height); + for (int i = 0; i < width * height; ++i) { + memcpy(&rgb[3 * i], &pixels[i].r, sizeof(half)); + memcpy(&rgb[3 * i + 1], &pixels[i].g, sizeof(half)); + memcpy(&rgb[3 * i + 2], &pixels[i].b, sizeof(half)); } - LOG(INFO) << StringPrintf("Read EXR image %s (%d x %d)", - name.c_str(), *width, *height); - return ret; + LOG(INFO) << StringPrintf("Read EXR image %s (%d x %d)", name.c_str(), + width, height); + *image = + Image(std::move(rgb), PixelFormat::RGB16, Point2i(width, height)); + return true; } catch (const std::exception &e) { Error("Unable to read image file \"%s\": %s", name.c_str(), e.what()); } - return NULL; + return false; } -static void WriteImageEXR(const std::string &name, const Float *pixels, - int xRes, int yRes, int totalXRes, int totalYRes, - int xOffset, int yOffset) { +bool Image::WriteEXR(const std::string &name, const Bounds2i &pixelBounds, + Point2i fullResolution) const { using namespace Imf; using namespace Imath; - Rgba *hrgba = new Rgba[xRes * yRes]; - for (int i = 0; i < xRes * yRes; ++i) - hrgba[i] = Rgba(pixels[3 * i], pixels[3 * i + 1], pixels[3 * i + 2]); + // FIXME: if we have RGB32, it seems like we should write out float32; + // require the caller to explicitly downcast/ask for float16 if + // desired? - // OpenEXR uses inclusive pixel bounds. - Box2i displayWindow(V2i(0, 0), V2i(totalXRes - 1, totalYRes - 1)); - Box2i dataWindow(V2i(xOffset, yOffset), - V2i(xOffset + xRes - 1, yOffset + yRes - 1)); + std::unique_ptr hrgba(new Rgba[resolution.x * resolution.y]); + for (int y = 0; y < resolution.y; ++y) + for (int x = 0; x < resolution.x; ++x) + // TODO: skip the half -> float -> half round trip... + hrgba[y * resolution.x + x] = + Rgba(GetChannel({x, y}, 0), GetChannel({x, y}, 1), + GetChannel({x, y}, 2)); + + Box2i displayWindow(V2i(0, 0), + V2i(fullResolution.x - 1, fullResolution.y - 1)); + Box2i dataWindow(V2i(pixelBounds.pMin.x, pixelBounds.pMin.y), + V2i(pixelBounds.pMax.x - 1, pixelBounds.pMax.y - 1)); try { RgbaOutputFile file(name.c_str(), displayWindow, dataWindow, WRITE_RGBA); - file.setFrameBuffer(hrgba - xOffset - yOffset * xRes, 1, xRes); - file.writePixels(yRes); + file.setFrameBuffer(hrgba.get() - pixelBounds.pMin.x - + pixelBounds.pMin.y * resolution.x, + 1, resolution.x); + file.writePixels(resolution.y); } catch (const std::exception &exc) { Error("Error writing \"%s\": %s", name.c_str(), exc.what()); + return false; + } + + return true; +} + +/////////////////////////////////////////////////////////////////////////// +// PNG Function Definitions + +static inline uint8_t FloatToSRGB(Float v) { + return uint8_t(Clamp(255.f * GammaCorrect(v) + 0.5f, 0.f, 255.f)); +} + +static bool ReadImagePNG(const std::string &name, bool gamma, Image *image) { + unsigned char *rgb; + unsigned width, height; + unsigned int error = + lodepng_decode24_file(&rgb, &width, &height, name.c_str()); + if (error != 0) { + Error("Error reading PNG \"%s\": %s", name.c_str(), + lodepng_error_text(error)); + return false; + } + + std::vector rgb8(3 * width * height); + unsigned char *src = rgb; + for (int y = 0; y < height; ++y) { + for (int x = 0; x < width; ++x, src += 3) { + for (int c = 0; c < 3; ++c) + rgb8[3 * ((y * width) + x) + c] = src[c]; + } + } + + PixelFormat format = gamma ? PixelFormat::SRGB8 : PixelFormat::RGB8; + *image = Image(std::move(rgb8), format, Point2i(width, height)); + free(rgb); + return true; +} + +bool Image::WritePNG(const std::string &name) const { + unsigned int error = 0; + switch (format) { + case PixelFormat::SRGB8: + error = lodepng_encode24_file(name.c_str(), &p8[0], resolution.x, + resolution.y); + break; + case PixelFormat::SY8: + error = lodepng_encode_file(name.c_str(), &p8[0], resolution.x, + resolution.y, LCT_GREY, 8 /* bitdepth */); + break; + case PixelFormat::RGB8: + case PixelFormat::RGB16: + case PixelFormat::RGB32: { + std::unique_ptr rgb8( + new uint8_t[3 * resolution.x * resolution.y]); + for (int y = 0; y < resolution.y; ++y) + for (int x = 0; x < resolution.x; ++x) + for (int c = 0; c < 3; ++c) + rgb8[3 * (y * resolution.x + x) + c] = + FloatToSRGB(GetChannel({x, y}, c)); + + error = lodepng_encode24_file(name.c_str(), rgb8.get(), resolution.x, + resolution.y); + break; + } + case PixelFormat::Y8: + case PixelFormat::Y16: + case PixelFormat::Y32: { + std::unique_ptr y8(new uint8_t[resolution.x * resolution.y]); + for (int y = 0; y < resolution.y; ++y) + for (int x = 0; x < resolution.x; ++x) + y8[y * resolution.x + x] = FloatToSRGB(GetChannel({x, y}, 0)); + + error = lodepng_encode_file(name.c_str(), y8.get(), resolution.x, + resolution.y, LCT_GREY, 8 /* bitdepth */); + break; + } } - delete[] hrgba; + if (error != 0) { + Error("Error writing PNG \"%s\": %s", name.c_str(), + lodepng_error_text(error)); + return false; + } + return true; } +/////////////////////////////////////////////////////////////////////////// // TGA Function Definitions -void WriteImageTGA(const std::string &name, const uint8_t *pixels, int xRes, - int yRes, int totalXRes, int totalYRes, int xOffset, - int yOffset) { - // Reformat to BGR layout. - std::unique_ptr outBuf(new uint8_t[3 * xRes * yRes]); + +bool Image::WriteTGA(const std::string &name) const { + int nc = nChannels(); + std::unique_ptr outBuf( + new uint8_t[nc * resolution.x * resolution.y]); uint8_t *dst = outBuf.get(); - const uint8_t *src = pixels; - for (int y = 0; y < yRes; ++y) { - for (int x = 0; x < xRes; ++x) { - dst[0] = src[2]; - dst[1] = src[1]; - dst[2] = src[0]; - dst += 3; - src += 3; + for (int y = 0; y < resolution.y; ++y) { + for (int x = 0; x < resolution.x; ++x) { + switch (format) { + case PixelFormat::SRGB8: + // Reformat to 8-bit BGR layout. + dst[0] = p8[3 * (y * resolution.x + x) + 2]; + dst[1] = p8[3 * (y * resolution.x + x) + 1]; + dst[2] = p8[3 * (y * resolution.x + x)]; + dst += 3; + break; + case PixelFormat::SY8: + *dst++ = p8[x * resolution.x + x]; + break; + case PixelFormat::Y8: + case PixelFormat::Y16: + case PixelFormat::Y32: + *dst++ = FloatToSRGB(GetChannel({x, y}, 0)); + break; + case PixelFormat::RGB8: + case PixelFormat::RGB16: + case PixelFormat::RGB32: + // Again, reorder to BGR... + dst[0] = FloatToSRGB(GetChannel({x, y}, 2)); + dst[1] = FloatToSRGB(GetChannel({x, y}, 1)); + dst[2] = FloatToSRGB(GetChannel({x, y}, 0)); + dst += 3; + break; + default: + LOG(FATAL) << "Unhandled pixel format in WriteTGA"; + } } } tga_result result; - if ((result = tga_write_bgr(name.c_str(), outBuf.get(), xRes, yRes, 24)) != - TGA_NOERR) - Error("Unable to write output file \"%s\" (%s)", - name.c_str(), tga_error(result)); + if (nc == 1) + result = tga_write_mono(name.c_str(), outBuf.get(), resolution.x, + resolution.y); + else + result = tga_write_bgr(name.c_str(), outBuf.get(), resolution.x, + resolution.y, 24); + + if (result != TGA_NOERR) { + Error("Unable to write output file \"%s\" (%s)", name.c_str(), + tga_error(result)); + return false; + } + return true; } -static RGBSpectrum *ReadImageTGA(const std::string &name, int *width, - int *height) { +static bool ReadImageTGA(const std::string &name, bool gamma, Image *image) { tga_image img; tga_result result; if ((result = tga_read(&img, name.c_str())) != TGA_NOERR) { - Error("Unable to read from TGA file \"%s\" (%s)", - name.c_str(), tga_error(result)); - return nullptr; + Error("Unable to read from TGA file \"%s\" (%s)", name.c_str(), + tga_error(result)); + return false; } if (tga_is_right_to_left(&img)) tga_flip_horiz(&img); - if (!tga_is_top_to_bottom(&img)) tga_flip_vert(&img); + if (tga_is_top_to_bottom(&img)) tga_flip_vert(&img); if (tga_is_colormapped(&img)) tga_color_unmap(&img); - *width = img.width; - *height = img.height; + int nChannels = tga_is_mono(&img) ? 1 : 3; + std::vector pixels(img.width * img.height * nChannels); // "Unpack" the pixels (origin in the lower left corner). - // TGA pixels are in BGRA format. - RGBSpectrum *ret = new RGBSpectrum[*width * *height]; - RGBSpectrum *dst = ret; - for (int y = 0; y < *height; y++) - for (int x = 0; x < *width; x++) { + uint8_t *dst = &pixels[0]; + for (int y = 0; y < img.height; y++) + for (int x = 0; x < img.width; x++) { uint8_t *src = tga_find_pixel(&img, x, y); - if (tga_is_mono(&img)) - *dst++ = RGBSpectrum(*src / 255.f); + if (nChannels == 1) + *dst++ = *src; else { - Float c[3]; - c[2] = src[0] / 255.f; - c[1] = src[1] / 255.f; - c[0] = src[2] / 255.f; - *dst++ = RGBSpectrum::FromRGB(c); + // TGA pixels are in BGRA format. + *dst++ = src[2]; + *dst++ = src[1]; + *dst++ = src[0]; } } - tga_free_buffers(&img); LOG(INFO) << StringPrintf("Read TGA image %s (%d x %d)", - name.c_str(), *width, *height); - - return ret; -} - -static RGBSpectrum *ReadImagePNG(const std::string &name, int *width, - int *height) { - unsigned char *rgb; - unsigned w, h; - unsigned int error = lodepng_decode24_file(&rgb, &w, &h, name.c_str()); - if (error != 0) { - Error("Error reading PNG \"%s\": %s", name.c_str(), - lodepng_error_text(error)); - return nullptr; - } - *width = w; - *height = h; - - RGBSpectrum *ret = new RGBSpectrum[*width * *height]; - unsigned char *src = rgb; - for (int y = 0; y < h; ++y) { - for (int x = 0; x < w; ++x, src += 3) { - Float c[3]; - c[0] = src[0] / 255.f; - c[1] = src[1] / 255.f; - c[2] = src[2] / 255.f; - ret[y * *width + x] = RGBSpectrum::FromRGB(c); - } + name.c_str(), img.width, img.height); + + PixelFormat format; + if (nChannels == 3) + format = gamma ? PixelFormat::SRGB8 : PixelFormat::RGB8; + else { + CHECK_EQ(1, nChannels); + format = gamma ? PixelFormat::SY8 : PixelFormat::Y8; } + *image = Image(std::move(pixels), format, + Point2i(img.width, img.height)); - free(rgb); - LOG(INFO) << StringPrintf("Read PNG image %s (%d x %d)", - name.c_str(), *width, *height); - return ret; + tga_free_buffers(&img); + return true; } +/////////////////////////////////////////////////////////////////////////// // PFM Function Definitions + /* * PFM reader/writer code courtesy Jiawen "Kevin" Chen * (http://people.csail.mit.edu/jiawen/) @@ -347,10 +424,8 @@ static int readWord(FILE *fp, char *buffer, int bufferLength) { return -1; } -static RGBSpectrum *ReadImagePFM(const std::string &filename, int *xres, - int *yres) { - float *data = nullptr; - RGBSpectrum *rgb = nullptr; +static bool ReadImagePFM(const std::string &filename, Image *image) { + std::vector rgb32; char buffer[BUFFER_SIZE]; unsigned int nFloats; int nChannels, width, height; @@ -374,12 +449,10 @@ static RGBSpectrum *ReadImagePFM(const std::string &filename, int *xres, // read width if (readWord(fp, buffer, BUFFER_SIZE) == -1) goto fail; width = atoi(buffer); - *xres = width; // read height if (readWord(fp, buffer, BUFFER_SIZE) == -1) goto fail; height = atoi(buffer); - *yres = height; // read scale if (readWord(fp, buffer, BUFFER_SIZE) == -1) goto fail; @@ -387,55 +460,42 @@ static RGBSpectrum *ReadImagePFM(const std::string &filename, int *xres, // read the data nFloats = nChannels * width * height; - data = new float[nFloats]; - // Flip in Y, as P*M has the origin at the lower left. - for (int y = height - 1; y >= 0; --y) { - if (fread(&data[y * nChannels * width], sizeof(float), - nChannels * width, fp) != nChannels * width) - goto fail; - } + rgb32.resize(nFloats); + for (int y = height - 1; y >= 0; --y) + if (fread(&rgb32[nChannels * y * width], sizeof(float), + nChannels * width, fp) != nChannels * width) goto fail; // apply endian conversian and scale if appropriate fileLittleEndian = (scale < 0.f); if (hostLittleEndian ^ fileLittleEndian) { uint8_t bytes[4]; for (unsigned int i = 0; i < nFloats; ++i) { - memcpy(bytes, &data[i], 4); + memcpy(bytes, &rgb32[i], 4); std::swap(bytes[0], bytes[3]); std::swap(bytes[1], bytes[2]); - memcpy(&data[i], bytes, 4); + memcpy(&rgb32[i], bytes, 4); } } if (std::abs(scale) != 1.f) - for (unsigned int i = 0; i < nFloats; ++i) data[i] *= std::abs(scale); + for (unsigned int i = 0; i < nFloats; ++i) rgb32[i] *= std::abs(scale); // create RGBs... - rgb = new RGBSpectrum[width * height]; - if (nChannels == 1) { - for (int i = 0; i < width * height; ++i) rgb[i] = RGBSpectrum(data[i]); - } else { - for (int i = 0; i < width * height; ++i) { - Float frgb[3] = {data[3 * i], data[3 * i + 1], data[3 * i + 2]}; - rgb[i] = RGBSpectrum::FromRGB(frgb); - } - } - - delete[] data; + *image = Image(std::move(rgb32), + nChannels == 1 ? PixelFormat::Y32 : PixelFormat::RGB32, + Point2i(width, height)); fclose(fp); + LOG(INFO) << StringPrintf("Read PFM image %s (%d x %d)", - filename.c_str(), *xres, *yres); - return rgb; + filename.c_str(), width, height); + return true; fail: Error("Error reading PFM file \"%s\"", filename.c_str()); if (fp) fclose(fp); - delete[] data; - delete[] rgb; - return nullptr; + return false; } -static bool WriteImagePFM(const std::string &filename, const Float *rgb, - int width, int height) { +bool Image::WritePFM(const std::string &filename) const { FILE *fp; float scale; @@ -445,13 +505,13 @@ static bool WriteImagePFM(const std::string &filename, const Float *rgb, return false; } - std::unique_ptr scanline(new float[3 * width]); + std::unique_ptr scanline(new float[3 * resolution.x]); // only write 3 channel PFMs here... if (fprintf(fp, "PF\n") < 0) goto fail; // write the width and height, which must be positive - if (fprintf(fp, "%d %d\n", width, height) < 0) goto fail; + if (fprintf(fp, "%d %d\n", resolution.x, resolution.y) < 0) goto fail; // write the scale, which encodes endianness scale = hostLittleEndian ? -1.f : 1.f; @@ -462,13 +522,17 @@ static bool WriteImagePFM(const std::string &filename, const Float *rgb, // The raster is a sequence of pixels, packed one after another, with no // delimiters of any kind. They are grouped by row, with the pixels in each // row ordered left to right and the rows ordered bottom to top. - for (int y = height - 1; y >= 0; y--) { - // in case Float is 'double', copy into a staging buffer that's - // definitely a 32-bit float... - for (int x = 0; x < 3 * width; ++x) - scanline[x] = rgb[y * width * 3 + x]; - if (fwrite(&scanline[0], sizeof(float), width * 3, fp) < - (size_t)(width * 3)) + for (int y = resolution.y - 1; y >= 0; y--) { + for (int x = 0; x < resolution.x; ++x) { + Spectrum s = GetSpectrum({x, y}); + Float rgb[3]; + s.ToRGB(rgb); + for (int c = 0; c < 3; ++c) + // Assign element-wise in case Float is typedefed as 'double'. + scanline[3 * x + c] = rgb[c]; + } + if (fwrite(&scanline[0], sizeof(float), 3 * resolution.x, fp) < + (size_t)(3 * resolution.x)) goto fail; } diff --git a/src/core/imageio.h b/src/core/imageio.h index 57cf1a1cff..47d8dc56a5 100644 --- a/src/core/imageio.h +++ b/src/core/imageio.h @@ -38,23 +38,6 @@ #ifndef PBRT_CORE_IMAGEIO_H #define PBRT_CORE_IMAGEIO_H -// core/imageio.h* -#include "pbrt.h" -#include "geometry.h" -#include - -namespace pbrt { - -// ImageIO Declarations -std::unique_ptr ReadImage(const std::string &name, - Point2i *resolution); -RGBSpectrum *ReadImageEXR(const std::string &name, int *width, - int *height, Bounds2i *dataWindow = nullptr, - Bounds2i *displayWindow = nullptr); - -void WriteImage(const std::string &name, const Float *rgb, - const Bounds2i &outputBounds, const Point2i &totalResolution); - -} // namespace pbrt +// TODO: feh. #endif // PBRT_CORE_IMAGEIO_H diff --git a/src/core/integrator.cpp b/src/core/integrator.cpp index 98fa0b1e88..544178cca9 100644 --- a/src/core/integrator.cpp +++ b/src/core/integrator.cpp @@ -282,8 +282,12 @@ void SamplerIntegrator::Render(const Scene &scene) { RayDifferential ray; Float rayWeight = camera->GenerateRayDifferential(cameraSample, &ray); + // Don't let the ray differentials get too small; after + // a point, it doens't help, and it leads to + // unnecessary pressure on the texture cache from + // accessing too-detailed texture LODs. ray.ScaleDifferentials( - 1 / std::sqrt((Float)tileSampler->samplesPerPixel)); + std::max(Float(.125), 1 / std::sqrt((Float)tileSampler->samplesPerPixel))); ++nCameraRays; // Evaluate radiance along camera ray diff --git a/src/core/interaction.cpp b/src/core/interaction.cpp index 52a7666329..3cc14bba50 100644 --- a/src/core/interaction.cpp +++ b/src/core/interaction.cpp @@ -30,13 +30,13 @@ */ - // core/interaction.cpp* #include "interaction.h" -#include "transform.h" +#include "light.h" #include "primitive.h" +#include "reflection.h" #include "shape.h" -#include "light.h" +#include "transform.h" namespace pbrt { @@ -151,4 +151,63 @@ Spectrum SurfaceInteraction::Le(const Vector3f &w) const { return area ? area->L(*this, w) : Spectrum(0.f); } +RayDifferential SurfaceInteraction::SpawnRay(const RayDifferential &rayi, + const Vector3f &wi, int bxdfType, + Float eta) const { + RayDifferential rd = SpawnRay(wi); + + rd.hasDifferentials = true; + rd.rxOrigin = p + dpdx; + rd.ryOrigin = p + dpdy; + + if (bxdfType & BSDF_DIFFUSE) { + Vector3f v[2]; + CoordinateSystem(wi, &v[0], &v[1]); + rd.rxDirection = Normalize(wi + .2f * v[0]); + rd.ryDirection = Normalize(wi + .2f * v[1]); + } else if (bxdfType & BSDF_GLOSSY) { + Vector3f v[2]; + CoordinateSystem(wi, &v[0], &v[1]); + rd.rxDirection = Normalize(wi + .1f * v[0]); + rd.ryDirection = Normalize(wi + .1f * v[1]); + } else if (bxdfType == BxDFType(BSDF_REFLECTION | BSDF_SPECULAR)) { + rd.rxOrigin = p + dpdx; + rd.ryOrigin = p + dpdy; + // Compute differential reflected directions + Normal3f dndx = shading.dndu * dudx + shading.dndv * dvdx; + Normal3f dndy = shading.dndu * dudy + shading.dndv * dvdy; + Vector3f dwodx = -rayi.rxDirection - wo, dwody = -rayi.ryDirection - wo; + Normal3f ns = shading.n; + Float dDNdx = Dot(dwodx, ns) + Dot(wo, dndx); + Float dDNdy = Dot(dwody, ns) + Dot(wo, dndy); + rd.rxDirection = + wi - dwodx + 2.f * Vector3f(Dot(wo, ns) * dndx + dDNdx * ns); + rd.ryDirection = + wi - dwody + 2.f * Vector3f(Dot(wo, ns) * dndy + dDNdy * ns); + } else if (bxdfType == BxDFType(BSDF_TRANSMISSION | BSDF_SPECULAR)) { + rd.rxOrigin = p + dpdx; + rd.ryOrigin = p + dpdy; + + Vector3f w = -wo; + Normal3f ns = shading.n; + if (Dot(wo, ns) < 0) eta = 1.f / eta; + + Normal3f dndx = shading.dndu * dudx + shading.dndv * dvdx; + Normal3f dndy = shading.dndu * dudy + shading.dndv * dvdy; + + Vector3f dwodx = -rayi.rxDirection - wo, dwody = -rayi.ryDirection - wo; + Float dDNdx = Dot(dwodx, ns) + Dot(wo, dndx); + Float dDNdy = Dot(dwody, ns) + Dot(wo, dndy); + + Float mu = eta * Dot(w, ns) - Dot(wi, ns); + Float dmudx = (eta - (eta * eta * Dot(w, ns)) / Dot(wi, ns)) * dDNdx; + Float dmudy = (eta - (eta * eta * Dot(w, ns)) / Dot(wi, ns)) * dDNdy; + + rd.rxDirection = wi + eta * dwodx - Vector3f(mu * dndx + dmudx * ns); + rd.ryDirection = wi + eta * dwody - Vector3f(mu * dndy + dmudy * ns); + } + + return rd; +} + } // namespace pbrt diff --git a/src/core/interaction.h b/src/core/interaction.h index b61cd0d1b6..529a9f2be1 100644 --- a/src/core/interaction.h +++ b/src/core/interaction.h @@ -133,6 +133,10 @@ class SurfaceInteraction : public Interaction { void ComputeDifferentials(const RayDifferential &r) const; Spectrum Le(const Vector3f &w) const; + using Interaction::SpawnRay; + RayDifferential SpawnRay(const RayDifferential &rayi, const Vector3f &wi, + int bxdfType, Float eta) const; + // SurfaceInteraction Public Data Point2f uv; Vector3f dpdu, dpdv; diff --git a/src/core/mipmap.cpp b/src/core/mipmap.cpp new file mode 100644 index 0000000000..386aa663d4 --- /dev/null +++ b/src/core/mipmap.cpp @@ -0,0 +1,362 @@ + +/* + pbrt source code is Copyright(c) 1998-2016 + Matt Pharr, Greg Humphreys, and Wenzel Jakob. + + This file is part of pbrt. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are + met: + + - Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + - Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS + IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + */ + +#include "mipmap.h" +#include +#include +#include "fileutil.h" +#include "fp16.h" +#include "imageio.h" +#include "parallel.h" +#include "stats.h" +#include "texture.h" + +namespace pbrt { + +STAT_COUNTER("Texture/Texture map EWA lookups", nEWALookups); +STAT_COUNTER("Texture/Texture map trilinear lookups", nTrilinearLookups); +STAT_COUNTER("Texture/Texture map bilinear lookups", nBilinearLookups); +STAT_COUNTER("Texture/Texture map point lookups", nPointLookups); +STAT_MEMORY_COUNTER("Memory/Preloaded image maps", imageMapMemory); + +/////////////////////////////////////////////////////////////////////////// +// MIPMap Helper Declarations + +TexelProvider::~TexelProvider() {} + +TextureCache *CachedTexelProvider::textureCache; + +std::unique_ptr CachedTexelProvider::CreateFromFile( + const std::string &filename, WrapMode wrapMode) { + if (!textureCache) textureCache = new TextureCache; + + int id = textureCache->AddTexture(filename); + if (id < 0) return nullptr; + if (wrapMode != textureCache->GetWrapMode(id)) + Warning("%s: wrap mode in file, %s, doesn't match expected, %s.", + filename.c_str(), WrapModeString(textureCache->GetWrapMode(id)), + WrapModeString(wrapMode)); + + return std::unique_ptr( + new CachedTexelProvider(filename, wrapMode, id)); +} + +CachedTexelProvider::CachedTexelProvider(const std::string &filename, + WrapMode wrapMode, int id) + : filename(filename), wrapMode(wrapMode), id(id) {} + +CachedTexelProvider::~CachedTexelProvider() {} + +int CachedTexelProvider::Levels() const { + return textureCache->GetLevelResolution(id).size(); +} + +Point2i CachedTexelProvider::LevelResolution(int level) const { + return textureCache->GetLevelResolution(id, level); +} + +Float CachedTexelProvider::TexelFloat(int level, Point2i p) const { + if (!RemapPixelCoords(&p, LevelResolution(level), wrapMode)) return 0; + return textureCache->Texel(id, level, p); +} + +Spectrum CachedTexelProvider::TexelSpectrum(int level, Point2i p) const { + if (!RemapPixelCoords(&p, LevelResolution(level), wrapMode)) return 0; + return textureCache->Texel(id, level, p); +} + +Float CachedTexelProvider::BilerpFloat(int level, Point2f st) const { + Point2i resolution = textureCache->GetLevelResolution(id, level); + Float s = st[0] * resolution.x - 0.5f; + Float t = st[1] * resolution.y - 0.5f; + int si = std::floor(s), ti = std::floor(t); + + Float ds = s - si, dt = t - ti; + return ((1 - ds) * (1 - dt) * TexelFloat(level, {si, ti}) + + (1 - ds) * dt * TexelFloat(level, {si, ti + 1}) + + ds * (1 - dt) * TexelFloat(level, {si + 1, ti}) + + ds * dt * TexelFloat(level, {si + 1, ti + 1})); +} + +Spectrum CachedTexelProvider::BilerpSpectrum(int level, Point2f st) const { + Point2i resolution = textureCache->GetLevelResolution(id, level); + Float s = st[0] * resolution.x - 0.5f; + Float t = st[1] * resolution.y - 0.5f; + int si = std::floor(s), ti = std::floor(t); + + Float ds = s - si, dt = t - ti; + return ((1 - ds) * (1 - dt) * TexelSpectrum(level, {si, ti}) + + (1 - ds) * dt * TexelSpectrum(level, {si, ti + 1}) + + ds * (1 - dt) * TexelSpectrum(level, {si + 1, ti}) + + ds * dt * TexelSpectrum(level, {si + 1, ti + 1})); +} + +/////////////////////////////////////////////////////////////////////////// +// ImageTexelProvider + +ImageTexelProvider::ImageTexelProvider(Image image, WrapMode wrapMode, + SpectrumType spectrumType) + : wrapMode(wrapMode), spectrumType(spectrumType) { + pyramid = image.GenerateMIPMap(wrapMode); + std::for_each(pyramid.begin(), pyramid.end(), + [](const Image &im) { imageMapMemory += im.BytesUsed(); }); +} + +Point2i ImageTexelProvider::LevelResolution(int level) const { + CHECK(level >= 0 && level < pyramid.size()); + return pyramid[level].resolution; +} + +int ImageTexelProvider::Levels() const { return int(pyramid.size()); } + +Float ImageTexelProvider::TexelFloat(int level, Point2i st) const { + CHECK(level >= 0 && level < pyramid.size()); + return pyramid[level].GetY(st, wrapMode); +} + +Spectrum ImageTexelProvider::TexelSpectrum(int level, Point2i st) const { + CHECK(level >= 0 && level < pyramid.size()); + return pyramid[level].GetSpectrum(st, spectrumType, wrapMode); +} + +Float ImageTexelProvider::BilerpFloat(int level, Point2f st) const { + CHECK(level >= 0 && level < pyramid.size()); + return pyramid[level].BilerpY(st, wrapMode); +} + +Spectrum ImageTexelProvider::BilerpSpectrum(int level, Point2f st) const { + CHECK(level >= 0 && level < pyramid.size()); + return pyramid[level].BilerpSpectrum(st, spectrumType, wrapMode); +} + +/////////////////////////////////////////////////////////////////////////// + +static PBRT_CONSTEXPR int WeightLUTSize = 128; +static Float weightLut[WeightLUTSize]; + +// MIPMap Method Definitions +MIPMap::MIPMap(std::unique_ptr tp, + const MIPMapFilterOptions &options) + : texelProvider(std::move(tp)), options(options) { + // Initialize EWA filter weights if needed + if (weightLut[0] == 0.) { + for (int i = 0; i < WeightLUTSize; ++i) { + Float alpha = 2; + Float r2 = Float(i) / Float(WeightLUTSize - 1); + weightLut[i] = std::exp(-alpha * r2) - std::exp(-alpha); + } + } +} + +std::unique_ptr MIPMap::CreateFromFile( + const std::string &filename, const MIPMapFilterOptions &options, + WrapMode wrapMode, bool gamma) { + ProfilePhase _(Prof::MIPMapCreation); + + if (HasExtension(filename, "txp")) { + std::unique_ptr tp = + CachedTexelProvider::CreateFromFile(filename, wrapMode); + return std::unique_ptr(new MIPMap(std::move(tp), options)); + } else { + Image image; + if (!Image::Read(filename, &image, gamma)) return nullptr; + + // TODO: make spectrum type configurable, or eliminate... + std::unique_ptr tp(new ImageTexelProvider( + std::move(image), wrapMode, SpectrumType::Reflectance)); + return std::unique_ptr(new MIPMap(std::move(tp), options)); + } +} + +MIPMap::~MIPMap() {} + +int MIPMap::Levels() const { return texelProvider->Levels(); } + +Point2i MIPMap::LevelResolution(int level) const { + return texelProvider->LevelResolution(level); +} + +template +T MIPMap::Lookup(const Point2f &st, Float width) const { + ProfilePhase p(Prof::TexFiltBasic); + // Compute MIPMap level + int nLevels = Levels(); + Float level = nLevels - 1 + Log2(std::max(width, (Float)1e-8)); + + if (level >= Levels() - 1) return Texel(Levels() - 1, {0, 0}); + + int iLevel = std::max(0, int(std::floor(level))); + if (options.filter == FilterFunction::Point) { + ++nPointLookups; + Point2i resolution = LevelResolution(iLevel); + Point2i sti(std::round(st[0] * resolution[0] - 0.5f), + std::round(st[1] * resolution[1] - 0.5f)); + return Texel(iLevel, sti); + } else if (options.filter == FilterFunction::Bilinear) { + ++nBilinearLookups; + return Bilerp(iLevel, st); + } else { + CHECK(options.filter == FilterFunction::Trilinear); + ++nTrilinearLookups; + + if (iLevel == 0) + return Bilerp(0, st); + else { + Float delta = level - iLevel; + CHECK_LE(delta, 1); + return ((1 - delta) * Bilerp(iLevel, st) + + delta * Bilerp(iLevel + 1, st)); + } + } +} + +template +T MIPMap::Lookup(const Point2f &st, Vector2f dst0, Vector2f dst1) const { + if (options.filter != FilterFunction::EWA) { + Float width = std::max(std::max(std::abs(dst0[0]), std::abs(dst0[1])), + std::max(std::abs(dst1[0]), std::abs(dst1[1]))); + return Lookup(st, 2 * width); + } + ++nEWALookups; + ProfilePhase p(Prof::TexFiltEWA); + // Compute ellipse minor and major axes + if (dst0.LengthSquared() < dst1.LengthSquared()) std::swap(dst0, dst1); + Float majorLength = dst0.Length(); + Float minorLength = dst1.Length(); + + // Clamp ellipse eccentricity if too large + if (minorLength * options.maxAnisotropy < majorLength && minorLength > 0) { + Float scale = majorLength / (minorLength * options.maxAnisotropy); + dst1 *= scale; + minorLength *= scale; + } + if (minorLength == 0) return Bilerp(0, st); + + // Choose level of detail for EWA lookup and perform EWA filtering + Float lod = std::max((Float)0, Levels() - (Float)1 + Log2(minorLength)); + int ilod = std::floor(lod); + // TODO: just do return EWA(ilog, st, dst0, dst1); + // TODO: also, when scaling camera ray differentials, just do e.g. + // 1 / std::min(sqrtSamplesPerPixel, 8); + return ((1 - (lod - ilod)) * EWA(ilod, st, dst0, dst1) + + (lod - ilod) * EWA(ilod + 1, st, dst0, dst1)); +} + +template +T MIPMap::Texel(int level, Point2i st) const { + T::unimplemented_function; +} + +template <> +Float MIPMap::Texel(int level, Point2i st) const { + return texelProvider->TexelFloat(level, st); +} + +template <> +Spectrum MIPMap::Texel(int level, Point2i st) const { + return texelProvider->TexelSpectrum(level, st); +} + +template +T MIPMap::Bilerp(int level, Point2f st) const { + T::unimplemented_function; +} + +template <> +Float MIPMap::Bilerp(int level, Point2f st) const { + return texelProvider->BilerpFloat(level, st); +} + +template <> +Spectrum MIPMap::Bilerp(int level, Point2f st) const { + return texelProvider->BilerpSpectrum(level, st); +} + +template +T MIPMap::EWA(int level, Point2f st, Vector2f dst0, Vector2f dst1) const { + if (level >= Levels()) return Texel(Levels() - 1, {0, 0}); + + // Convert EWA coordinates to appropriate scale for level + Point2i levelRes = LevelResolution(level); + st[0] = st[0] * levelRes[0] - 0.5f; + st[1] = st[1] * levelRes[1] - 0.5f; + dst0[0] *= levelRes[0]; + dst0[1] *= levelRes[1]; + dst1[0] *= levelRes[0]; + dst1[1] *= levelRes[1]; + + // Compute ellipse coefficients to bound EWA filter region + Float A = dst0[1] * dst0[1] + dst1[1] * dst1[1] + 1; + Float B = -2 * (dst0[0] * dst0[1] + dst1[0] * dst1[1]); + Float C = dst0[0] * dst0[0] + dst1[0] * dst1[0] + 1; + Float invF = 1 / (A * C - B * B * 0.25f); + A *= invF; + B *= invF; + C *= invF; + + // Compute the ellipse's $(s,t)$ bounding box in texture space + Float det = -B * B + 4 * A * C; + Float invDet = 1 / det; + Float uSqrt = std::sqrt(det * C), vSqrt = std::sqrt(A * det); + int s0 = std::ceil(st[0] - 2 * invDet * uSqrt); + int s1 = std::floor(st[0] + 2 * invDet * uSqrt); + int t0 = std::ceil(st[1] - 2 * invDet * vSqrt); + int t1 = std::floor(st[1] + 2 * invDet * vSqrt); + + // Scan over ellipse bound and compute quadratic equation + T sum{}; + Float sumWts = 0; + for (int it = t0; it <= t1; ++it) { + Float tt = it - st[1]; + for (int is = s0; is <= s1; ++is) { + Float ss = is - st[0]; + // Compute squared radius and filter texel if inside ellipse + Float r2 = A * ss * ss + B * ss * tt + C * tt * tt; + if (r2 < 1) { + int index = + std::min((int)(r2 * WeightLUTSize), WeightLUTSize - 1); + Float weight = weightLut[index]; + sum += weight * Texel(level, {is, it}); + sumWts += weight; + } + } + } + return sum / sumWts; +} + +// Explicit template instantiation.. +template Float MIPMap::Lookup(const Point2f &st, Float width) const; +template Spectrum MIPMap::Lookup(const Point2f &st, Float width) const; +template Float MIPMap::Lookup(const Point2f &st, Vector2f, Vector2f) const; +template Spectrum MIPMap::Lookup(const Point2f &st, Vector2f, Vector2f) const; + +} // namespace pbrt diff --git a/src/core/mipmap.h b/src/core/mipmap.h index 790486cca5..dff4f6809b 100644 --- a/src/core/mipmap.h +++ b/src/core/mipmap.h @@ -40,320 +40,115 @@ // core/mipmap.h* #include "pbrt.h" +#include "image.h" #include "spectrum.h" -#include "texture.h" -#include "stats.h" -#include "parallel.h" +#include "texcache.h" namespace pbrt { -STAT_COUNTER("Texture/EWA lookups", nEWALookups); -STAT_COUNTER("Texture/Trilinear lookups", nTrilerpLookups); -STAT_MEMORY_COUNTER("Memory/Texture MIP maps", mipMapMemory); +enum class FilterFunction { Point, Bilinear, Trilinear, EWA }; + +static bool ParseFilter(const std::string &f, FilterFunction *func) { + if (f == "ewa" || f == "EWA") { + *func = FilterFunction::EWA; + return true; + } else if (f == "trilinear") { + *func = FilterFunction::Trilinear; + return true; + } else if (f == "bilinear") { + *func = FilterFunction::Bilinear; + return true; + } else if (f == "point") { + *func = FilterFunction::Point; + return true; + } else + return false; +} -// MIPMap Helper Declarations -enum class ImageWrap { Repeat, Black, Clamp }; -struct ResampleWeight { - int firstTexel; - Float weight[4]; +struct MIPMapFilterOptions { + FilterFunction filter = FilterFunction::EWA; + Float maxAnisotropy = 8.f; }; -// MIPMap Declarations -template -class MIPMap { +class TexelProvider { public: - // MIPMap Public Methods - MIPMap(const Point2i &resolution, const T *data, bool doTri = false, - Float maxAniso = 8.f, ImageWrap wrapMode = ImageWrap::Repeat); - int Width() const { return resolution[0]; } - int Height() const { return resolution[1]; } - int Levels() const { return pyramid.size(); } - const T &Texel(int level, int s, int t) const; - T Lookup(const Point2f &st, Float width = 0.f) const; - T Lookup(const Point2f &st, Vector2f dstdx, Vector2f dstdy) const; - - private: - // MIPMap Private Methods - std::unique_ptr resampleWeights(int oldRes, int newRes) { - CHECK_GE(newRes, oldRes); - std::unique_ptr wt(new ResampleWeight[newRes]); - Float filterwidth = 2.f; - for (int i = 0; i < newRes; ++i) { - // Compute image resampling weights for _i_th texel - Float center = (i + .5f) * oldRes / newRes; - wt[i].firstTexel = std::floor((center - filterwidth) + 0.5f); - for (int j = 0; j < 4; ++j) { - Float pos = wt[i].firstTexel + j + .5f; - wt[i].weight[j] = Lanczos((pos - center) / filterwidth); - } - - // Normalize filter weights for texel resampling - Float invSumWts = 1 / (wt[i].weight[0] + wt[i].weight[1] + - wt[i].weight[2] + wt[i].weight[3]); - for (int j = 0; j < 4; ++j) wt[i].weight[j] *= invSumWts; - } - return wt; - } - Float clamp(Float v) { return Clamp(v, 0.f, Infinity); } - RGBSpectrum clamp(const RGBSpectrum &v) { return v.Clamp(0.f, Infinity); } - SampledSpectrum clamp(const SampledSpectrum &v) { - return v.Clamp(0.f, Infinity); - } - T triangle(int level, const Point2f &st) const; - T EWA(int level, Point2f st, Vector2f dst0, Vector2f dst1) const; - - // MIPMap Private Data - const bool doTrilinear; - const Float maxAnisotropy; - const ImageWrap wrapMode; - Point2i resolution; - std::vector>> pyramid; - static PBRT_CONSTEXPR int WeightLUTSize = 128; - static Float weightLut[WeightLUTSize]; + virtual ~TexelProvider(); + virtual int Levels() const = 0; + virtual Point2i LevelResolution(int level) const = 0; + virtual Float TexelFloat(int level, Point2i st) const = 0; + virtual Spectrum TexelSpectrum(int level, Point2i st) const = 0; + virtual Float BilerpFloat(int level, Point2f st) const = 0; + virtual Spectrum BilerpSpectrum(int level, Point2f st) const = 0; }; -// MIPMap Method Definitions -template -MIPMap::MIPMap(const Point2i &res, const T *img, bool doTrilinear, - Float maxAnisotropy, ImageWrap wrapMode) - : doTrilinear(doTrilinear), - maxAnisotropy(maxAnisotropy), - wrapMode(wrapMode), - resolution(res) { - ProfilePhase _(Prof::MIPMapCreation); - - std::unique_ptr resampledImage = nullptr; - if (!IsPowerOf2(resolution[0]) || !IsPowerOf2(resolution[1])) { - // Resample image to power-of-two resolution - Point2i resPow2(RoundUpPow2(resolution[0]), RoundUpPow2(resolution[1])); - LOG(INFO) << "Resampling MIPMap from " << resolution << " to " << - resPow2 << ". Ratio= " << (Float(resPow2.x * resPow2.y) / - Float(resolution.x * resolution.y)); - // Resample image in $s$ direction - std::unique_ptr sWeights = - resampleWeights(resolution[0], resPow2[0]); - resampledImage.reset(new T[resPow2[0] * resPow2[1]]); - - // Apply _sWeights_ to zoom in $s$ direction - ParallelFor([&](int t) { - for (int s = 0; s < resPow2[0]; ++s) { - // Compute texel $(s,t)$ in $s$-zoomed image - resampledImage[t * resPow2[0] + s] = 0.f; - for (int j = 0; j < 4; ++j) { - int origS = sWeights[s].firstTexel + j; - if (wrapMode == ImageWrap::Repeat) - origS = Mod(origS, resolution[0]); - else if (wrapMode == ImageWrap::Clamp) - origS = Clamp(origS, 0, resolution[0] - 1); - if (origS >= 0 && origS < (int)resolution[0]) - resampledImage[t * resPow2[0] + s] += - sWeights[s].weight[j] * - img[t * resolution[0] + origS]; - } - } - }, resolution[1], 16); - - // Resample image in $t$ direction - std::unique_ptr tWeights = - resampleWeights(resolution[1], resPow2[1]); - std::vector resampleBufs; - int nThreads = MaxThreadIndex(); - for (int i = 0; i < nThreads; ++i) - resampleBufs.push_back(new T[resPow2[1]]); - ParallelFor([&](int s) { - T *workData = resampleBufs[ThreadIndex]; - for (int t = 0; t < resPow2[1]; ++t) { - workData[t] = 0.f; - for (int j = 0; j < 4; ++j) { - int offset = tWeights[t].firstTexel + j; - if (wrapMode == ImageWrap::Repeat) - offset = Mod(offset, resolution[1]); - else if (wrapMode == ImageWrap::Clamp) - offset = Clamp(offset, 0, (int)resolution[1] - 1); - if (offset >= 0 && offset < (int)resolution[1]) - workData[t] += tWeights[t].weight[j] * - resampledImage[offset * resPow2[0] + s]; - } - } - for (int t = 0; t < resPow2[1]; ++t) - resampledImage[t * resPow2[0] + s] = clamp(workData[t]); - }, resPow2[0], 32); - for (auto ptr : resampleBufs) delete[] ptr; - resolution = resPow2; - } - // Initialize levels of MIPMap from image - int nLevels = 1 + Log2Int(std::max(resolution[0], resolution[1])); - pyramid.resize(nLevels); - - // Initialize most detailed level of MIPMap - pyramid[0].reset( - new BlockedArray(resolution[0], resolution[1], - resampledImage ? resampledImage.get() : img)); - for (int i = 1; i < nLevels; ++i) { - // Initialize $i$th MIPMap level from $i-1$st level - int sRes = std::max(1, pyramid[i - 1]->uSize() / 2); - int tRes = std::max(1, pyramid[i - 1]->vSize() / 2); - pyramid[i].reset(new BlockedArray(sRes, tRes)); - - // Filter four texels from finer level of pyramid - ParallelFor([&](int t) { - for (int s = 0; s < sRes; ++s) - (*pyramid[i])(s, t) = - .25f * (Texel(i - 1, 2 * s, 2 * t) + - Texel(i - 1, 2 * s + 1, 2 * t) + - Texel(i - 1, 2 * s, 2 * t + 1) + - Texel(i - 1, 2 * s + 1, 2 * t + 1)); - }, tRes, 16); - } - - // Initialize EWA filter weights if needed - if (weightLut[0] == 0.) { - for (int i = 0; i < WeightLUTSize; ++i) { - Float alpha = 2; - Float r2 = Float(i) / Float(WeightLUTSize - 1); - weightLut[i] = std::exp(-alpha * r2) - std::exp(-alpha); - } - } - mipMapMemory += (4 * resolution[0] * resolution[1] * sizeof(T)) / 3; -} - -template -const T &MIPMap::Texel(int level, int s, int t) const { - CHECK_LT(level, pyramid.size()); - const BlockedArray &l = *pyramid[level]; - // Compute texel $(s,t)$ accounting for boundary conditions - switch (wrapMode) { - case ImageWrap::Repeat: - s = Mod(s, l.uSize()); - t = Mod(t, l.vSize()); - break; - case ImageWrap::Clamp: - s = Clamp(s, 0, l.uSize() - 1); - t = Clamp(t, 0, l.vSize() - 1); - break; - case ImageWrap::Black: { - static const T black = 0.f; - if (s < 0 || s >= (int)l.uSize() || t < 0 || t >= (int)l.vSize()) - return black; - break; - } - } - return l(s, t); -} - -template -T MIPMap::Lookup(const Point2f &st, Float width) const { - ++nTrilerpLookups; - ProfilePhase p(Prof::TexFiltTrilerp); - // Compute MIPMap level for trilinear filtering - Float level = Levels() - 1 + Log2(std::max(width, (Float)1e-8)); +class ImageTexelProvider : public TexelProvider { + public: + ImageTexelProvider(Image image, WrapMode wrapMode, + SpectrumType spectrumType); - // Perform trilinear interpolation at appropriate MIPMap level - if (level < 0) - return triangle(0, st); - else if (level >= Levels() - 1) - return Texel(Levels() - 1, 0, 0); - else { - int iLevel = std::floor(level); - Float delta = level - iLevel; - return Lerp(delta, triangle(iLevel, st), triangle(iLevel + 1, st)); - } -} + int Levels() const override; + Point2i LevelResolution(int level) const override; + Float TexelFloat(int level, Point2i st) const override; + Spectrum TexelSpectrum(int level, Point2i st) const override; + Float BilerpFloat(int level, Point2f st) const override; + Spectrum BilerpSpectrum(int level, Point2f st) const override; -template -T MIPMap::triangle(int level, const Point2f &st) const { - level = Clamp(level, 0, Levels() - 1); - Float s = st[0] * pyramid[level]->uSize() - 0.5f; - Float t = st[1] * pyramid[level]->vSize() - 0.5f; - int s0 = std::floor(s), t0 = std::floor(t); - Float ds = s - s0, dt = t - t0; - return (1 - ds) * (1 - dt) * Texel(level, s0, t0) + - (1 - ds) * dt * Texel(level, s0, t0 + 1) + - ds * (1 - dt) * Texel(level, s0 + 1, t0) + - ds * dt * Texel(level, s0 + 1, t0 + 1); -} + private: + std::vector pyramid; + const WrapMode wrapMode; + const SpectrumType spectrumType; +}; -template -T MIPMap::Lookup(const Point2f &st, Vector2f dst0, Vector2f dst1) const { - if (doTrilinear) { - Float width = std::max(std::max(std::abs(dst0[0]), std::abs(dst0[1])), - std::max(std::abs(dst1[0]), std::abs(dst1[1]))); - return Lookup(st, 2 * width); - } - ++nEWALookups; - ProfilePhase p(Prof::TexFiltEWA); - // Compute ellipse minor and major axes - if (dst0.LengthSquared() < dst1.LengthSquared()) std::swap(dst0, dst1); - Float majorLength = dst0.Length(); - Float minorLength = dst1.Length(); +class CachedTexelProvider : public TexelProvider { + public: + static std::unique_ptr CreateFromFile( + const std::string &filename, WrapMode wrapMode); + ~CachedTexelProvider(); - // Clamp ellipse eccentricity if too large - if (minorLength * maxAnisotropy < majorLength && minorLength > 0) { - Float scale = majorLength / (minorLength * maxAnisotropy); - dst1 *= scale; - minorLength *= scale; - } - if (minorLength == 0) return triangle(0, st); + int Levels() const override; + Point2i LevelResolution(int level) const override; + Float TexelFloat(int level, Point2i st) const override; + Spectrum TexelSpectrum(int level, Point2i st) const override; + Float BilerpFloat(int level, Point2f st) const override; + Spectrum BilerpSpectrum(int level, Point2f st) const override; - // Choose level of detail for EWA lookup and perform EWA filtering - Float lod = std::max((Float)0, Levels() - (Float)1 + Log2(minorLength)); - int ilod = std::floor(lod); - return Lerp(lod - ilod, EWA(ilod, st, dst0, dst1), - EWA(ilod + 1, st, dst0, dst1)); -} + static TextureCache *textureCache; -template -T MIPMap::EWA(int level, Point2f st, Vector2f dst0, Vector2f dst1) const { - if (level >= Levels()) return Texel(Levels() - 1, 0, 0); - // Convert EWA coordinates to appropriate scale for level - st[0] = st[0] * pyramid[level]->uSize() - 0.5f; - st[1] = st[1] * pyramid[level]->vSize() - 0.5f; - dst0[0] *= pyramid[level]->uSize(); - dst0[1] *= pyramid[level]->vSize(); - dst1[0] *= pyramid[level]->uSize(); - dst1[1] *= pyramid[level]->vSize(); + private: + CachedTexelProvider(const std::string &filename, WrapMode wrapMode, int id); - // Compute ellipse coefficients to bound EWA filter region - Float A = dst0[1] * dst0[1] + dst1[1] * dst1[1] + 1; - Float B = -2 * (dst0[0] * dst0[1] + dst1[0] * dst1[1]); - Float C = dst0[0] * dst0[0] + dst1[0] * dst1[0] + 1; - Float invF = 1 / (A * C - B * B * 0.25f); - A *= invF; - B *= invF; - C *= invF; + const std::string filename; + const WrapMode wrapMode; + const int id; +}; - // Compute the ellipse's $(s,t)$ bounding box in texture space - Float det = -B * B + 4 * A * C; - Float invDet = 1 / det; - Float uSqrt = std::sqrt(det * C), vSqrt = std::sqrt(A * det); - int s0 = std::ceil(st[0] - 2 * invDet * uSqrt); - int s1 = std::floor(st[0] + 2 * invDet * uSqrt); - int t0 = std::ceil(st[1] - 2 * invDet * vSqrt); - int t1 = std::floor(st[1] + 2 * invDet * vSqrt); +class MIPMap { + public: + MIPMap(std::unique_ptr tp, + const MIPMapFilterOptions &options); + static std::unique_ptr CreateFromFile( + const std::string &filename, const MIPMapFilterOptions &options, + WrapMode wrapMode, bool gamma); + ~MIPMap(); + + template + T Lookup(const Point2f &st, Float width = 0.f) const; + template + T Lookup(const Point2f &st, Vector2f dstdx, Vector2f dstdy) const; - // Scan over ellipse bound and compute quadratic equation - T sum(0.f); - Float sumWts = 0; - for (int it = t0; it <= t1; ++it) { - Float tt = it - st[1]; - for (int is = s0; is <= s1; ++is) { - Float ss = is - st[0]; - // Compute squared radius and filter texel if inside ellipse - Float r2 = A * ss * ss + B * ss * tt + C * tt * tt; - if (r2 < 1) { - int index = - std::min((int)(r2 * WeightLUTSize), WeightLUTSize - 1); - Float weight = weightLut[index]; - sum += Texel(level, is, it) * weight; - sumWts += weight; - } - } - } - return sum / sumWts; -} + private: + int Levels() const; + Point2i LevelResolution(int level) const; + template + T Texel(int level, Point2i st) const; + template + T Bilerp(int level, Point2f st) const; + template + T EWA(int level, Point2f st, Vector2f dst0, Vector2f dst1) const; -template -Float MIPMap::weightLut[WeightLUTSize]; + std::unique_ptr texelProvider; + MIPMapFilterOptions options; +}; } // namespace pbrt diff --git a/src/core/parallel.cpp b/src/core/parallel.cpp index af1e8cc6b7..0a76e45340 100644 --- a/src/core/parallel.cpp +++ b/src/core/parallel.cpp @@ -183,7 +183,8 @@ static void workerThreadFunc(int tIndex, std::shared_ptr barrier) { // Parallel Definitions void ParallelFor(std::function func, int64_t count, int chunkSize) { - CHECK(threads.size() > 0 || MaxThreadIndex() == 1); + if (threads.size() == 0 && MaxThreadIndex() > 1) + LOG(WARNING) << "Threads not launched; ParallelFor will run serially"; // Run iterations immediately if not using threads or if _count_ is small if (threads.empty() || count < chunkSize) { @@ -245,7 +246,8 @@ int MaxThreadIndex() { } void ParallelFor2D(std::function func, const Point2i &count) { - CHECK(threads.size() > 0 || MaxThreadIndex() == 1); + if (threads.size() == 0 && MaxThreadIndex() > 1) + LOG(WARNING) << "Threads not launched; ParallelFor will run serially"; if (threads.empty() || count.x * count.y <= 1) { for (int y = 0; y < count.y; ++y) diff --git a/src/core/parallel.h b/src/core/parallel.h index 1a7557e0ff..60616328df 100644 --- a/src/core/parallel.h +++ b/src/core/parallel.h @@ -41,6 +41,7 @@ // core/parallel.h* #include "pbrt.h" #include "geometry.h" +#include "stats.h" #include #include #include diff --git a/src/core/pbrt.h b/src/core/pbrt.h index 3e01b7e138..6f693417d9 100644 --- a/src/core/pbrt.h +++ b/src/core/pbrt.h @@ -152,6 +152,8 @@ template struct ParamSetItem; struct Options { int nThreads = 0; + int texCacheMB = 96; + int texReadMinMS = 0; bool quickRender = false; bool quiet = false; bool cat = false, toPly = false; diff --git a/src/core/spectrum.h b/src/core/spectrum.h index 35107cf7f9..ce19f44e30 100644 --- a/src/core/spectrum.h +++ b/src/core/spectrum.h @@ -251,6 +251,13 @@ class CoefficientSpectrum { m = std::max(m, c[i]); return m; } + Float Average() const { + Float sum = 0; + for (int i = 0; i < nSpectrumSamples; ++i) + sum += c[i]; + return sum / nSpectrumSamples; + } + bool HasNaNs() const { for (int i = 0; i < nSpectrumSamples; ++i) if (std::isnan(c[i])) return true; diff --git a/src/core/stats.h b/src/core/stats.h index f330711e88..ea0cd5bf5a 100644 --- a/src/core/stats.h +++ b/src/core/stats.h @@ -179,8 +179,12 @@ enum class Prof { AddFilmSample, StartPixel, GetSample, - TexFiltTrilerp, + TexFiltBasic, TexFiltEWA, + TexCacheGetTexel, + TexCacheGetTile, + TexCacheReadTile, + TexCacheFree, NumProfCategories }; @@ -233,8 +237,12 @@ static const char *ProfNames[] = { "Film::AddSample()", "Sampler::StartPixelSample()", "Sampler::GetSample[12]D()", - "MIPMap::Lookup() (trilinear)", + "MIPMap::Lookup() (basic)", "MIPMap::Lookup() (EWA)", + "TextureCache::Texel()", + "TextureCache::GetTile()", + "TextureCache::ReadTile()", + "TextureCache::FreeMemory()", }; static_assert((int)Prof::NumProfCategories == diff --git a/src/core/texcache.cpp b/src/core/texcache.cpp new file mode 100644 index 0000000000..e397098c2e --- /dev/null +++ b/src/core/texcache.cpp @@ -0,0 +1,908 @@ + +/* + pbrt source code is Copyright(c) 1998-2016 + Matt Pharr, Greg Humphreys, and Wenzel Jakob. + + This file is part of pbrt. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are + met: + + - Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + - Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS + IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + */ + +// core/texcache.cpp* +#include +#if defined(PBRT_IS_LINUX) || defined(PBRT_IS_OSX) +#include +#include +#endif +#ifdef PBRT_IS_MSVC +#include +#include +#endif + +#include +#include +#include +#include "parallel.h" +#include "stats.h" +#include "texcache.h" + +namespace pbrt { + +static std::chrono::duration minReadDuration(0); + +STAT_RATIO("Texture/Tiles read", tilesRead, totalTiles); +STAT_COUNTER("Texture/File open calls", fileOpens); +STAT_PERCENT("Texture/File cache hits", fileCacheHits, fileCacheLookups); +STAT_PERCENT("Texture/Tile cache hits", tileCacheHits, tileCacheLookups); + +STAT_MEMORY_COUNTER("Texture/Texels read from disk", texelReadBytes); +STAT_FLOAT_DISTRIBUTION("Texture/Hash table load", hashLoad); +STAT_INT_DISTRIBUTION("Texture/Tile hash probes", tileHashProbes); +STAT_INT_DISTRIBUTION("Texture/MIP level accessed", mipLevelAccessed); +STAT_INT_DISTRIBUTION("Texture/Orphaned texture tiles", orphanedTiles); +STAT_COUNTER("Texture/Tile free passes", freePasses); +STAT_MEMORY_COUNTER("Texture/Top-levels in memory", topLevelBytes); +STAT_FLOAT_DISTRIBUTION("Texture/Tile read time (ms)", tileReadTimeMS); +STAT_COUNTER("Texture/Total read time (ms)", totalTileReadMS); + +// Texture Cache Utility Functions +inline Point2i nTiles(Point2i resolution, int logTileSize) { + int tileSize = 1 << logTileSize; + return {(resolution[0] + tileSize - 1) >> logTileSize, + (resolution[1] + tileSize - 1) >> logTileSize}; +} + +// http://zimbry.blogspot.ch/2011/09/better-bit-mixing-improving-on.html +inline uint64_t MixBits(uint64_t v) { + v ^= (v >> 31); + v *= 0x7fb5d329728ea185; + v ^= (v >> 27); + v *= 0x81dadef4bc2dd44d; + v ^= (v >> 33); + return v; +} + +// TileId Declarations +struct TileId { + // TileId Public Methods + TileId() : TileId(0, -1, {0, 0}) {} + TileId(int texId32, int level32, Point2i p32) { + CHECK(texId32 >= 0 && texId32 < 1 << 16); + CHECK(level32 >= -1 && level32 < 1 << 15); + CHECK(p32[0] >= 0 && p32[0] < 1 << 16); + CHECK(p32[1] >= 0 && p32[1] < 1 << 16); + bits = texId32; + bits |= (uint64_t(level32) & 0xffff) << 16; + bits |= uint64_t(p32[0]) << 32; + bits |= uint64_t(p32[1]) << 48; + } + int texId() const { return bits & 0xffff; } + int level() const { return (bits >> 16) & 0xffff; } + Point2i p() const { + return Point2i((bits >> 32) & 0xffff, (bits >> 48) & 0xffff); + } + size_t hash() const { return MixBits(bits); } + bool operator==(const TileId &t) const { return t.bits == bits; } + bool operator<(const TileId &t) const { return bits < t.bits; } + friend inline std::ostream &operator<<(std::ostream &os, + const TileId &tid) { + return os << StringPrintf("TileId { texId:%d level:%d p:%d, %d }", + tid.texId(), tid.level(), tid.p()[0], + tid.p()[1]); + } + + private: + uint64_t bits; +}; + +// TextureTile Declarations +struct TextureTile { + // TextureTile Public Methods + void Clear() { + tileId = TileId(); + marked.store(false, std::memory_order_relaxed); + } + + // TextureTile Public Data + TileId tileId; + char *texels = nullptr; + mutable std::atomic marked; +}; + +// TiledImagePyramid Method Definitions +bool TiledImagePyramid::Create(std::vector images, + const std::string &filename, WrapMode wrapMode, + int tileSize, int topLevelsBytes) { + FILE *f = fopen(filename.c_str(), "wb"); + if (!f) { + Error("%s: %s", filename.c_str(), strerror(errno)); + return false; + } + // Compute tile size and allocate temporary buffer for tiles + PixelFormat format = images[0].format; + // CHECK_LE(tileSize * tileSize * TexelBytes(format), TileAllocSize); + CHECK(IsPowerOf2(tileSize)); + int logTileSize = Log2Int(tileSize); + int bufSize = tileSize * tileSize * TexelBytes(format); + bufSize = (bufSize + TileDiskAlignment - 1) & ~(TileDiskAlignment - 1); + std::unique_ptr buf(new char[bufSize]); + int64_t filePos; + int firstInMemoryLevel; + // Write tiled texture file header + auto writeInt = [f](int32_t v) { + return fwrite(&v, sizeof(int), 1, f) == 1; + }; + if (!writeInt(tiledFileMagic) || !writeInt(int(format)) || + !writeInt(int(wrapMode)) || !writeInt(logTileSize) || + !writeInt(images.size())) + goto fail; + for (size_t level = 0; level < images.size(); ++level) + if (!writeInt(images[level].resolution[0]) || + !writeInt(images[level].resolution[1])) + goto fail; + + // Write packed top levels of the image pyramid + + // Determine how many top levels to store packed + firstInMemoryLevel = images.size(); + for (int level = images.size() - 1; level >= 0; --level) { + int levelBytes = images[level].resolution[0] * + images[level].resolution[1] * TexelBytes(format); + topLevelsBytes -= levelBytes; + if (topLevelsBytes < 0) break; + --firstInMemoryLevel; + } + + // Write out texels for the packed top levels of the pyramid + if (!writeInt(firstInMemoryLevel)) goto fail; + for (int level = firstInMemoryLevel; level < images.size(); ++level) { + int totalPixels = + images[level].resolution[0] * images[level].resolution[1]; + if (fwrite(images[level].RawPointer({0, 0}), totalPixels, + TexelBytes(format), f) != TexelBytes(format)) + goto fail; + } + + // Advance to the required on-disk alignment before writing tiles + filePos = ftell(f); + filePos = (filePos + TileDiskAlignment - 1) & ~(TileDiskAlignment - 1); + if (fseek(f, filePos, SEEK_SET) != 0) goto fail; + + // Write texture tiles for remaining levels of the pyramid + for (size_t level = 0; level < firstInMemoryLevel; ++level) { + const Image &image = images[level]; + CHECK(format == image.format); + Point2i resolution = image.resolution; + Bounds2i tileBounds({0, 0}, nTiles(resolution, logTileSize)); + for (Point2i tile : tileBounds) { + // Write texels for _tile_ to disk + memset(buf.get(), 0, bufSize); + + // Compute image-space bounds for this tile + Point2i pMin(tile[0] * tileSize, tile[1] * tileSize); + Point2i pMax(pMin[0] + tileSize, pMin[1] + tileSize); + pMax = Min(pMax, resolution); + + // Fill the tile with texel values from the source texture + for (int y = pMin.y; y < pMax.y; ++y) + for (int x = pMin.x; x < pMax.x; ++x) { + int outOffset = TexelBytes(format) * + ((y - pMin.y) * tileSize + (x - pMin.x)); + memcpy(&buf[outOffset], image.RawPointer({x, y}), + TexelBytes(format)); + } + if (fwrite(buf.get(), 1, bufSize, f) != bufSize) goto fail; + } + } + + // Close tiled texture and return + if (fclose(f) != 0) { + Error("%s: %s", filename.c_str(), strerror(errno)); + return false; + } + return true; +fail: + // Handle error case for tiled texture creation + Error("%s: %s", filename.c_str(), strerror(errno)); + fclose(f); + return false; +} + +bool TiledImagePyramid::Read(const std::string &filename, + TiledImagePyramid *tex) { + FILE *file = fopen(filename.c_str(), "rb"); + if (!file) { + Error("%s: %s", filename.c_str(), strerror(errno)); + return false; + } + + auto readInt = [&](int32_t *v) { + if (fread(v, sizeof(*v), 1, file) != 1) { + Error("%s: %s", filename.c_str(), strerror(errno)); + fclose(file); + return false; + } + // TODO: handle endian issues + return true; + }; + + int32_t magic, format, wrap, nLevels, logTileSize; + if (!readInt(&magic) || !readInt(&format) || !readInt(&wrap) || + !readInt(&logTileSize) || !readInt(&nLevels)) { + Error("%s: %s", filename.c_str(), strerror(errno)); + fclose(file); + return false; + } + + if (magic != tiledFileMagic) { + Error("%s: doesn't appear to be a valid txp file", filename.c_str()); + LOG(ERROR) << StringPrintf("Read magic %x, expected %x", magic, + tiledFileMagic); + fclose(file); + return false; + } + + tex->filename = filename; + tex->pixelFormat = PixelFormat(format); + tex->wrapMode = WrapMode(wrap); + tex->logTileSize = logTileSize; + for (int i = 0; i < nLevels; ++i) { + int32_t res[2]; + if (!readInt(&res[0]) || !readInt(&res[1])) { + Error("%s: %s", filename.c_str(), strerror(errno)); + fclose(file); + return false; + } + tex->levelResolution.push_back(Point2i(res[0], res[1])); + } + + int32_t firstInMemoryLevel; + if (!readInt(&firstInMemoryLevel)) { + Error("%s: %s", filename.c_str(), strerror(errno)); + fclose(file); + return false; + } + tex->firstInMemoryLevel = firstInMemoryLevel; + for (int level = firstInMemoryLevel; level < nLevels; ++level) { + int levelBytes = tex->levelResolution[level][0] * + tex->levelResolution[level][1] * + TexelBytes(tex->pixelFormat); + topLevelBytes += levelBytes; + // TODO: single allocation for all of these + std::unique_ptr buf(new char[levelBytes]); + // TODO: endian + if (!fread(buf.get(), levelBytes, 1, file)) { + Error("%s: %s", filename.c_str(), strerror(errno)); + fclose(file); + return false; + } + tex->inMemoryLevels.push_back(std::move(buf)); + } + + int64_t levelPos = ftell(file); + levelPos = (levelPos + TileDiskAlignment - 1) & ~(TileDiskAlignment - 1); + CHECK_EQ(0, fseek(file, levelPos, SEEK_SET)); + + for (int i = 0; i < firstInMemoryLevel; ++i) { + tex->levelOffset.push_back(levelPos); + Point2i nt = nTiles(tex->levelResolution[i], logTileSize); + levelPos += nt[0] * nt[1] * tex->TileDiskBytes(); + totalTiles += nt[0] * nt[1]; + } + + fclose(file); + return true; +} + +// TileHashTable Declarations +class TileHashTable { + public: + // TileHashTable Public Methods + TileHashTable(size_t size); + void ReportStats(); + int CountOrphaned(); + const char *Lookup(TileId tileId) const; + void Insert(TextureTile *entry); + void MarkEntries(); + void CopyActive(TileHashTable *dest); + void ReclaimUncopied(std::vector *returnedEntries); + + private: + // TileHashTable Private Data + std::unique_ptr[]> table; + const size_t size; + std::vector hashEntryCopied; +}; + +// TileHashTable Method Definitions +TileHashTable::TileHashTable(size_t size) : size(size) { + table.reset(new std::atomic[ size ]); + for (size_t i = 0; i < size; ++i) table[i] = nullptr; + hashEntryCopied.resize(size); +} + +void TileHashTable::ReportStats() { + int active = 0; + for (size_t i = 0; i < size; ++i) + if (table[i].load(std::memory_order_relaxed) != nullptr) ++active; + ReportValue(hashLoad, float(active) / float(size)); +} + +int TileHashTable::CountOrphaned() { + int nOrphaned = 0; + for (size_t i = 0; i < size; ++i) { + TextureTile *entry = table[i].load(std::memory_order_relaxed); + if (entry && entry->marked.load(std::memory_order_relaxed) == false && + hashEntryCopied[i] == false) + ++nOrphaned; + } + return nOrphaned; +} + +const char *TileHashTable::Lookup(TileId tileId) const { + int hashOffset = tileId.hash() % size; + int step = 1; + int probes = 1; + for (;;) { + CHECK(hashOffset >= 0 && hashOffset < size); + const TextureTile *entry = + table[hashOffset].load(std::memory_order_acquire); + // Process _entry_ for hash table lookup + if (entry == nullptr) + return nullptr; + else if (entry->tileId == tileId) { + // Update _entry_'s _marked_ field after cache hit + if (entry->marked.load(std::memory_order_relaxed)) + entry->marked.store(false, std::memory_order_relaxed); + ReportValue(tileHashProbes, probes); + return entry->texels; + } else { + hashOffset += step * step; + ++step; + if (hashOffset >= size) hashOffset %= size; + ++probes; + } + } +} + +void TileHashTable::Insert(TextureTile *tile) { + int hashOffset = tile->tileId.hash() % size; + int probes = 1; + int step = 1; + for (;;) { + // Attempt to insert _tile_ at _hashOffset_ + CHECK(hashOffset >= 0 && hashOffset < size); + TextureTile *cur = nullptr; + if (table[hashOffset].compare_exchange_weak( + cur, tile, std::memory_order_release)) { + ReportValue(tileHashProbes, probes); + return; + } + + // Handle compare--exchange failure for hash table insertion + if (cur != nullptr) { + hashOffset += step * step; + ++step; + if (hashOffset >= size) hashOffset %= size; + ++probes; + CHECK_LT(probes, 100); + } + } +} + +void TileHashTable::MarkEntries() { + for (size_t i = 0; i < size; ++i) { + TextureTile *entry = table[i].load(std::memory_order_acquire); + if (entry) entry->marked.store(true, std::memory_order_relaxed); + } +} + +void TileHashTable::CopyActive(TileHashTable *dest) { + int nCopied = 0, nActive = 0; + // Insert unmarked entries from hash table to _dest_ + for (size_t i = 0; i < size; ++i) { + hashEntryCopied[i] = false; + if (TextureTile *entry = table[i].load(std::memory_order_acquire)) { + // Add _entry_ to _dest_ if unmarked + ++nActive; + if (entry->marked.load(std::memory_order_relaxed) == false) { + hashEntryCopied[i] = true; + ++nCopied; + dest->Insert(entry); + } + } + } + ReportValue(hashLoad, float(nActive) / float(size)); + LOG(INFO) << "Copied " << nCopied << " / " << nActive + << " to dest TileHashTable"; + + // Handle case of all entries copied to _freeHashTable_ + if (nCopied == nActive) { + LOG(WARNING) << "No entries were marked; everything was copied. " + "Freeing arbitrarily."; + for (size_t i = 0; i < dest->size; ++i) + dest->table[i].store(nullptr, std::memory_order_relaxed); + int copyCounter = 0; + const int copyRate = 5; + for (size_t i = 0; i < size; ++i) { + TextureTile *entry = table[i].load(std::memory_order_acquire); + if (entry) { + // Either copy _entry_ to _dest_ or free it + hashEntryCopied[i] = copyCounter++ < copyRate; + if (hashEntryCopied[i]) + dest->Insert(entry); + else + copyCounter = 0; + } + } + } +} + +void TileHashTable::ReclaimUncopied(std::vector *returned) { + for (size_t i = 0; i < size; ++i) { + if (TextureTile *entry = table[i].load(std::memory_order_relaxed)) { + if (!hashEntryCopied[i]) returned->push_back(entry); + table[i].store(nullptr, std::memory_order_relaxed); + } + } +} + +// FdCache Method Definitions +FdCache::FdCache(int fdsSpared) { + // Preallocate _FdEntry_s and initialize free list + + // Determine maximum number of file descriptors, _maxFds_ to use for + // textures +#if defined(PBRT_IS_LINUX) || defined(PBRT_IS_OSX) + struct rlimit rlim; + CHECK_EQ(0, getrlimit(RLIMIT_NOFILE, &rlim)) << strerror(errno); + LOG(INFO) << "Current limit open files " << rlim.rlim_cur << ", max " + << rlim.rlim_max; +#ifdef PBRT_IS_OSX + LOG(INFO) << "Open max " << OPEN_MAX; + rlim.rlim_cur = OPEN_MAX; +#else + if (rlim.rlim_max != RLIM_INFINITY) rlim.rlim_cur = rlim.rlim_max; +#endif + CHECK_EQ(0, setrlimit(RLIMIT_NOFILE, &rlim)) << strerror(errno); + LOG(INFO) << "Max open files now = " << rlim.rlim_cur; + int maxFds = rlim.rlim_cur - fdsSpared; +#else + // TODO: figure out the right thing to do here, especially for windows. + int maxFds = 500; +#endif + allocPtr.reset(new FdEntry[maxFds]); + for (int i = 0; i < maxFds; ++i) { + FdEntry *entry = &allocPtr[i]; + entry->next = freeList; + freeList = entry; + } + + // Allocate hash table for _FdCache_ + int nBuckets = RoundUpPow2(maxFds / 32); + LOG(INFO) << "Allocating " << nBuckets + << " buckets in hash table for up to " << maxFds + << " file descriptors."; + logBuckets = Log2Int(nBuckets); + hashTable.resize(nBuckets); +} + +FdCache::~FdCache() { + for (size_t i = 0; i < hashTable.size(); ++i) { + FdEntry *entry = hashTable[i]; + while (entry) { +#ifdef PBRT_IS_WINDOWS + CHECK_EQ(0, _close(entry->fd)) << strerror(errno); +#else + CHECK_EQ(0, close(entry->fd)) << strerror(errno); +#endif + entry = entry->next; + } + } +} + +FdEntry *FdCache::Lookup(int fileId, const std::string &filename) { + int hash = MixBits(fileId) & ((1 << logBuckets) - 1); + CHECK(hash >= 0 && hash < hashTable.size()); + ++fileCacheLookups; + mutex.lock(); + // Return fd for _fileId_ from hash table if available + FdEntry *prev = nullptr; + FdEntry *cur = hashTable[hash]; + while (cur) { + if (cur->fileId == fileId && !cur->inUse) { + // Return available _FdEntry_ for _fileId_ + ++fileCacheHits; + cur->inUse = true; + + // Move _cur_ to head of list + if (prev != nullptr) { + prev->next = cur->next; + cur->next = hashTable[hash]; + hashTable[hash] = cur; + } + mutex.unlock(); + return cur; + } + prev = cur; + cur = cur->next; + } + + // Find a _FdEntry_ from free list or recycle one + FdEntry *entry = nullptr; + if (freeList != nullptr) { + entry = freeList; + freeList = freeList->next; + } else { + // Recycle a not-currently-used entry in the fd hash table + for (; entry == nullptr; ++nextVictim) { + if (nextVictim == hashTable.size()) nextVictim = 0; + FdEntry *cur = hashTable[nextVictim], *prev = nullptr; + FdEntry *lastAvailable = nullptr, *lastAvailablePrev = nullptr; + // Find last entry in list that's not currently in use + while (cur) { + if (!cur->inUse) { + lastAvailable = cur; + lastAvailablePrev = prev; + } + prev = cur; + cur = cur->next; + } + if (lastAvailable) { + // Remove entry from list and initialize _entry_ + entry = lastAvailable; + if (lastAvailablePrev) + lastAvailablePrev->next = lastAvailable->next; + else { + CHECK(hashTable[nextVictim] == lastAvailable); + hashTable[nextVictim] = lastAvailable->next; + } + ++nextVictim; + } + } + } + + // Add _entry_ to head of list + entry->next = hashTable[hash]; + hashTable[hash] = entry; + entry->inUse = true; + mutex.unlock(); + // Open file to get fd for _filename_ + if (entry->fd >= 0) { + LOG(INFO) << "Closing fd " << entry->fd; +#ifdef PBRT_IS_WINDOWS + CHECK_EQ(0, _close(entry->fd)) << strerror(errno); +#else + CHECK_EQ(0, close(entry->fd)) << strerror(errno); +#endif + } + entry->fileId = fileId; + LOG(INFO) << "Opening file for " << filename; + ++fileOpens; +#ifdef PBRT_IS_WINDOWS + entry->fd = _open(filename.c_str(), _O_RDONLY); +#else + entry->fd = open(filename.c_str(), O_RDONLY); +#endif + CHECK_NE(entry->fd, -1) << "Couldn't open " << filename << ", " + << strerror(errno) << ". Try \"ulimit -n 65536\"."; + return entry; +} + +void FdCache::Return(FdEntry *entry) { + CHECK(entry->inUse); + std::lock_guard lock(mutex); + entry->inUse = false; +} + +// TextureCache Method Definitions +TextureCache::TextureCache() { + minReadDuration = + std::chrono::duration(PbrtOptions.texReadMinMS); + LOG(INFO) << "Min tex read duration " << minReadDuration.count() << "ms"; + // Allocate texture tiles and initialize free list + size_t maxTextureBytes = size_t(PbrtOptions.texCacheMB) * 1024 * 1024; + size_t nTiles = maxTextureBytes / TileAllocSize; + + // Allocate tile memory for texture cache + tileMemAlloc.reset(new char[nTiles * TileAllocSize]); + char *tilePtr = tileMemAlloc.get(); + LOG(INFO) << "Allocating " << nTiles << " TextureTile objects"; + + // Allocate _TextureTile_s and initialize free list + allTilesAlloc.reset(new TextureTile[nTiles]); + for (int i = 0; i < nTiles; ++i) { + allTilesAlloc[i].texels = tilePtr + i * TileAllocSize; + freeTiles.push_back(&allTilesAlloc[i]); + } + + // Allocate hash tables for texture tiles + int hashSize = 8 * nTiles; + hashTable = new TileHashTable(hashSize); + freeHashTable = new TileHashTable(hashSize); + + // Initialize _markFreeCapacity_ + markFreeCapacity = 1 + nTiles / 8; + + // Allocate _threadActiveFlags_ + threadActiveFlags = std::vector(MaxThreadIndex()); + if (minReadDuration > std::chrono::duration::zero()) + LOG(WARNING) + << "Will sleep to ensure tile I/O takes at least " + << std::chrono::duration(minReadDuration).count() + << "ms"; +} + +TextureCache::~TextureCache() { + hashTable.load()->ReportStats(); + delete hashTable.load(); + delete freeHashTable; +} + +int TextureCache::TileSize(PixelFormat format) { + switch (format) { + case PixelFormat::SY8: + case PixelFormat::Y8: + return 64; + case PixelFormat::RGB8: + case PixelFormat::SRGB8: + return 64; + case PixelFormat::Y16: + return 64; + case PixelFormat::RGB16: + return 32; + case PixelFormat::Y32: + return 32; + case PixelFormat::RGB32: + return 16; + default: + LOG(FATAL) << "Unhandled pixel format"; + } +} + +int TextureCache::AddTexture(const std::string &filename) { + // Return preexisting id if texture has already been added + auto iter = std::find_if(textures.begin(), textures.end(), + [&filename](const TiledImagePyramid &tex) { + return tex.filename == filename; + }); + if (iter != textures.end()) return iter - textures.begin(); + TiledImagePyramid tex; + if (!TiledImagePyramid::Read(filename, &tex)) return -1; + textures.push_back(std::move(tex)); + return textures.size() - 1; +} + +void TextureCache::PreloadTexture(int texId) { + CHECK(texId >= 0 && texId < textures.size()); + const TiledImagePyramid &tex = textures[texId]; + int nLevels = tex.levelResolution.size(); + int tileSize = 1 << tex.logTileSize; + + for (int level = 0; level < nLevels; ++level) { + Point2i res = tex.levelResolution[level]; + for (int y = 0; y < res.y; y += tileSize) + for (int x = 0; x < res.x; x += tileSize) { + (void)GetTile(TileId(texId, level, tex.TileIndex({x, y}))); + } + } +} + +template +T TextureCache::Texel(int texId, int level, Point2i p) { + ReportValue(mipLevelAccessed, level); + ProfilePhase _(Prof::TexCacheGetTexel); + CHECK(texId >= 0 && texId < textures.size()); + const TiledImagePyramid &tex = textures[texId]; + Point2i res = tex.levelResolution[level]; + CHECK(p.x >= 0 && p.x < res.x); + CHECK(p.y >= 0 && p.y < res.y); + // Return texel from preloaded levels, if applicable + const char *texel = tex.GetTexel(level, p); + if (texel != nullptr) return ConvertTexel(texel, tex.pixelFormat); + + // Get texel pointer from cache and return value + TileId tileId(texId, level, tex.TileIndex(p)); + texel = GetTile(tileId) + tex.TexelOffset(p); + T ret = ConvertTexel(texel, tex.pixelFormat); + RCUEnd(); + return ret; +} + +// Explicit instantiation +template Float TextureCache::Texel(int texId, int level, Point2i p); +template Spectrum TextureCache::Texel(int texId, int level, Point2i p); + +const char *TextureCache::GetTile(TileId tileId) { + ProfilePhase _(Prof::TexCacheGetTile); + ++tileCacheLookups; + // Return tile if it's present in the hash table + RCUBegin(); + TileHashTable *t = hashTable.load(std::memory_order_acquire); + if (const char *texels = t->Lookup(tileId)) { + ++tileCacheHits; + return texels; + } + RCUEnd(); + + // Check to see if another thread is already loading this tile + outstandingReadsMutex.lock(); + for (const TileId &readTileId : outstandingReads) { + if (readTileId == tileId) { + // Wait for _tileId_ to be read before retrying lookup + LOG(INFO) << "Another reader is already on it. Waiting for " + << tileId; + std::unique_lock readsLock(outstandingReadsMutex, + std::adopt_lock); + outstandingReadsCondition.wait(readsLock); + LOG(INFO) << "Read done. Retrying " << tileId; + readsLock.unlock(); + return GetTile(tileId); + } + } + + // Record that the current thread will read _tileId_ + outstandingReads.push_back(tileId); + outstandingReadsMutex.unlock(); + + // Load texture tile from disk + TextureTile *tile = GetFreeTile(); + ReadTile(tileId, tile); + + // Add tile to hash table and return texel pointer + RCUBegin(); + t = hashTable.load(std::memory_order_relaxed); + t->Insert(tile); + + // Update _outstandingReads_ for read tile + outstandingReadsMutex.lock(); + DCHECK(std::find(outstandingReads.begin(), outstandingReads.end(), + tileId) != outstandingReads.end()); + for (auto iter = outstandingReads.begin(); iter != outstandingReads.end(); + ++iter) { + if (*iter == tileId) { + outstandingReads.erase(iter); + break; + } + } + CHECK_LE(outstandingReads.size(), threadActiveFlags.size()); + outstandingReadsMutex.unlock(); + outstandingReadsCondition.notify_all(); + return tile->texels; +} + +TextureTile *TextureCache::GetFreeTile() { + std::lock_guard lock(freeTilesMutex); + if (freeTiles.size() == 0) { + LOG(INFO) << "Kicking off free"; + FreeTiles(); + CHECK_GT(freeTiles.size(), 0); + } + // Mark hash table entries if free-tile availability is low + if (freeTiles.size() == markFreeCapacity) + hashTable.load(std::memory_order_acquire)->MarkEntries(); + + // Return tile from _freeTiles_ + TextureTile *tile = freeTiles.back(); + freeTiles.pop_back(); + tile->Clear(); + return tile; +} + +void TextureCache::ReadTile(TileId tileId, TextureTile *tile) { + // Note that profiling ReadTile() using the profiling system isn't accurate: + // signal delivery is disabled when a thread is in the kernel, and most + // of the time here is in read()... + ProfilePhase _(Prof::TexCacheReadTile); + auto startTime = std::chrono::system_clock::now(); + tile->tileId = tileId; + CHECK(tileId.texId() >= 0 && tileId.texId() < textures.size()); + const TiledImagePyramid &tex = textures[tileId.texId()]; + // Get file descriptor and seek to start of texture tile + int64_t offset = tex.FileOffset(tileId.level(), tileId.p()); + CHECK_EQ(0, offset & (TileDiskAlignment - 1)) << offset; + FdEntry *fdEntry = fdCache.Lookup(tileId.texId(), tex.filename); +#ifdef PBRT_IS_WINDOWS + if (_lseeki64(fdEntry->fd, offset, SEEK_SET) == -1) +#else + if (lseek(fdEntry->fd, offset, SEEK_SET) == -1) +#endif + Error("%s: seek error %s", tex.filename.c_str(), strerror(errno)); + + // Read texel data and return file descriptor + int tileBytes = tex.TileBytes(); + CHECK_LE(tileBytes, TileAllocSize); + Point2i nt = nTiles(tex.levelResolution[tileId.level()], tex.logTileSize); + CHECK(tileId.p()[0] < nt[0]); + CHECK(tileId.p()[1] < nt[1]); + ++tilesRead; +#ifdef PBRT_IS_WINDOWS + if (_read(fdEntry->fd, tile->texels, tileBytes) != tileBytes) +#else + if (read(fdEntry->fd, tile->texels, tileBytes) != tileBytes) +#endif + Error("%s: read error %s", tex.filename.c_str(), strerror(errno)); + + using DurationMS = std::chrono::duration; + DurationMS elapsed(std::chrono::system_clock::now() - startTime); + if (elapsed < minReadDuration) + std::this_thread::sleep_for(minReadDuration - elapsed); + elapsed = DurationMS(std::chrono::system_clock::now() - startTime); + ReportValue(tileReadTimeMS, elapsed.count()); + totalTileReadMS += elapsed.count(); + + fdCache.Return(fdEntry); + texelReadBytes += tileBytes; +} + +void TextureCache::FreeTiles() { + ProfilePhase _(Prof::TexCacheFree); + LOG(INFO) << "Starting to free memory"; + ++freePasses; + // Copy unmarked tiles to _freeHashTable_ + hashTable.load(std::memory_order_relaxed)->CopyActive(freeHashTable); + + // Swap texture cache hash tables + freeHashTable = + hashTable.exchange(freeHashTable, std::memory_order_acq_rel); + + // Ensure that no threads are accessing the old hash table + for (size_t i = 0; i < threadActiveFlags.size(); ++i) WaitForQuiescent(i); + LOG(INFO) << "Got all thread locks"; + + // FIXME?: Anything added to the original hash table during + // CopyActive() and getting the per-thread locks will have its copied + // field still false; as such it suffers the unlucky fate of being + // freed shortly after creation here... + int nOrphaned = freeHashTable->CountOrphaned(); + LOG(INFO) << "Orphaned entries in hash table: " << nOrphaned; + ReportValue(orphanedTiles, nOrphaned); + // Add inactive tiles in _freeHashTable_ to free list + freeHashTable->ReclaimUncopied(&freeTiles); + if (freeTiles.size() < markFreeCapacity) + hashTable.load(std::memory_order_acquire)->MarkEntries(); + LOG(INFO) << "Finished freeing"; +} + +Image TextureCache::GetLevelImage(int texId, int level) { + Point2i res = textures[texId].levelResolution[level]; + PixelFormat format = textures[texId].pixelFormat; + + Image image(format, res); + for (int y = 0; y < res.y; ++y) + for (int x = 0; x < res.x; ++x) { + if (nChannels(format) == 1) + image.SetChannel({x, y}, 0, Texel(texId, level, {x, y})); + else { + CHECK_EQ(3, nChannels(format)); + Spectrum s = Texel(texId, level, {x, y}); + image.SetSpectrum({x, y}, s); + } + } + + return image; +} + +} // namespace pbrt diff --git a/src/core/texcache.h b/src/core/texcache.h new file mode 100644 index 0000000000..6acd239ddc --- /dev/null +++ b/src/core/texcache.h @@ -0,0 +1,251 @@ + +/* + pbrt source code is Copyright(c) 1998-2016 + Matt Pharr, Greg Humphreys, and Wenzel Jakob. + + This file is part of pbrt. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are + met: + + - Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + - Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS + IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + */ + +#if defined(_MSC_VER) +#define NOMINMAX +#pragma once +#endif + +#ifndef PBRT_CORE_TEXCACHE_H +#define PBRT_CORE_TEXCACHE_H + +// core/texcache.h* +#include +#include +#include +#include +#include "geometry.h" +#include "image.h" +#include "memory.h" +#include "parallel.h" +#include "pbrt.h" + +/* +TODO (future); +- allow non-square tiles (but still pow2) + +- reduce rgb->y if greyscale, etc. + - reduce to lower bit depths if appropriate + - do at Image::Read time, so all benefit? +*/ + +namespace pbrt { + +struct TileId; +struct TextureTile; +class TileHashTable; + +// Texture Cache Constants +static const uint32_t tiledFileMagic = 0x65028088; +static PBRT_CONSTEXPR int TileDiskAlignment = 4096; +#ifdef PBRT_IS_LINUX +static PBRT_CONSTEXPR int MaxOpenFiles = 4000; +#else +static PBRT_CONSTEXPR int MaxOpenFiles = 200; // TODO: how big? +#endif + +// ActiveFlag Declarations +#ifdef PBRT_HAVE_ALIGNAS +struct alignas(PBRT_L1_CACHE_LINE_SIZE) ActiveFlag { +#else +struct ActiveFlag { char pad[PBRT_L1_CACHE_LINE_SIZE]; +#endif + std::atomic flag{false}; +}; + +// TiledImagePyramid Declarations +class TiledImagePyramid { + public: + // TiledImagePyramid Public Methods + static bool Create(std::vector levels, const std::string &filename, + WrapMode wrapMode, int tileSize, + int topLevelsBytes = 3800); + static bool Read(const std::string &filename, TiledImagePyramid *tex); + size_t TileBytes() const { + return (1 << logTileSize) * (1 << logTileSize) * + TexelBytes(pixelFormat); + } + size_t TileDiskBytes() const { + return (TileBytes() + TileDiskAlignment - 1) & ~(TileDiskAlignment - 1); + } + Point2i TileIndex(Point2i p) const { + return {p[0] >> logTileSize, p[1] >> logTileSize}; + } + int TexelOffset(Point2i p) const { + int tileMask = (1 << logTileSize) - 1; + Point2i tilep{p[0] & tileMask, p[1] & tileMask}; + int tileWidth = 1 << logTileSize; + return TexelBytes(pixelFormat) * (tilep[1] * tileWidth + tilep[0]); + } + int64_t FileOffset(int level, Point2i p) const { + int tileWidth = 1 << logTileSize; + CHECK_LE(level, levelOffset.size()); // preloaded + int xTiles = (levelResolution[level][0] + tileWidth - 1) >> logTileSize; + return levelOffset[level] + TileDiskBytes() * (p[1] * xTiles + p[0]); + } + const char *GetTexel(int level, Point2i p) const { + // Assumes that p has already gone through remapping. + CHECK(p.x >= 0 && p.x < levelResolution[level].x); + CHECK(p.y >= 0 && p.y < levelResolution[level].y); + + if (level < firstInMemoryLevel) return nullptr; + const char *levelStart = + inMemoryLevels[level - firstInMemoryLevel].get(); + return levelStart + + TexelBytes(pixelFormat) * + (p[1] * levelResolution[level][0] + p[0]); + } + + // TiledImagePyramid Public Data + std::string filename; + PixelFormat pixelFormat; + WrapMode wrapMode; + int logTileSize; + std::vector levelResolution; + std::vector levelOffset; + int firstInMemoryLevel; + std::vector> inMemoryLevels; +}; + +// FdEntry Declarations +class FdEntry { + public: + int fd = -1; + + private: + // FdEntry Private Data + friend class FdCache; + int fileId = -1; + FdEntry *next = nullptr; + bool inUse = false; +}; + +// FdCache Declarations +class FdCache { + public: + // FdCache Public Methods + FdCache(int fdsSpared = 40); + ~FdCache(); + FdEntry *Lookup(int id, const std::string &filename); + void Return(FdEntry *entry); + + private: + // FdCache Private Data + std::unique_ptr allocPtr; + FdEntry *freeList = nullptr; + std::vector hashTable; + int logBuckets; + std::mutex mutex; + int nextVictim = 0; +}; + +// TextureCache Declarations +class TextureCache { + public: + // TextureCache Public Methods + TextureCache(); + ~TextureCache(); + static int TileSize(PixelFormat format); + int AddTexture(const std::string &filename); + const std::vector &GetLevelResolution(int texId) const { + CHECK(texId >= 0 && texId < textures.size()); + return textures[texId].levelResolution; + } + Point2i GetLevelResolution(int texId, int level) const { + const std::vector &res = GetLevelResolution(texId); + CHECK(level >= 0 && level < res.size()); + return res[level]; + } + WrapMode GetWrapMode(int texId) const { + CHECK(texId >= 0 && texId < textures.size()); + return textures[texId].wrapMode; + } + int Levels(int texId) const { + CHECK(texId >= 0 && texId < textures.size()); + return textures[texId].levelResolution.size(); + } + PixelFormat GetPixelFormat(int texId) const { + CHECK(texId >= 0 && texId < textures.size()); + return textures[texId].pixelFormat; + } + void PreloadTexture(int texId); + template + T Texel(int texId, int level, Point2i p); + Image GetLevelImage(int texId, int level); + + private: + // TextureCache Private Methods + void RCUBegin() { + std::atomic &flag = threadActiveFlags[ThreadIndex].flag; + flag.store(true, std::memory_order_acquire); + } + void RCUEnd() { + std::atomic &flag = threadActiveFlags[ThreadIndex].flag; + flag.store(false, std::memory_order_release); + } + void WaitForQuiescent(int thread) { + std::atomic &flag = threadActiveFlags[thread].flag; + while (flag.load(std::memory_order_acquire) == true) + ; // spin + } + const char *GetTile(TileId tileId); + TextureTile *GetFreeTile(); + void ReadTile(TileId tileId, TextureTile *tile); + void FreeTiles(); + + // TextureCache Private Data + static PBRT_CONSTEXPR int TileAllocSize = 3 * 64 * 64; + std::unique_ptr tileMemAlloc; + std::unique_ptr allTilesAlloc; + std::mutex freeTilesMutex; + std::vector freeTiles; + std::atomic hashTable; + TileHashTable *freeHashTable; + std::vector textures; + std::vector threadActiveFlags; + +#ifdef PBRT_HAVE_ALIGNAS + alignas(PBRT_L1_CACHE_LINE_SIZE) +#else + char pad[PBRT_L1_CACHE_LINE_SIZE]; +#endif + std::mutex outstandingReadsMutex; + + std::vector outstandingReads; + std::condition_variable outstandingReadsCondition; + FdCache fdCache; + int markFreeCapacity; +}; + +} // namespace pbrt + +#endif // PBRT_CORE_TEXCACHE_H diff --git a/src/integrators/path.cpp b/src/integrators/path.cpp index ddc16f9a28..84f3122248 100644 --- a/src/integrators/path.cpp +++ b/src/integrators/path.cpp @@ -147,7 +147,7 @@ Spectrum PathIntegrator::Li(const RayDifferential &r, const Scene &scene, // medium. etaScale *= (Dot(wo, isect.n) > 0) ? (eta * eta) : 1 / (eta * eta); } - ray = isect.SpawnRay(wi); + ray = isect.SpawnRay(ray, wi, flags, isect.bsdf->eta); // Account for subsurface scattering, if applicable if (isect.bssrdf && (flags & BSDF_TRANSMISSION)) { diff --git a/src/integrators/sppm.cpp b/src/integrators/sppm.cpp index b3bc648e33..1cf1867e3c 100644 --- a/src/integrators/sppm.cpp +++ b/src/integrators/sppm.cpp @@ -35,7 +35,7 @@ #include "integrators/sppm.h" #include "parallel.h" #include "scene.h" -#include "imageio.h" +#include "image.h" #include "spectrum.h" #include "rng.h" #include "paramset.h" @@ -461,8 +461,7 @@ void SPPMIntegrator::Render(const Scene &scene) { camera->film->WriteImage(); // Write SPPM radius image, if requested if (getenv("SPPM_RADIUS")) { - std::unique_ptr rimg( - new Float[3 * pixelBounds.Area()]); + Image rimg(PixelFormat::Y32, Point2i(pixelBounds.Diagonal())); Float minrad = 1e30f, maxrad = 0; for (int y = pixelBounds.pMin.y; y < pixelBounds.pMax.y; ++y) { for (int x = x0; x < x1; ++x) { @@ -483,14 +482,11 @@ void SPPMIntegrator::Render(const Scene &scene) { pixels[(y - pixelBounds.pMin.y) * (x1 - x0) + (x - x0)]; Float v = 1.f - (p.radius - minrad) / (maxrad - minrad); - rimg[offset++] = v; - rimg[offset++] = v; - rimg[offset++] = v; + rimg.SetY({x - x0, y - pixelBounds.pMin.y}, v); } } - Point2i res(pixelBounds.pMax.x - pixelBounds.pMin.x, - pixelBounds.pMax.y - pixelBounds.pMin.y); - WriteImage("sppm_radius.png", rimg.get(), pixelBounds, res); + rimg.Write("sppm_radius.png", pixelBounds, + camera->film->fullResolution); } } } diff --git a/src/integrators/volpath.cpp b/src/integrators/volpath.cpp index 1b0077e9a4..96f9e69a2c 100644 --- a/src/integrators/volpath.cpp +++ b/src/integrators/volpath.cpp @@ -144,7 +144,7 @@ Spectrum VolPathIntegrator::Li(const RayDifferential &r, const Scene &scene, etaScale *= (Dot(wo, isect.n) > 0) ? (eta * eta) : 1 / (eta * eta); } - ray = isect.SpawnRay(wi); + ray = isect.SpawnRay(ray, wi, flags, isect.bsdf->eta); // Account for attenuated subsurface scattering, if applicable if (isect.bssrdf && (flags & BSDF_TRANSMISSION)) { diff --git a/src/lights/distant.cpp b/src/lights/distant.cpp index bb1f0037a8..372a635217 100644 --- a/src/lights/distant.cpp +++ b/src/lights/distant.cpp @@ -30,7 +30,6 @@ */ - // lights/distant.cpp* #include "lights/distant.h" #include "paramset.h" diff --git a/src/lights/goniometric.cpp b/src/lights/goniometric.cpp index b0d1332f85..cba081ff1e 100644 --- a/src/lights/goniometric.cpp +++ b/src/lights/goniometric.cpp @@ -30,7 +30,6 @@ */ - // lights/goniometric.cpp* #include "lights/goniometric.h" #include "paramset.h" @@ -49,13 +48,20 @@ Spectrum GonioPhotometricLight::Sample_Li(const Interaction &ref, *pdf = 1.f; *vis = VisibilityTester(ref, Interaction(pLight, ref.time, mediumInterface)); - return I * Scale(-*wi) / DistanceSquared(pLight, ref.p); + return Scale(-*wi) / DistanceSquared(pLight, ref.p); } Spectrum GonioPhotometricLight::Power() const { - return 4 * Pi * I * Spectrum(mipmap ? mipmap->Lookup(Point2f(.5f, .5f), .5f) - : RGBSpectrum(1.f), - SpectrumType::Illuminant); + Spectrum sumL(0.); + int width = image.resolution.x, height = image.resolution.y; + for (int v = 0; v < height; ++v) { + Float sinTheta = std::sin(Pi * Float(v + .5f) / Float(height)); + for (int u = 0; u < width; ++u) { + sumL += + image.GetSpectrum({u, v}, SpectrumType::Illuminant) * sinTheta; + } + } + return 4 * Pi * sumL / (width * height); } Float GonioPhotometricLight::Pdf_Li(const Interaction &, @@ -73,7 +79,7 @@ Spectrum GonioPhotometricLight::Sample_Le(const Point2f &u1, const Point2f &u2, *nLight = (Normal3f)ray->d; *pdfPos = 1.f; *pdfDir = UniformSpherePdf(); - return I * Scale(ray->d); + return Scale(ray->d); } void GonioPhotometricLight::Pdf_Le(const Ray &, const Normal3f &, Float *pdfPos, diff --git a/src/lights/goniometric.h b/src/lights/goniometric.h index d2d9d40421..caea36ae76 100644 --- a/src/lights/goniometric.h +++ b/src/lights/goniometric.h @@ -42,9 +42,7 @@ #include "pbrt.h" #include "light.h" #include "shape.h" -#include "scene.h" -#include "mipmap.h" -#include "imageio.h" +#include "image.h" namespace pbrt { @@ -60,11 +58,10 @@ class GonioPhotometricLight : public Light { : Light((int)LightFlags::DeltaPosition, LightToWorld, mediumInterface), pLight(LightToWorld(Point3f(0, 0, 0))), I(I) { - // Create _mipmap_ for _GonioPhotometricLight_ - Point2i resolution; - std::unique_ptr texels = ReadImage(texname, &resolution); - if (texels) - mipmap.reset(new MIPMap(resolution, texels.get())); + if (!Image::Read(texname, &image)) { + std::vector one = {(Float)1}; + image = Image(std::move(one), PixelFormat::Y32, {1, 1}); + } } Spectrum Scale(const Vector3f &w) const { Vector3f wp = Normalize(WorldToLight(w)); @@ -72,8 +69,7 @@ class GonioPhotometricLight : public Light { Float theta = SphericalTheta(wp); Float phi = SphericalPhi(wp); Point2f st(phi * Inv2Pi, theta * InvPi); - return !mipmap ? RGBSpectrum(1.f) - : Spectrum(mipmap->Lookup(st), SpectrumType::Illuminant); + return I * image.BilerpSpectrum(st, SpectrumType::Illuminant); } Spectrum Power() const; Float Pdf_Li(const Interaction &, const Vector3f &) const; @@ -87,7 +83,7 @@ class GonioPhotometricLight : public Light { // GonioPhotometricLight Private Data const Point3f pLight; const Spectrum I; - std::unique_ptr> mipmap; + Image image; }; std::shared_ptr CreateGoniometricLight( diff --git a/src/lights/infinite.cpp b/src/lights/infinite.cpp index 8da4a02f7e..1ed3d032d5 100644 --- a/src/lights/infinite.cpp +++ b/src/lights/infinite.cpp @@ -32,9 +32,9 @@ // lights/infinite.cpp* #include "lights/infinite.h" -#include "imageio.h" -#include "paramset.h" #include "sampling.h" +#include "paramset.h" +#include "parallel.h" #include "stats.h" namespace pbrt { @@ -42,57 +42,54 @@ namespace pbrt { // InfiniteAreaLight Method Definitions InfiniteAreaLight::InfiniteAreaLight(const Transform &LightToWorld, const Spectrum &L, int nSamples, - const std::string &texmap) + const std::string &filename) : Light((int)LightFlags::Infinite, LightToWorld, MediumInterface(), - nSamples) { - // Read texel data from _texmap_ and initialize _Lmap_ - Point2i resolution; - std::unique_ptr texels(nullptr); - if (texmap != "") { - texels = ReadImage(texmap, &resolution); - if (texels) - for (int i = 0; i < resolution.x * resolution.y; ++i) - texels[i] *= L.ToRGBSpectrum(); + nSamples), + Lscale(L) { + if (!Image::Read(filename, &image)) { + std::vector one = {(Float)1}; + image = Image(std::move(one), PixelFormat::Y32, {1, 1}); } - if (!texels) { - resolution.x = resolution.y = 1; - texels = std::unique_ptr(new RGBSpectrum[1]); - texels[0] = L.ToRGBSpectrum(); - } - Lmap.reset(new MIPMap(resolution, texels.get())); // Initialize sampling PDFs for infinite area light // Compute scalar-valued image _img_ from environment map - int width = 2 * Lmap->Width(), height = 2 * Lmap->Height(); + int width = 2 * image.resolution.x, height = 2 * image.resolution.y; std::unique_ptr img(new Float[width * height]); float fwidth = 0.5f / std::min(width, height); ParallelFor( [&](int64_t v) { Float vp = (v + .5f) / (Float)height; - Float sinTheta = std::sin(Pi * (v + .5f) / height); + Float sinTheta = std::sin(Pi * Float(v + .5f) / Float(height)); for (int u = 0; u < width; ++u) { Float up = (u + .5f) / (Float)width; - img[u + v * width] = Lmap->Lookup(Point2f(up, vp), fwidth).y(); + img[u + v * width] = image.BilerpY({up, vp}); img[u + v * width] *= sinTheta; } - }, - height, 32); + }, height, 32); // Compute sampling distributions for rows and columns of image distribution.reset(new Distribution2D(img.get(), width, height)); } Spectrum InfiniteAreaLight::Power() const { - return Pi * worldRadius * worldRadius * - Spectrum(Lmap->Lookup(Point2f(.5f, .5f), .5f), - SpectrumType::Illuminant); + Spectrum sumL(0.); + + int width = image.resolution.x, height = image.resolution.y; + for (int v = 0; v < height; ++v) { + Float sinTheta = std::sin(Pi * Float(v + .5f) / Float(height)); + for (int u = 0; u < width; ++u) { + sumL += + image.GetSpectrum({u, v}, SpectrumType::Illuminant) * sinTheta; + } + } + return Pi * worldRadius * worldRadius * Lscale * sumL / (width * height); } Spectrum InfiniteAreaLight::Le(const RayDifferential &ray) const { Vector3f w = Normalize(WorldToLight(ray.d)); Point2f st(SphericalPhi(w) * Inv2Pi, SphericalTheta(w) * InvPi); - return Spectrum(Lmap->Lookup(st), SpectrumType::Illuminant); + return Lscale * image.BilerpSpectrum(st, SpectrumType::Illuminant); } Spectrum InfiniteAreaLight::Sample_Li(const Interaction &ref, const Point2f &u, @@ -118,7 +115,7 @@ Spectrum InfiniteAreaLight::Sample_Li(const Interaction &ref, const Point2f &u, // Return radiance value for infinite light direction *vis = VisibilityTester(ref, Interaction(ref.p + *wi * (2 * worldRadius), ref.time, mediumInterface)); - return Spectrum(Lmap->Lookup(uv), SpectrumType::Illuminant); + return Lscale * image.BilerpSpectrum(uv, SpectrumType::Illuminant); } Float InfiniteAreaLight::Pdf_Li(const Interaction &, const Vector3f &w) const { @@ -159,7 +156,7 @@ Spectrum InfiniteAreaLight::Sample_Le(const Point2f &u1, const Point2f &u2, // Compute _InfiniteAreaLight_ ray PDFs *pdfDir = sinTheta == 0 ? 0 : mapPdf / (2 * Pi * Pi * sinTheta); *pdfPos = 1 / (Pi * worldRadius * worldRadius); - return Spectrum(Lmap->Lookup(uv), SpectrumType::Illuminant); + return Lscale * image.BilerpSpectrum(uv, SpectrumType::Illuminant); } void InfiniteAreaLight::Pdf_Le(const Ray &ray, const Normal3f &, Float *pdfPos, diff --git a/src/lights/infinite.h b/src/lights/infinite.h index 47ae257f8a..a736ae0744 100644 --- a/src/lights/infinite.h +++ b/src/lights/infinite.h @@ -44,7 +44,7 @@ #include "texture.h" #include "shape.h" #include "scene.h" -#include "mipmap.h" +#include "image.h" namespace pbrt { @@ -70,7 +70,8 @@ class InfiniteAreaLight : public Light { private: // InfiniteAreaLight Private Data - std::unique_ptr> Lmap; + Image image; + Spectrum Lscale; Point3f worldCenter; Float worldRadius; std::unique_ptr distribution; diff --git a/src/lights/point.cpp b/src/lights/point.cpp index d65290c7eb..04c4b2f156 100644 --- a/src/lights/point.cpp +++ b/src/lights/point.cpp @@ -30,7 +30,6 @@ */ - // lights/point.cpp* #include "lights/point.h" #include "scene.h" diff --git a/src/lights/projection.cpp b/src/lights/projection.cpp index d0f1f4c904..a17672502f 100644 --- a/src/lights/projection.cpp +++ b/src/lights/projection.cpp @@ -30,12 +30,10 @@ */ - // lights/projection.cpp* #include "lights/projection.h" #include "sampling.h" #include "paramset.h" -#include "imageio.h" #include "reflection.h" #include "stats.h" @@ -50,14 +48,13 @@ ProjectionLight::ProjectionLight(const Transform &LightToWorld, pLight(LightToWorld(Point3f(0, 0, 0))), I(I) { // Create _ProjectionLight_ MIP map - Point2i resolution; - std::unique_ptr texels = ReadImage(texname, &resolution); - if (texels) - projectionMap.reset(new MIPMap(resolution, texels.get())); + if (!Image::Read(texname, &image)) { + std::vector one = {(Float)1}; + image = Image(std::move(one), PixelFormat::Y32, {1, 1}); + } // Initialize _ProjectionLight_ projection matrix - Float aspect = - projectionMap ? (Float(resolution.x) / Float(resolution.y)) : 1; + Float aspect = Float(image.resolution.x) / Float(image.resolution.y); if (aspect > 1) screenBounds = Bounds2f(Point2f(-aspect, -1), Point2f(aspect, 1)); else @@ -81,7 +78,7 @@ Spectrum ProjectionLight::Sample_Li(const Interaction &ref, const Point2f &u, *pdf = 1; *vis = VisibilityTester(ref, Interaction(pLight, ref.time, mediumInterface)); - return I * Projection(-*wi) / DistanceSquared(pLight, ref.p); + return Projection(-*wi) / DistanceSquared(pLight, ref.p); } Spectrum ProjectionLight::Projection(const Vector3f &w) const { @@ -92,17 +89,17 @@ Spectrum ProjectionLight::Projection(const Vector3f &w) const { // Project point onto projection plane and compute light Point3f p = lightProjection(Point3f(wl.x, wl.y, wl.z)); if (!Inside(Point2f(p.x, p.y), screenBounds)) return 0.f; - if (!projectionMap) return 1; Point2f st = Point2f(screenBounds.Offset(Point2f(p.x, p.y))); - return Spectrum(projectionMap->Lookup(st), SpectrumType::Illuminant); + return I * image.BilerpSpectrum(st, SpectrumType::Illuminant); } Spectrum ProjectionLight::Power() const { - return (projectionMap - ? Spectrum(projectionMap->Lookup(Point2f(.5f, .5f), .5f), - SpectrumType::Illuminant) - : Spectrum(1.f)) * - I * 2 * Pi * (1.f - cosTotalWidth); + Spectrum sum(0.f); + for (int v = 0; v < image.resolution.y; ++v) + for (int u = 0; u < image.resolution.x; ++u) + sum += image.GetSpectrum({u, v}, SpectrumType::Illuminant); + return I * 2 * Pi * (1.f - cosTotalWidth) * sum / + (image.resolution.x * image.resolution.y); } Float ProjectionLight::Pdf_Li(const Interaction &, const Vector3f &) const { @@ -118,7 +115,7 @@ Spectrum ProjectionLight::Sample_Le(const Point2f &u1, const Point2f &u2, *nLight = (Normal3f)ray->d; /// same here *pdfPos = 1.f; *pdfDir = UniformConePdf(cosTotalWidth); - return I * Projection(ray->d); + return Projection(ray->d); } void ProjectionLight::Pdf_Le(const Ray &ray, const Normal3f &, Float *pdfPos, diff --git a/src/lights/projection.h b/src/lights/projection.h index a20e7a0161..c8bf3995ff 100644 --- a/src/lights/projection.h +++ b/src/lights/projection.h @@ -42,7 +42,7 @@ #include "pbrt.h" #include "light.h" #include "shape.h" -#include "mipmap.h" +#include "image.h" namespace pbrt { @@ -66,7 +66,7 @@ class ProjectionLight : public Light { private: // ProjectionLight Private Data - std::unique_ptr> projectionMap; + Image image; const Point3f pLight; const Spectrum I; Transform lightProjection; diff --git a/src/lights/spot.cpp b/src/lights/spot.cpp index decba3f29f..5028d6f5bc 100644 --- a/src/lights/spot.cpp +++ b/src/lights/spot.cpp @@ -30,7 +30,6 @@ */ - // lights/spot.cpp* #include "lights/spot.h" #include "paramset.h" diff --git a/src/main/pbrt.cpp b/src/main/pbrt.cpp index 22451b2167..3be8943581 100644 --- a/src/main/pbrt.cpp +++ b/src/main/pbrt.cpp @@ -47,6 +47,7 @@ static void usage(const char *msg = nullptr) { fprintf(stderr, R"(usage: pbrt [] Rendering options: --help Print this help text. + --texcachemb Texture cache size in MB. --nthreads Use specified number of threads for rendering. --outfile Write the final image to the given filename. --quick Automatically reduce a number of quality settings to @@ -85,6 +86,16 @@ int main(int argc, char *argv[]) { options.nThreads = atoi(argv[++i]); } else if (!strncmp(argv[i], "--nthreads=", 11)) { options.nThreads = atoi(&argv[i][11]); + } else if (!strcmp(argv[i], "--texcachemb") || !strcmp(argv[i], "-texcachemb")) { + if (i + 1 == argc) + usage("missing value after --texcachemb argument"); + options.texCacheMB = atoi(argv[++i]); + CHECK_GT(options.texCacheMB, 0); + } else if (!strcmp(argv[i], "--texreadms") || !strcmp(argv[i], "-texreadms")) { + if (i + 1 == argc) + usage("missing value after --texreadms argument"); + options.texReadMinMS = atoi(argv[++i]); + CHECK_GT(options.texCacheMB, 0); } else if (!strcmp(argv[i], "--outfile") || !strcmp(argv[i], "-outfile")) { if (i + 1 == argc) usage("missing value after --outfile argument"); diff --git a/src/tests/analytic_scenes.cpp b/src/tests/analytic_scenes.cpp index 06c6b539aa..60c086cafa 100644 --- a/src/tests/analytic_scenes.cpp +++ b/src/tests/analytic_scenes.cpp @@ -1,7 +1,7 @@ #include "tests/gtest/gtest.h" #include "pbrt.h" - +#include "image.h" #include "accelerators/bvh.h" #include "api.h" #include "cameras/orthographic.h" @@ -50,16 +50,17 @@ void PrintTo(const TestIntegrator &tr, ::std::ostream *os) { } void CheckSceneAverage(const char *filename, float expected) { - Point2i resolution; - std::unique_ptr image = ReadImage(filename, &resolution); - ASSERT_TRUE(image.get() != nullptr); + Image image; + ASSERT_TRUE(Image::Read(filename, &image)); + ASSERT_EQ(image.nChannels(), 3); float delta = .02; float sum = 0; - for (int i = 0; i < resolution.x * resolution.y; ++i) - for (int c = 0; c < 3; ++c) sum += image[i][c]; - int nPixels = resolution.x * resolution.y * 3; + for (int t = 0; t < image.resolution[1]; ++t) + for (int s = 0; s < image.resolution[0]; ++s) + for (int c = 0; c < 3; ++c) sum += image.GetChannel(Point2i(s, t), c); + int nPixels = image.resolution.x * image.resolution.y * 3; EXPECT_NEAR(expected, sum / nPixels, delta); } diff --git a/src/tests/half.cpp b/src/tests/half.cpp new file mode 100644 index 0000000000..10d80cd6a7 --- /dev/null +++ b/src/tests/half.cpp @@ -0,0 +1,84 @@ + +#include "tests/gtest/gtest.h" + +#include "pbrt.h" +#include "core/fp16.h" +#include "rng.h" + +using namespace pbrt; + +TEST(Half, Basics) { + EXPECT_EQ(FloatToHalf(0.f), kHalfPositiveZero); + EXPECT_EQ(FloatToHalf(-0.f), kHalfNegativeZero); + EXPECT_EQ(FloatToHalf(Infinity), kHalfPositiveInfinity); + EXPECT_EQ(FloatToHalf(-Infinity), kHalfNegativeInfinity); + + EXPECT_TRUE(HalfIsNaN(FloatToHalf(0.f / 0.f))); + EXPECT_TRUE(HalfIsNaN(FloatToHalf(-0.f / 0.f))); + EXPECT_FALSE(HalfIsNaN(kHalfPositiveInfinity)); +} + +TEST(Half, ExactConversions) { + // Test round-trip conversion of integers that are perfectly + // representable. + for (int i = -2048; i <= 2048; ++i) { + EXPECT_EQ(i, HalfToFloat(FloatToHalf(i))); + } + + // Similarly for some well-behaved floats + float limit = 1024, delta = 0.5; + for (int i = 0; i < 10; ++i) { + for (float f = -limit; f <= limit; f += delta) + EXPECT_EQ(f, HalfToFloat(FloatToHalf(f))); + limit /= 2; + delta /= 2; + } +} + +TEST(Half, Randoms) { + RNG rng; + // Choose a bunch of random positive floats and make sure that they + // convert to reasonable values. + for (int i = 0; i < 1024; ++i) { + float f = rng.UniformFloat() * 512; + uint16_t h = FloatToHalf(f); + float fh = HalfToFloat(h); + if (fh == f) { + // Very unlikely, but we happened to pick a value exactly + // representable as a half. + continue; + } + else { + // The other half value that brackets the float. + uint16_t hother; + if (fh > f) { + // The closest half was a bit bigger; therefore, the half before it + // s the other one. + hother = h - 1; + if (hother > h) { + // test for wrapping around zero + continue; + } + } else { + hother = h + 1; + if (hother < h) { + // test for wrapping around zero + continue; + } + } + + // Make sure the two half values bracket the float. + float fother = HalfToFloat(hother); + float dh = std::abs(fh - f); + float dother = std::abs(fother - f); + if (fh > f) + EXPECT_LT(fother, f); + else + EXPECT_GT(fother, f); + + // Make sure rounding to the other one of them wouldn't have given a + // closer half. + EXPECT_LE(dh, dother); + } + } +} diff --git a/src/tests/image.cpp b/src/tests/image.cpp new file mode 100644 index 0000000000..6f5d266b64 --- /dev/null +++ b/src/tests/image.cpp @@ -0,0 +1,345 @@ + +#include "tests/gtest/gtest.h" + +#include "pbrt.h" +#include "image.h" +#include "rng.h" +#include "mipmap.h" +#include "half.h" + +using namespace pbrt; + +// TODO: +// for tga and png i/o: test mono and rgb; make sure mono is smaller +// pixel bounds stuff... (including i/o paths...) +// basic lookups, bilerps, etc +// also clamp, repeat, etc... +// resize? +// round trip: init, write, read, check +// FlipY() + +TEST(Image, Basics) { + Image y8(PixelFormat::Y8, {4, 8}); + EXPECT_EQ(y8.nChannels(), 1); + EXPECT_EQ(y8.BytesUsed(), y8.resolution[0] * y8.resolution[1]); + + Image sy8(PixelFormat::SY8, {4, 8}); + EXPECT_EQ(sy8.nChannels(), 1); + EXPECT_EQ(sy8.BytesUsed(), sy8.resolution[0] * sy8.resolution[1]); + + Image y16(PixelFormat::Y16, {4, 8}); + EXPECT_EQ(y16.nChannels(), 1); + EXPECT_EQ(y16.BytesUsed(), 2 * y16.resolution[0] * y16.resolution[1]); + + Image y32(PixelFormat::Y32, {4, 8}); + EXPECT_EQ(y32.nChannels(), 1); + EXPECT_EQ(y32.BytesUsed(), 4 * y32.resolution[0] * y32.resolution[1]); + + Image rgb8(PixelFormat::RGB8, {4, 8}); + EXPECT_EQ(rgb8.nChannels(), 3); + EXPECT_EQ(rgb8.BytesUsed(), 3 * rgb8.resolution[0] * rgb8.resolution[1]); + + Image srgb8(PixelFormat::SRGB8, {4, 8}); + EXPECT_EQ(srgb8.nChannels(), 3); + EXPECT_EQ(srgb8.BytesUsed(), 3 * srgb8.resolution[0] * srgb8.resolution[1]); + + Image rgb16(PixelFormat::RGB16, {4, 16}); + EXPECT_EQ(rgb16.nChannels(), 3); + EXPECT_EQ(rgb16.BytesUsed(), + 2 * 3 * rgb16.resolution[0] * rgb16.resolution[1]); + + Image rgb32(PixelFormat::RGB32, {4, 32}); + EXPECT_EQ(rgb32.nChannels(), 3); + EXPECT_EQ(rgb32.BytesUsed(), + 4 * 3 * rgb32.resolution[0] * rgb32.resolution[1]); +} + +static Float sRGBRoundTrip(Float v) { + Float encoded = 255. * GammaCorrect(Clamp(v, 0, 1)); + return InverseGammaCorrect(std::round(encoded) / 255.); +} + +static std::vector GetInt8Pixels(Point2i res, int nc) { + std::vector r; + for (int y = 0; y < res[1]; ++y) + for (int x = 0; x < res[0]; ++x) + for (int c = 0; c < nc; ++c) r.push_back((x * y + c) % 255); + return r; +} + +static std::vector GetFloatPixels(Point2i res, int nc) { + std::vector p; + for (int y = 0; y < res[1]; ++y) + for (int x = 0; x < res[0]; ++x) + for (int c = 0; c < nc; ++c) + p.push_back(-.25 + + 2. * (c + 3 * x + 3 * y * res[0]) / + (res[0] * res[1])); + return p; +} + +TEST(Image, GetSetY) { + Point2i res(9, 3); + std::vector yPixels = GetFloatPixels(res, 1); + + for (auto format : {PixelFormat::Y8, PixelFormat::SY8, PixelFormat::Y16, + PixelFormat::Y32}) { + Image image(format, res); + for (int y = 0; y < res[1]; ++y) + for (int x = 0; x < res[0]; ++x) { + image.SetChannel({x, y}, 0, yPixels[y * res[0] + x]); + } + for (int y = 0; y < res[1]; ++y) + for (int x = 0; x < res[0]; ++x) { + Float v = image.GetChannel({x, y}, 0); + EXPECT_EQ(v, image.GetY({x, y})); + switch (format) { + case PixelFormat::Y32: + EXPECT_EQ(v, yPixels[y * res[0] + x]); + break; + case PixelFormat::Y16: + EXPECT_EQ( + v, HalfToFloat(FloatToHalf(yPixels[y * res[0] + x]))); + break; + case PixelFormat::Y8: { + Float delta = + std::abs(v - Clamp(yPixels[y * res[0] + x], 0, 1)); + EXPECT_LE(delta, 0.501 / 255.); + break; + } + case PixelFormat::SY8: { + EXPECT_EQ(v, sRGBRoundTrip(yPixels[y * res[0] + x])); + break; + } + default: // silence compiler warning + break; + } + } + } +} + +TEST(Image, GetSetRGB) { + Point2i res(7, 32); + std::vector rgbPixels = GetFloatPixels(res, 3); + + for (auto format : {PixelFormat::RGB8, PixelFormat::SRGB8, + PixelFormat::RGB16, PixelFormat::RGB32}) { + Image image(format, res); + for (int y = 0; y < res[1]; ++y) + for (int x = 0; x < res[0]; ++x) + for (int c = 0; c < 3; ++c) + image.SetChannel({x, y}, c, + rgbPixels[3 * y * res[0] + 3 * x + c]); + + for (int y = 0; y < res[1]; ++y) + for (int x = 0; x < res[0]; ++x) { + Spectrum s = image.GetSpectrum({x, y}); + Float rgb[3]; + s.ToRGB(rgb); + + for (int c = 0; c < 3; ++c) { + // This is assuming Spectrum==RGBSpectrum, which is bad. + ASSERT_EQ(sizeof(RGBSpectrum), sizeof(Spectrum)); + + EXPECT_EQ(rgb[c], image.GetChannel({x, y}, c)); + + int offset = 3 * y * res[0] + 3 * x + c; + switch (format) { + case PixelFormat::RGB32: + EXPECT_EQ(rgb[c], rgbPixels[offset]); + break; + case PixelFormat::RGB16: + EXPECT_EQ(rgb[c], + HalfToFloat(FloatToHalf(rgbPixels[offset]))); + break; + case PixelFormat::RGB8: { + Float delta = + std::abs(rgb[c] - Clamp(rgbPixels[offset], 0, 1)); + EXPECT_LE(delta, 0.501 / 255.); + break; + } + case PixelFormat::SRGB8: { + EXPECT_EQ(rgb[c], sRGBRoundTrip(rgbPixels[offset])); + break; + } + default: // silence compiler warning + break; + } + } + } + } +} + +TEST(Image, PfmIO) { + Point2i res(16, 49); + std::vector rgbPixels = GetFloatPixels(res, 3); + + Image image(rgbPixels, PixelFormat::RGB32, res); + EXPECT_TRUE(image.Write("test.pfm")); + Image read; + EXPECT_TRUE(Image::Read("test.pfm", &read)); + + EXPECT_EQ(image.resolution, read.resolution); + EXPECT_EQ(read.format, PixelFormat::RGB32); + + for (int y = 0; y < res[1]; ++y) + for (int x = 0; x < res[0]; ++x) + for (int c = 0; c < 3; ++c) + EXPECT_EQ(image.GetChannel({x, y}, c), + read.GetChannel({x, y}, c)); + + EXPECT_EQ(0, remove("test.pfm")); +} + +TEST(Image, ExrIO) { + Point2i res(16, 49); + std::vector rgbPixels = GetFloatPixels(res, 3); + + Image image(rgbPixels, PixelFormat::RGB32, res); + EXPECT_TRUE(image.Write("test.exr")); + Image read; + EXPECT_TRUE(Image::Read("test.exr", &read)); + + EXPECT_EQ(image.resolution, read.resolution); + EXPECT_EQ(read.format, PixelFormat::RGB16); + + for (int y = 0; y < res[1]; ++y) + for (int x = 0; x < res[0]; ++x) + for (int c = 0; c < 3; ++c) + EXPECT_EQ(HalfToFloat(FloatToHalf(image.GetChannel({x, y}, c))), + read.GetChannel({x, y}, c)); + EXPECT_EQ(0, remove("test.exr")); +} + +TEST(Image, TgaRgbIO) { + Point2i res(11, 48); + std::vector rgbPixels = GetFloatPixels(res, 3); + + Image image(rgbPixels, PixelFormat::RGB32, res); + EXPECT_TRUE(image.Write("test.tga")); + Image read; + EXPECT_TRUE(Image::Read("test.tga", &read)); + + EXPECT_EQ(image.resolution, read.resolution); + EXPECT_EQ(read.format, PixelFormat::SRGB8); + + for (int y = 0; y < res[1]; ++y) + for (int x = 0; x < res[0]; ++x) + for (int c = 0; c < 3; ++c) + EXPECT_EQ(sRGBRoundTrip(image.GetChannel({x, y}, c)), + read.GetChannel({x, y}, c)) + << " x " << x << ", y " << y << ", c " << c << ", orig " + << rgbPixels[3 * y * res[0] + 3 * x + c]; + + EXPECT_EQ(0, remove("test.tga")); +} + +TEST(Image, PngRgbIO) { + Point2i res(11, 50); + std::vector rgbPixels = GetFloatPixels(res, 3); + + Image image(rgbPixels, PixelFormat::RGB32, res); + EXPECT_TRUE(image.Write("test.png")); + Image read; + EXPECT_TRUE(Image::Read("test.png", &read)); + + EXPECT_EQ(image.resolution, read.resolution); + EXPECT_EQ(read.format, PixelFormat::SRGB8); + + for (int y = 0; y < res[1]; ++y) + for (int x = 0; x < res[0]; ++x) + for (int c = 0; c < 3; ++c) + EXPECT_EQ(sRGBRoundTrip(image.GetChannel({x, y}, c)), + read.GetChannel({x, y}, c)) + << " x " << x << ", y " << y << ", c " << c << ", orig " + << rgbPixels[3 * y * res[0] + 3 * x + c]; + + EXPECT_EQ(0, remove("test.png")); +} + +/////////////////////////////////////////////////////////////////////////// + +TEST(ImageTexelProvider, Y32) { + Point2i res(32, 8); + + // Must be a power of 2, so that the base image isn't resampled when + // generating the MIP levels. + ASSERT_TRUE(IsPowerOf2(res[0]) && IsPowerOf2(res[1])); + PixelFormat format = PixelFormat::Y32; + ASSERT_EQ(1, nChannels(format)); + + std::vector pixels = GetFloatPixels(res, nChannels(format)); + Image image(pixels, format, res); + ImageTexelProvider provider(image, WrapMode::Clamp, + SpectrumType::Reflectance); + + for (Point2i p : Bounds2i({0, 0}, res)) { + Float pv = provider.TexelFloat(0, p); + EXPECT_EQ(image.GetY(p), pv); + EXPECT_EQ(pixels[p.x + p.y * res.x], pv); + } +} + +TEST(ImageTexelProvider, RGB32) { + Point2i res(2, 4); //16, 32); + // Must be a power of 2, so that the base image isn't resampled when + // generating the MIP levels. + ASSERT_TRUE(IsPowerOf2(res[0]) && IsPowerOf2(res[1])); + PixelFormat format = PixelFormat::RGB32; + ASSERT_EQ(3, nChannels(format)); + + std::vector pixels = GetFloatPixels(res, nChannels(format)); + Image image(pixels, format, res); + ImageTexelProvider provider(image, WrapMode::Clamp, + SpectrumType::Reflectance); + + for (Point2i p : Bounds2i({0, 0}, res)) { + Spectrum is = image.GetSpectrum(p); + Spectrum ps = provider.TexelSpectrum(0, p); + EXPECT_EQ(is, ps) << "At pixel " << p << ", image gives : " << is << + ", image provider gives " << ps; + Float rgb[3]; + is.ToRGB(rgb); + for (int c = 0; c < 3; ++c) { + EXPECT_EQ(pixels[3 * (p.x + p.y * res.x) + c], rgb[c]); + } + } +} + +#if 0 +TEST(TiledTexelProvider, Y32) { + Point2i res(32, 8); + TestFloatProvider(res, PixelFormat::Y32); +} +#endif + +#if 0 +TEST(TiledTexelProvider, RGB32) { + Point2i res(16, 32); + + ASSERT_TRUE(IsPowerOf2(res[0]) && IsPowerOf2(res[1])); + PixelFormat format = PixelFormat::RGB32; + ASSERT_EQ(3, nChannels(format)); + + std::vector pixels = GetFloatPixels(res, nChannels(format)); + Image image(pixels, format, res); + const char *fn = "tiledprovider.pfm"; + image.Write(fn); + TiledTexelProvider provider(fn, WrapMode::Clamp, SpectrumType::Reflectance, + false); + + for (Point2i p : Bounds2i({0, 0}, res)) { + Spectrum is = image.GetSpectrum(p); + Spectrum ps = provider.TexelSpectrum(0, p); + // FIXME: this doesn't work with the flip above :-p + // CO EXPECT_EQ(is, ps) << is << "vs " << ps; + Float rgb[3]; + ps.ToRGB(rgb); + for (int c = 0; c < 3; ++c) { + EXPECT_EQ(pixels[3 * (p.x + p.y * res.x) + c], rgb[c]); + } + } + + EXPECT_EQ(0, remove(fn)); +} +#endif diff --git a/src/tests/imageio.cpp b/src/tests/imageio.cpp index e81ee33339..3f8989134f 100644 --- a/src/tests/imageio.cpp +++ b/src/tests/imageio.cpp @@ -1,40 +1,38 @@ #include "tests/gtest/gtest.h" #include "pbrt.h" +#include "image.h" #include "fileutil.h" #include "spectrum.h" -#include "imageio.h" using namespace pbrt; static void TestRoundTrip(const char *fn, bool gamma) { Point2i res(16, 29); - std::vector pixels(3 * res[0] * res[1]); + Image image(PixelFormat::RGB32, res); for (int y = 0; y < res[1]; ++y) for (int x = 0; x < res[0]; ++x) { - int offset = 3 * (y * res[0] + x); - pixels[offset] = Float(x) / Float(res[0] - 1); - pixels[offset + 1] = Float(y) / Float(res[1] - 1); - pixels[offset + 2] = -1.5f; + Float rgb[3] = {Float(x) / Float(res[0] - 1), + Float(y) / Float(res[1] - 1), Float(-1.5)}; + Spectrum s = Spectrum::FromRGB(rgb); + image.SetSpectrum({x, y}, s); } - WriteImage(fn, &pixels[0], Bounds2i({0, 0}, res), res); + ASSERT_TRUE(image.Write(fn)); Point2i readRes; - auto readPixels = ReadImage(fn, &readRes); - ASSERT_TRUE(readPixels.get() != nullptr); - EXPECT_EQ(readRes, res); + Image readImage; + ASSERT_TRUE(Image::Read(fn, &readImage)); + ASSERT_EQ(readImage.resolution, res); for (int y = 0; y < res[1]; ++y) for (int x = 0; x < res[0]; ++x) { + Spectrum s = readImage.GetSpectrum({x, y}); Float rgb[3]; - readPixels[y * res[0] + x].ToRGB(rgb); + s.ToRGB(rgb); for (int c = 0; c < 3; ++c) { - if (gamma) - rgb[c] = InverseGammaCorrect(rgb[c]); - - float wrote = pixels[3 * (y * res[0] + x) + c]; + float wrote = image.GetChannel({x, y}, c); float delta = wrote - rgb[c]; if (HasExtension(fn, "pfm")) { // Everything should come out exact. @@ -72,6 +70,8 @@ static void TestRoundTrip(const char *fn, bool gamma) { } } } + + EXPECT_EQ(0, remove(fn)); } TEST(ImageIO, RoundTripEXR) { TestRoundTrip("out.exr", false); } diff --git a/src/tests/texcache.cpp b/src/tests/texcache.cpp new file mode 100644 index 0000000000..6dcb9d6020 --- /dev/null +++ b/src/tests/texcache.cpp @@ -0,0 +1,181 @@ + +/* + pbrt source code is Copyright(c) 1998-2016 + Matt Pharr, Greg Humphreys, and Wenzel Jakob. + + This file is part of pbrt. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are + met: + + - Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + - Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS + IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + */ + +// tests/texcache.cpp* +#include "tests/gtest/gtest.h" + +#include +#include "half.h" +#include "image.h" +#include "mipmap.h" +#include "parallel.h" +#include "pbrt.h" +#include "rng.h" +#include "texcache.h" + +using namespace pbrt; + +// Returns an image of the given format and resolution with random pixel +// values. +static Image GetImage(PixelFormat format, Point2i res) { + RNG rng; + + auto v = [format, &rng]() -> Float { + if (Is8Bit(format)) + return rng.UniformFloat(); + else if (Is16Bit(format)) + return Lerp(rng.UniformFloat(), -100., 100.); + else { + EXPECT_TRUE(Is32Bit(format)); + return Lerp(rng.UniformFloat(), -1e6f, 1e6f); + } + }; + + Image image(format, res); + for (int y = 0; y < res.y; ++y) + for (int x = 0; x < res.x; ++x) + for (int c = 0; c < image.nChannels(); ++c) { + Float val = v(); + image.SetChannel({x, y}, c, val); + } + return image; +} + +// Create a tiled texture with the given pixel format and tile size; +// then, add it to a texture cache and verify that all texels in all +// MIP levels exactly match the values in the original MIP chain. +static void TestFormat(PixelFormat format) { + ParallelInit(); + + std::unique_ptr cache(new TextureCache); + Image image = GetImage(format, {129, 60}); + + char filename[64]; + sprintf(filename, "tx_fmt-%d", int(format)); + WrapMode wrapMode = WrapMode::Clamp; + std::vector mips = image.GenerateMIPMap(wrapMode); + int tileSize = TextureCache::TileSize(format); + ASSERT_TRUE(TiledImagePyramid::Create(mips, filename, wrapMode, tileSize)); + + int id = cache->AddTexture(filename); + EXPECT_GE(id, 0); + + for (size_t level = 0; level < mips.size(); ++level) { + EXPECT_EQ(cache->GetPixelFormat(id), format); + EXPECT_EQ(cache->GetPixelFormat(id), mips[level].format); + ASSERT_EQ(cache->GetLevelResolution(id, level), mips[level].resolution); + + for (int y = 0; y < mips[level].resolution.y; ++y) + for (int x = 0; x < mips[level].resolution.x; ++x) { + if (nChannels(format) == 1) { + Float val = cache->Texel(id, level, {x, y}); + EXPECT_EQ(mips[level].GetChannel({x, y}, 0), val); + } else { + ASSERT_EQ(3, nChannels(format)); + Spectrum s = cache->Texel(id, level, {x, y}); + for (int c = 0; c < 3; ++c) + EXPECT_EQ(mips[level].GetChannel({x, y}, c), s[c]); + } + } + } + EXPECT_EQ(0, remove(filename)); + ParallelCleanup(); +} + +TEST(Texcache, SY8) { TestFormat(PixelFormat::SY8); } + +TEST(Texcache, Y8) { TestFormat(PixelFormat::Y8); } + +TEST(Texcache, Y16) { TestFormat(PixelFormat::Y16); } + +TEST(Texcache, Y32) { TestFormat(PixelFormat::Y32); } + +TEST(Texcache, SRGB8) { TestFormat(PixelFormat::SRGB8); } + +TEST(Texcache, RGB8) { TestFormat(PixelFormat::RGB8); } + +TEST(Texcache, RGB16) { TestFormat(PixelFormat::RGB16); } + +TEST(Texcache, RGB32) { TestFormat(PixelFormat::RGB32); } + +TEST(Texcache, ThreadInsanity) { + ParallelInit(); + + // Create a bunch of images with random sizes and contents. + std::vector> images; + std::vector filenames; + RNG rng; + for (int i = 0; i < 100; ++i) { + Point2i res(2 + rng.UniformUInt32(1000), 2 + rng.UniformUInt32(1000)); + Image im = GetImage(PixelFormat::SRGB8, res); + // Just do one level; save the time of creating MIP levels... + std::vector mips{im}; + images.push_back(mips); + std::string filename = StringPrintf("img-%d-%d.txp", res.x, res.y); + filenames.push_back(filename); + int tileSize = TextureCache::TileSize(im.format); + ASSERT_TRUE(TiledImagePyramid::Create(mips, filename, WrapMode::Clamp, + tileSize)); + } + + PbrtOptions.texCacheMB = 32; + std::unique_ptr cache(new TextureCache); + ASSERT_TRUE(cache.get() != nullptr); + + // Supply textures to texture cache. + std::vector ids; + for (const auto &fn : filenames) ids.push_back(cache->AddTexture(fn)); + + // Have a bunch of threads hammer on it in parallel. + ParallelFor( + [&](int64_t chunk) { + RNG rng(chunk); + for (int i = 0; i < 10000; ++i) { + // Choose a random texture and level. + int texIndex = rng.UniformUInt32(ids.size()); + int texId = ids[texIndex]; + int level = rng.UniformUInt32(images[texIndex].size()); + Point2i res = cache->GetLevelResolution(texId, level); + + // Choose a random point in the texture. + Point2i p(rng.UniformUInt32(res[0]), rng.UniformUInt32(res[1])); + + Spectrum v = cache->Texel(texId, level, p); + EXPECT_EQ(v, images[texIndex][level].GetSpectrum(p)); + } + }, + 1000); + + for (const auto &fn : filenames) EXPECT_EQ(0, remove(fn.c_str())); + + ParallelCleanup(); +} diff --git a/src/textures/imagemap.cpp b/src/textures/imagemap.cpp index 2902682a40..f587bbbb56 100644 --- a/src/textures/imagemap.cpp +++ b/src/textures/imagemap.cpp @@ -39,70 +39,45 @@ namespace pbrt { // ImageTexture Method Definitions -template -ImageTexture::ImageTexture( +template +ImageTexture::ImageTexture( std::unique_ptr mapping, const std::string &filename, - bool doTrilinear, Float maxAniso, ImageWrap wrapMode, Float scale, + const std::string &filter, Float maxAniso, WrapMode wrapMode, Float scale, bool gamma) - : mapping(std::move(mapping)) { + : mapping(std::move(mapping)), scale(scale) { mipmap = - GetTexture(filename, doTrilinear, maxAniso, wrapMode, scale, gamma); + GetTexture(filename, filter, maxAniso, wrapMode, gamma); } -template -MIPMap *ImageTexture::GetTexture( - const std::string &filename, bool doTrilinear, Float maxAniso, - ImageWrap wrap, Float scale, bool gamma) { +template +MIPMap *ImageTexture::GetTexture( + const std::string &filename, const std::string &filter, Float maxAniso, + WrapMode wrap, bool gamma) { // Return _MIPMap_ from texture cache if present - TexInfo texInfo(filename, doTrilinear, maxAniso, wrap, scale, gamma); + TexInfo texInfo(filename, filter, maxAniso, wrap, gamma); if (textures.find(texInfo) != textures.end()) return textures[texInfo].get(); // Create _MIPMap_ for _filename_ ProfilePhase _(Prof::TextureLoading); - Point2i resolution; - std::unique_ptr texels = ReadImage(filename, &resolution); - if (!texels) { - Warning("Creating a constant grey texture to replace \"%s\".", - filename.c_str()); - resolution.x = resolution.y = 1; - RGBSpectrum *rgb = new RGBSpectrum[1]; - *rgb = RGBSpectrum(0.5f); - texels.reset(rgb); - } - - // Flip image in y; texture coordinate space has (0,0) at the lower - // left corner. - for (int y = 0; y < resolution.y / 2; ++y) - for (int x = 0; x < resolution.x; ++x) { - int o1 = y * resolution.x + x; - int o2 = (resolution.y - 1 - y) * resolution.x + x; - std::swap(texels[o1], texels[o2]); - } - - MIPMap *mipmap = nullptr; - if (texels) { - // Convert texels to type _Tmemory_ and create _MIPMap_ - std::unique_ptr convertedTexels( - new Tmemory[resolution.x * resolution.y]); - for (int i = 0; i < resolution.x * resolution.y; ++i) - convertIn(texels[i], &convertedTexels[i], scale, gamma); - mipmap = new MIPMap(resolution, convertedTexels.get(), - doTrilinear, maxAniso, wrap); - } else { - // Create one-valued _MIPMap_ - Tmemory oneVal = scale; - mipmap = new MIPMap(Point2i(1, 1), &oneVal); - } - textures[texInfo].reset(mipmap); - return mipmap; + MIPMapFilterOptions options; + options.maxAnisotropy = maxAniso; + if (!ParseFilter(filter, &options.filter)) + Warning("%s: filter function unknown", filter.c_str()); + std::unique_ptr mipmap = + MIPMap::CreateFromFile(filename, options, wrap, gamma); + if (mipmap) { + textures[texInfo] = std::move(mipmap); + return textures[texInfo].get(); + } else + return nullptr; } -template -std::map>> - ImageTexture::textures; -ImageTexture *CreateImageFloatTexture(const Transform &tex2world, - const TextureParams &tp) { +template +std::map> ImageTexture::textures; + +ImageTexture *CreateImageFloatTexture(const Transform &tex2world, + const TextureParams &tp) { // Initialize 2D texture mapping _map_ from _tp_ std::unique_ptr map; std::string type = tp.FindString("mapping", "uv"); @@ -128,22 +103,21 @@ ImageTexture *CreateImageFloatTexture(const Transform &tex2world, // Initialize _ImageTexture_ parameters Float maxAniso = tp.FindFloat("maxanisotropy", 8.f); - bool trilerp = tp.FindBool("trilinear", false); + std::string filter = tp.FindString("filter", "bilinear"); std::string wrap = tp.FindString("wrap", "repeat"); - ImageWrap wrapMode = ImageWrap::Repeat; - if (wrap == "black") - wrapMode = ImageWrap::Black; - else if (wrap == "clamp") - wrapMode = ImageWrap::Clamp; + WrapMode wrapMode; + std::string wrapString = tp.FindString("wrap", "repeat"); + if (!ParseWrapMode(wrapString.c_str(), &wrapMode)) + Warning("%s: wrap mode unknown", wrapString.c_str()); Float scale = tp.FindFloat("scale", 1.f); std::string filename = tp.FindFilename("filename"); bool gamma = tp.FindBool("gamma", HasExtension(filename, ".tga") || HasExtension(filename, ".png")); - return new ImageTexture(std::move(map), filename, trilerp, - maxAniso, wrapMode, scale, gamma); + return new ImageTexture(std::move(map), filename, filter, + maxAniso, wrapMode, scale, gamma); } -ImageTexture *CreateImageSpectrumTexture( +ImageTexture *CreateImageSpectrumTexture( const Transform &tex2world, const TextureParams &tp) { // Initialize 2D texture mapping _map_ from _tp_ std::unique_ptr map; @@ -170,19 +144,18 @@ ImageTexture *CreateImageSpectrumTexture( // Initialize _ImageTexture_ parameters Float maxAniso = tp.FindFloat("maxanisotropy", 8.f); - bool trilerp = tp.FindBool("trilinear", false); + std::string filter = tp.FindString("filter", "bilinear"); std::string wrap = tp.FindString("wrap", "repeat"); - ImageWrap wrapMode = ImageWrap::Repeat; - if (wrap == "black") - wrapMode = ImageWrap::Black; - else if (wrap == "clamp") - wrapMode = ImageWrap::Clamp; + WrapMode wrapMode; + std::string wrapString = tp.FindString("wrap", "repeat"); + if (!ParseWrapMode(wrapString.c_str(), &wrapMode)) + Warning("%s: wrap mode unknown", wrapString.c_str()); Float scale = tp.FindFloat("scale", 1.f); std::string filename = tp.FindFilename("filename"); bool gamma = tp.FindBool("gamma", HasExtension(filename, ".tga") || HasExtension(filename, ".png")); - return new ImageTexture( - std::move(map), filename, trilerp, maxAniso, wrapMode, scale, gamma); + return new ImageTexture( + std::move(map), filename, filter, maxAniso, wrapMode, scale, gamma); } } // namespace pbrt diff --git a/src/textures/imagemap.h b/src/textures/imagemap.h index 8de3b7c53c..91e4788fcb 100644 --- a/src/textures/imagemap.h +++ b/src/textures/imagemap.h @@ -49,80 +49,61 @@ namespace pbrt { // TexInfo Declarations struct TexInfo { - TexInfo(const std::string &f, bool dt, Float ma, ImageWrap wm, Float sc, + TexInfo(const std::string &f, const std::string &filt, Float ma, WrapMode wm, bool gamma) : filename(f), - doTrilinear(dt), + filter(filt), maxAniso(ma), wrapMode(wm), - scale(sc), gamma(gamma) {} std::string filename; - bool doTrilinear; + std::string filter; Float maxAniso; - ImageWrap wrapMode; - Float scale; + WrapMode wrapMode; bool gamma; bool operator<(const TexInfo &t2) const { - if (filename != t2.filename) return filename < t2.filename; - if (doTrilinear != t2.doTrilinear) return doTrilinear < t2.doTrilinear; - if (maxAniso != t2.maxAniso) return maxAniso < t2.maxAniso; - if (scale != t2.scale) return scale < t2.scale; - if (gamma != t2.gamma) return !gamma; - return wrapMode < t2.wrapMode; + return std::tie(filename, filter, maxAniso, gamma, wrapMode) < + std::tie(t2.filename, t2.filter, t2.maxAniso, t2.gamma, t2.wrapMode); } }; // ImageTexture Declarations -template -class ImageTexture : public Texture { +template +class ImageTexture : public Texture { public: // ImageTexture Public Methods ImageTexture(std::unique_ptr m, - const std::string &filename, bool doTri, Float maxAniso, - ImageWrap wm, Float scale, bool gamma); + const std::string &filename, const std::string &filter, + Float maxAniso, WrapMode wm, Float scale, bool gamma); static void ClearCache() { textures.erase(textures.begin(), textures.end()); } - Treturn Evaluate(const SurfaceInteraction &si) const { + T Evaluate(const SurfaceInteraction &si) const { + if (!mipmap) return T(scale); Vector2f dstdx, dstdy; Point2f st = mapping->Map(si, &dstdx, &dstdy); - Tmemory mem = mipmap->Lookup(st, dstdx, dstdy); - Treturn ret; - convertOut(mem, &ret); - return ret; + // Texture coordinates are (0,0) in the lower left corner, but + // image coordinates are (0,0) in the upper left. + st[1] = 1 - st[1]; + return scale * mipmap->template Lookup(st, dstdx, dstdy); } private: // ImageTexture Private Methods - static MIPMap *GetTexture(const std::string &filename, - bool doTrilinear, Float maxAniso, - ImageWrap wm, Float scale, bool gamma); - static void convertIn(const RGBSpectrum &from, RGBSpectrum *to, Float scale, - bool gamma) { - for (int i = 0; i < RGBSpectrum::nSamples; ++i) - (*to)[i] = scale * (gamma ? InverseGammaCorrect(from[i]) : from[i]); - } - static void convertIn(const RGBSpectrum &from, Float *to, Float scale, - bool gamma) { - *to = scale * (gamma ? InverseGammaCorrect(from.y()) : from.y()); - } - static void convertOut(const RGBSpectrum &from, Spectrum *to) { - Float rgb[3]; - from.ToRGB(rgb); - *to = Spectrum::FromRGB(rgb); - } - static void convertOut(Float from, Float *to) { *to = from; } + static MIPMap *GetTexture( + const std::string &filename, const std::string &filter, Float maxAniso, + WrapMode wm, bool gamma); // ImageTexture Private Data std::unique_ptr mapping; - MIPMap *mipmap; - static std::map>> textures; + const Float scale; + MIPMap *mipmap; + static std::map> textures; }; -ImageTexture *CreateImageFloatTexture(const Transform &tex2world, - const TextureParams &tp); -ImageTexture *CreateImageSpectrumTexture( +ImageTexture *CreateImageFloatTexture(const Transform &tex2world, + const TextureParams &tp); +ImageTexture *CreateImageSpectrumTexture( const Transform &tex2world, const TextureParams &tp); } // namespace pbrt diff --git a/src/tools/imgtool.cpp b/src/tools/imgtool.cpp index 5dd6a66de9..1b54010470 100644 --- a/src/tools/imgtool.cpp +++ b/src/tools/imgtool.cpp @@ -11,9 +11,10 @@ #include #include "fileutil.h" #include "imageio.h" +#include "mipmap.h" +#include "parallel.h" #include "pbrt.h" #include "spectrum.h" -#include "parallel.h" extern "C" { #include "ext/ArHosekSkyModel.h" } @@ -31,7 +32,7 @@ static void usage(const char *msg = nullptr, ...) { } fprintf(stderr, R"(usage: imgtool [options] -commands: assemble, cat, convert, diff, info, makesky +commands: assemble, cat, convert, diff, info, makesky, maketiled assemble option: --outfile Output image filename. @@ -80,6 +81,10 @@ makesky options: (Horizontal resolution is twice this value.) Default: 2048 +maketiled options: + --wrapmode Image wrap mode used for out-of-bounds texture accesses. + Options: "clamp", "repeat", "black". Default: "clamp". + )"); exit(1); } @@ -154,35 +159,41 @@ int makesky(int argc, char *argv[]) { Vector3f sunDir(0., std::sin(elevation), std::cos(elevation)); int nTheta = resolution, nPhi = 2 * nTheta; - std::vector img(3 * nTheta * nPhi, 0.f); + Image img(PixelFormat::RGB32, {nPhi, nTheta}); + ParallelInit(); - ParallelFor([&](int64_t t) { - Float theta = float(t + 0.5) / nTheta * Pi; - if (theta > Pi / 2.) return; - for (int p = 0; p < nPhi; ++p) { - Float phi = float(p + 0.5) / nPhi * 2. * Pi; - - // Vector corresponding to the direction for this pixel. - Vector3f v(std::cos(phi) * std::sin(theta), std::cos(theta), - std::sin(phi) * std::sin(theta)); - // Compute the angle between the pixel's direction and the sun - // direction. - Float gamma = std::acos(Clamp(Dot(v, sunDir), -1, 1)); - CHECK(gamma >= 0 && gamma <= Pi); - - for (int c = 0; c < num_channels; ++c) { - float val = arhosekskymodel_solar_radiance( - skymodel_state[c], theta, gamma, lambda[c]); - // For each of red, green, and blue, average the three - // values for the three wavelengths for the color. - // TODO: do a better spectral->RGB conversion. - img[3 * (t * nPhi + p) + c / 3] += val / 3.f; + ParallelFor( + [&](int64_t t) { + Float theta = float(t + 0.5) / nTheta * Pi; + if (theta > Pi / 2.) return; + for (int p = 0; p < nPhi; ++p) { + Float phi = float(p + 0.5) / nPhi * 2. * Pi; + + // Vector corresponding to the direction for this pixel. + Vector3f v(std::cos(phi) * std::sin(theta), std::cos(theta), + std::sin(phi) * std::sin(theta)); + // Compute the angle between the pixel's direction and the sun + // direction. + Float gamma = std::acos(Clamp(Dot(v, sunDir), -1, 1)); + CHECK(gamma >= 0 && gamma <= Pi); + + Float rgb[3] = {Float(0), Float(0), Float(0)}; + for (int c = 0; c < num_channels; ++c) { + float val = arhosekskymodel_solar_radiance( + skymodel_state[c], theta, gamma, lambda[c]); + // For each of red, green, and blue, average the three + // values for the three wavelengths for the color. + // TODO: do a better spectral->RGB conversion. + rgb[c / 3] += val / 3.f; + } + for (int c = 0; c < 3; ++c) + img.SetChannel({p, (int)t}, c, rgb[c]); } - } - }, nTheta, 32); + }, + nTheta, 32); + + CHECK(img.Write(outfile)); - WriteImage(outfile, (Float *)&img[0], Bounds2i({0, 0}, {nPhi, nTheta}), - {nPhi, nTheta}); ParallelCleanup(); return 0; } @@ -204,7 +215,7 @@ int assemble(int argc, char *argv[]) { if (!outfile) usage("--outfile not provided for \"assemble\""); - std::unique_ptr fullImg; + Image fullImage; std::vector seenPixel; int seenMultiple = 0; Point2i fullRes; @@ -217,14 +228,13 @@ int assemble(int argc, char *argv[]) { Bounds2i dataWindow, dspw; Point2i res; - std::unique_ptr img( - ReadImageEXR(file, &res.x, &res.y, &dataWindow, &dspw)); - if (!img) continue; + Image img; + if (!Image::Read(file, &img, true, &dataWindow, &dspw)) continue; - if (!fullImg) { + if (fullImage.resolution == Point2i(0, 0)) { // First image read. fullRes = Point2i(dspw.pMax - dspw.pMin); - fullImg.reset(new RGBSpectrum[fullRes.x * fullRes.y]); + fullImage = Image(img.format, fullRes); seenPixel.resize(fullRes.x * fullRes.y); displayWindow = dspw; } else { @@ -253,6 +263,12 @@ int assemble(int argc, char *argv[]) { displayWindow.pMax.x, displayWindow.pMax.y); continue; } + if (fullImage.nChannels() != img.nChannels()) { + fprintf(stderr, + "%s: %d channel image; expecting %d channels.\n", file, + img.nChannels(), fullImage.nChannels()); + continue; + } } // Copy pixels. @@ -260,9 +276,11 @@ int assemble(int argc, char *argv[]) { for (int x = 0; x < res.x; ++x) { int fullOffset = (y + dataWindow.pMin.y) * fullRes.x + (x + dataWindow.pMin.x); - fullImg[fullOffset] = img[y * res.x + x]; if (seenPixel[fullOffset]) ++seenMultiple; seenPixel[fullOffset] = true; + Point2i fullXY{x + dataWindow.pMin.x, y + dataWindow.pMin.y}; + for (int c = 0; c < fullImage.nChannels(); ++c) + fullImage.SetChannel(fullXY, c, img.GetChannel({x, y}, c)); } } @@ -278,8 +296,7 @@ int assemble(int argc, char *argv[]) { fprintf(stderr, "%s: %d pixels not present in any images.\n", outfile, unseenPixels); - // TODO: fix yet another bad cast here. - WriteImage(outfile, (Float *)fullImg.get(), displayWindow, fullRes); + fullImage.Write(outfile); return 0; } @@ -294,34 +311,42 @@ int cat(int argc, char *argv[]) { continue; } - Point2i res; - std::unique_ptr img = ReadImage(argv[i], &res); - if (!img) continue; + Image img; + if (!Image::Read(argv[i], &img)) { + fprintf(stderr, "imgtool: couldn't open \"%s\".\n", argv[i]); + continue; + } + if (sort) { - std::vector> sorted; - sorted.reserve(res.x * res.y); - for (int y = 0; y < res.y; ++y) { - for (int x = 0; x < res.x; ++x) { - int offset = y * res.x + x; - sorted.push_back({offset, img[offset]}); + std::vector>> sorted; + sorted.reserve(img.resolution.x * img.resolution.y); + for (int y = 0; y < img.resolution.y; ++y) + for (int x = 0; x < img.resolution.x; ++x) { + Spectrum s = img.GetSpectrum({x, y}); + std::array rgb; + s.ToRGB(&rgb[0]); + sorted.push_back(std::make_tuple(x, y, rgb)); } - } + std::sort(sorted.begin(), sorted.end(), - [](const std::pair &a, - const std::pair &b) { - return a.second.y() < b.second.y(); + [](const std::tuple> &a, + const std::tuple> &b) { + const std::array ac = std::get<2>(a); + const std::array bc = std::get<2>(b); + return (ac[0] + ac[1] + ac[2]) < + (bc[0] + bc[1] + bc[2]); }); for (const auto &v : sorted) { - Float rgb[3]; - v.second.ToRGB(rgb); - printf("(%d, %d): (%.9g %.9g %.9g)\n", v.first / res.x, - v.first % res.x, rgb[0], rgb[1], rgb[2]); + const std::array &rgb = std::get<2>(v); + printf("(%d, %d): (%.9g %.9g %.9g)\n", std::get<0>(v), + std::get<1>(v), rgb[0], rgb[1], rgb[2]); } } else { - for (int y = 0; y < res.y; ++y) { - for (int x = 0; x < res.x; ++x) { - Float rgb[3]; - img[y * res.x + x].ToRGB(rgb); + for (int y = 0; y < img.resolution.y; ++y) { + for (int x = 0; x < img.resolution.x; ++x) { + Spectrum s = img.GetSpectrum({x, y}); + std::array rgb; + s.ToRGB(&rgb[0]); printf("(%d, %d): (%.9g %.9g %.9g)\n", x, y, rgb[0], rgb[1], rgb[2]); } @@ -367,57 +392,54 @@ int diff(int argc, char *argv[]) { usage("excess filenames provided to \"diff\""); const char *filename[2] = {argv[i], argv[i + 1]}; - Point2i res[2]; - std::unique_ptr imgs[2] = {ReadImage(filename[0], &res[0]), - ReadImage(filename[1], &res[1])}; - if (!imgs[0]) { - fprintf(stderr, "%s: unable to read image\n", filename[0]); - return 1; - } - if (!imgs[1]) { - fprintf(stderr, "%s: unable to read image\n", filename[1]); - return 1; - } - if (res[0] != res[1]) { + Image img[2]; + for (int i = 0; i < 2; ++i) + if (!Image::Read(filename[i], &img[i])) { + fprintf(stderr, "%s: unable to read image\n", filename[i]); + return 1; + } + + if (img[0].resolution != img[1].resolution) { fprintf(stderr, "imgtool: image resolutions don't match \"%s\": (%d, %d) " "\"%s\": (%d, %d)\n", - filename[0], res[0].x, res[0].y, filename[1], res[1].x, - res[1].y); + filename[0], img[0].resolution.x, img[0].resolution.y, + filename[1], img[1].resolution.x, img[1].resolution.y); return 1; } - std::unique_ptr diffImage; - if (outfile) diffImage.reset(new RGBSpectrum[res[0].x * res[0].y]); + Point2i res = img[0].resolution; + Image diffImage(PixelFormat::RGB32, res); double sum[2] = {0., 0.}; int smallDiff = 0, bigDiff = 0; double mse = 0.f; - for (int i = 0; i < res[0].x * res[0].y; ++i) { - Float rgb[2][3]; - imgs[0][i].ToRGB(rgb[0]); - imgs[1][i].ToRGB(rgb[1]); + for (int y = 0; y < res.y; ++y) { + for (int x = 0; x < res.x; ++x) { + Spectrum s[2] = {img[0].GetSpectrum({x, y}), + img[1].GetSpectrum({x, y})}; + Spectrum diff; - Float diffRGB[3]; - for (int c = 0; c < 3; ++c) { - Float c0 = rgb[0][c], c1 = rgb[1][c]; - diffRGB[c] = std::abs(c0 - c1); + for (int c = 0; c < Spectrum::nSamples; ++c) { + Float c0 = s[0][c], c1 = s[1][c]; + diff[c] = std::abs(c0 - c1); - if (c0 == 0 && c1 == 0) continue; + if (c0 == 0 && c1 == 0) continue; - sum[0] += c0; - sum[1] += c1; + sum[0] += c0; + sum[1] += c1; - float d = std::abs(c0 - c1) / c0; - mse += (c0 - c1) * (c0 - c1); - if (d > .005) ++smallDiff; - if (d > .05) ++bigDiff; + float d = std::abs(c0 - c1) / c0; + mse += (c0 - c1) * (c0 - c1); + if (d > .005) ++smallDiff; + if (d > .05) ++bigDiff; + } + diffImage.SetSpectrum({x, y}, diff); } - if (diffImage) diffImage[i].FromRGB(diffRGB); } - double avg[2] = {sum[0] / (3. * res[0].x * res[0].y), - sum[1] / (3. * res[0].x * res[0].y)}; + double avg[2] = {sum[0] / (Spectrum::nSamples * res.x * res.y), + sum[1] / (Spectrum::nSamples * res.x * res.y)}; double avgDelta = (avg[0] - avg[1]) / std::min(avg[0], avg[1]); if ((tol == 0. && (bigDiff > 0 || smallDiff > 0)) || (tol > 0. && 100.f * std::abs(avgDelta) > tol)) { @@ -426,14 +448,14 @@ int diff(int argc, char *argv[]) { "\tavg 1 = %g, avg2 = %g (%f%% delta)\n" "\tMSE = %g, RMS = %.3f%%\n", filename[0], filename[1], bigDiff, - 100.f * float(bigDiff) / (3 * res[0].x * res[0].y), smallDiff, - 100.f * float(smallDiff) / (3 * res[0].x * res[0].y), avg[0], - avg[1], 100. * avgDelta, mse / (3. * res[0].x * res[0].y), - 100. * sqrt(mse / (3. * res[0].x * res[0].y))); + 100.f * float(bigDiff) / (3 * res.x * res.y), smallDiff, + 100.f * float(smallDiff) / (3 * res.x * res.y), avg[0], avg[1], + 100. * avgDelta, mse / (3. * res.x * res.y), + 100. * sqrt(mse / (3. * res.x * res.y))); if (outfile) { - // FIXME: diffImage cast is bad. - WriteImage(outfile, (Float *)diffImage.get(), - Bounds2i(Point2i(0, 0), res[0]), res[0]); + if (!diffImage.Write(outfile)) + fprintf(stderr, "imgtool: unable to write \"%s\": %s\n", + outfile, strerror(errno)); } return 1; } @@ -441,71 +463,113 @@ int diff(int argc, char *argv[]) { return 0; } -int info(int argc, char *argv[]) { - int err = 0; - for (int i = 0; i < argc; ++i) { - Point2i res; - std::unique_ptr image = ReadImage(argv[i], &res); - if (!image) { - fprintf(stderr, "%s: unable to load image.\n", argv[i]); - err = 1; - continue; - } - - printf("%s: resolution %d, %d\n", argv[i], res.x, res.y); - Float min[3] = {Infinity, Infinity, Infinity}; - Float max[3] = {-Infinity, -Infinity, -Infinity}; - double sum[3] = {0., 0., 0.}; - double logYSum = 0.; - int nNaN = 0, nInf = 0, nValid = 0; - for (int i = 0; i < res.x * res.y; ++i) { - Float y = image[i].y(); - if (!std::isnan(y) && !std::isinf(y)) - logYSum += std::log(Float(1e-6) + y); - - Float rgb[3]; - image[i].ToRGB(rgb); - for (int c = 0; c < 3; ++c) { - if (std::isnan(rgb[c])) +static void printImageStats(const char *name, const Image &image) { + printf("%s: resolution %d, %d\n", name, image.resolution.x, + image.resolution.y); + Float min[3] = {Infinity, Infinity, Infinity}; + Float max[3] = {-Infinity, -Infinity, -Infinity}; + double sum[3] = {0., 0., 0.}; + double logYSum = 0.; + int nNaN = 0, nInf = 0, nValid = 0; + int nc = image.nChannels(); + CHECK_LE(nc, 3); // fixed-sized arrays above... + for (int y = 0; y < image.resolution.y; ++y) + for (int x = 0; x < image.resolution.x; ++x) { + Float lum = image.GetY({x, y}); + if (!std::isnan(lum) && !std::isinf(lum)) + logYSum += std::log(Float(1e-6) + lum); + + for (int c = 0; c < nc; ++c) { + Float v = image.GetChannel({x, y}, c); + if (std::isnan(v)) ++nNaN; - else if (std::isinf(rgb[c])) + else if (std::isinf(v)) ++nInf; else { - min[c] = std::min(min[c], rgb[c]); - max[c] = std::max(max[c], rgb[c]); - sum[c] += rgb[c]; + min[c] = std::min(min[c], v); + max[c] = std::max(max[c], v); + sum[c] += v; ++nValid; } } } - printf("%s: %d infinite pixel components, %d NaN, %d valid.\n", argv[i], - nInf, nNaN, nValid); - printf("%s: log average luminance %f\n", argv[i], - std::exp(logYSum / (res.x * res.y))); - printf("%s: min rgb (%f, %f, %f)\n", argv[i], min[0], min[1], min[2]); - printf("%s: max rgb (%f, %f, %f)\n", argv[i], max[0], max[1], max[2]); - printf("%s: avg rgb (%f, %f, %f)\n", argv[i], sum[0] / nValid, - sum[1] / nValid, sum[2] / nValid); + + printf("%s: %d infinite pixel components, %d NaN, %d valid.\n", name, nInf, + nNaN, nValid); + printf("%s: log average luminance %f\n", name, + std::exp(logYSum / (image.resolution.x * image.resolution.y))); + printf("%s: min channel:", name); + for (int c = 0; c < nc; ++c) + printf(" %f%c", min[c], (c < nc - 1) ? ',' : ' '); + printf("\n"); + printf("%s: max channel:", name); + for (int c = 0; c < nc; ++c) + printf(" %f%c", max[c], (c < nc - 1) ? ',' : ' '); + printf("\n"); + printf("%s: avg channel:", name); + for (int c = 0; c < nc; ++c) + printf(" %f%c", sum[c] / nValid, (c < nc - 1) ? ',' : ' '); + printf("\n"); +} + +int info(int argc, char *argv[]) { + int err = 0; + for (int i = 0; i < argc; ++i) { + if (HasExtension(argv[i], "txp")) { + std::unique_ptr cache(new TextureCache); + int id = cache->AddTexture(argv[i]); + if (id < 0) { + err = 1; + continue; + } + printf("%s: wrap mode \"%s\"\n", argv[i], + WrapModeString(cache->GetWrapMode(id))); + + for (int level = 0; level < cache->Levels(id); ++level) { + Image image = cache->GetLevelImage(id, level); + printImageStats( + StringPrintf("%s-level%d", argv[i], level).c_str(), image); + } + } else { + Point2i res; + Image image; + if (!Image::Read(argv[i], &image)) { + fprintf(stderr, "%s: unable to load image.\n", argv[i]); + err = 1; + continue; + } + + printImageStats(argv[i], image); + } } return err; } -std::unique_ptr bloom(std::unique_ptr image, - const Point2i &res, Float level, int width, - Float scale, int iters) { - std::vector> blurred; +Image bloom(Image image, Float level, int width, Float scale, int iters) { + std::vector blurred; + CHECK(image.nChannels() == 1 || image.nChannels() == 3); + PixelFormat format = + image.nChannels() == 1 ? PixelFormat::Y32 : PixelFormat::RGB32; // First, threshold the source image int nSurvivors = 0; - std::unique_ptr thresholded(new RGBSpectrum[res.x * res.y]); - for (int i = 0; i < res.x * res.y; ++i) { - Float rgb[3]; - image[i].ToRGB(rgb); - if (rgb[0] > level || rgb[1] > level || rgb[2] > level) { - ++nSurvivors; - thresholded[i] = image[i]; - } else - thresholded[i] = 0.f; + Point2i res = image.resolution; + int nc = image.nChannels(); + Image thresholdedImage(format, image.resolution); + for (int y = 0; y < res.y; ++y) { + for (int x = 0; x < res.x; ++x) { + bool overThreshold = false; + for (int c = 0; c < nc; ++c) + if (image.GetChannel({x, y}, c) > level) overThreshold = true; + if (overThreshold) { + ++nSurvivors; + for (int c = 0; c < nc; ++c) + thresholdedImage.SetChannel({x, y}, c, + image.GetChannel({x, y}, c)); + } else + for (int c = 0; c < nc; ++c) + thresholdedImage.SetChannel({x, y}, c, 0.f); + } } if (nSurvivors == 0) { fprintf(stderr, @@ -513,7 +577,7 @@ std::unique_ptr bloom(std::unique_ptr image, level); return image; } - blurred.push_back(std::move(thresholded)); + blurred.push_back(std::move(thresholdedImage)); if ((width % 2) == 0) { ++width; @@ -536,50 +600,53 @@ std::unique_ptr bloom(std::unique_ptr image, // Normalize filter weights. for (int i = 0; i < width; ++i) wts[i] /= wtSum; - auto getTexel = [&](const std::unique_ptr &img, Point2i p) { - // Clamp at boundaries - if (p.x < 0) p.x = 0; - if (p.x >= res.x) p.x = res.x - 1; - if (p.y < 0) p.y = 0; - if (p.y >= res.y) p.y = res.y - 1; - return img[p.y * res.x + p.x]; - }; - // Now successively blur the thresholded image. - std::unique_ptr blurx(new RGBSpectrum[res.x * res.y]); + Image blurx(format, res); for (int iter = 0; iter < iters; ++iter) { // Separable blur; first blur in x into blurx for (int y = 0; y < res.y; ++y) { for (int x = 0; x < res.x; ++x) { - RGBSpectrum result = 0; - for (int r = -radius; r <= radius; ++r) - result += - wts[r + radius] * getTexel(blurred.back(), {x + r, y}); - blurx[y * res.x + x] = result; + for (int c = 0; c < nc; ++c) { + Float result = 0; + for (int r = -radius; r <= radius; ++r) + result += wts[r + radius] * + blurred.back().GetChannel({x + r, y}, c); + blurx.SetChannel({x, y}, c, result); + } } } // Now blur in y from blur x to the result - std::unique_ptr blury(new RGBSpectrum[res.x * res.y]); + Image blury(format, res); for (int y = 0; y < res.y; ++y) { for (int x = 0; x < res.x; ++x) { - RGBSpectrum result = 0; - for (int r = -radius; r <= radius; ++r) - result += wts[r + radius] * getTexel(blurx, {x, y + r}); - blury[y * res.x + x] = result; + for (int c = 0; c < nc; ++c) { + Float result = 0; + for (int r = -radius; r <= radius; ++r) + result += + wts[r + radius] * blurx.GetChannel({x, y + r}, c); + blury.SetChannel({x, y}, c, result); + } } } blurred.push_back(std::move(blury)); } // Finally, add all of the blurred images, scaled, to the original. - for (int i = 0; i < res.x * res.y; ++i) { - RGBSpectrum blurredSum = 0.f; - // Skip the thresholded image, since it's already present in the - // original; just add pixels from the blurred ones. - for (size_t j = 1; j < blurred.size(); ++j) blurredSum += blurred[j][i]; - image[i] += (scale / iters) * blurredSum; + for (int y = 0; y < res.y; ++y) { + for (int x = 0; x < res.x; ++x) { + for (int c = 0; c < nc; ++c) { + Float blurredSum = 0.f; + // Skip the thresholded image, since it's already + // present in the original; just add pixels from the + // blurred ones. + for (size_t j = 1; j < blurred.size(); ++j) + blurredSum += blurred[j].GetChannel({x, y}, c); + image.SetChannel({x, y}, c, (scale / iters) * blurredSum); + } + } } + return image; } @@ -621,7 +688,8 @@ int convert(int argc, char *argv[]) { flipy = !flipy; else if (!strcmp(argv[i], "--tonemap") || !strcmp(argv[i], "-tonemap")) tonemap = !tonemap; - else if (!strcmp(argv[i], "--preservecolors") || !strcmp(argv[i], "-preservecolors")) + else if (!strcmp(argv[i], "--preservecolors") || + !strcmp(argv[i], "-preservecolors")) preserveColors = !preserveColors; else { std::pair arg = parseArg(); @@ -657,36 +725,74 @@ int convert(int argc, char *argv[]) { usage("missing filenames for \"convert\""); const char *inFilename = argv[i], *outFilename = argv[i + 1]; - Point2i res; - std::unique_ptr image(ReadImage(inFilename, &res)); - if (!image) { + Image image; + if (HasExtension(inFilename, "txp")) { + std::unique_ptr cache(new TextureCache); + int id = cache->AddTexture(inFilename); + if (id < 0) { + fprintf(stderr, "%s: unable to read image\n", inFilename); + return 1; + } + + std::vector levelImages; + int sumWidth = 0, maxHeight = 0; + for (int level = 0; level < cache->Levels(id); ++level) { + levelImages.push_back(cache->GetLevelImage(id, level)); + sumWidth += levelImages.back().resolution[0]; + maxHeight = std::max(maxHeight, levelImages.back().resolution[1]); + if (level > 0) + CHECK(levelImages[level].format == levelImages[0].format); + } + + image = Image(levelImages[0].format, {sumWidth, maxHeight}); + int xStart = 0; + int nc = image.nChannels(); + for (const auto &im : levelImages) { + for (int y = 0; y < im.resolution[1]; ++y) + for (int x = 0; x < im.resolution[0]; ++x) + for (int c = 0; c < nc; ++c) + image.SetChannel({x + xStart, y}, c, + im.GetChannel({x, y}, c)); + xStart += im.resolution[0]; + } + } else if (!Image::Read(inFilename, &image)) { fprintf(stderr, "%s: unable to read image\n", inFilename); return 1; } + Point2i res = image.resolution; + int nc = image.nChannels(); + + // Convert to a 32-bit format for maximum accuracy in the following + // processing. + if (!Is32Bit(image.format)) { + CHECK(nc == 1 || nc == 3); + image = image.ConvertToFormat(nc == 1 ? PixelFormat::Y32 + : PixelFormat::RGB32); + } - for (int i = 0; i < res.x * res.y; ++i) image[i] *= scale; + for (int y = 0; y < res.y; ++y) + for (int x = 0; x < res.x; ++x) + for (int c = 0; c < nc; ++c) + image.SetChannel({x, y}, c, + scale * image.GetChannel({x, y}, c)); if (despikeLimit < Infinity) { - std::unique_ptr filteredImg( - new RGBSpectrum[res.x * res.y]); + Image filteredImg = image; int despikeCount = 0; for (int y = 0; y < res.y; ++y) { for (int x = 0; x < res.x; ++x) { - if (image[y * res.x + x].y() < despikeLimit) { - filteredImg[y * res.x + x] = image[y * res.x + x]; - continue; - } + if (image.GetY({x, y}) < despikeLimit) continue; // Copy all of the valid neighbor pixels into neighbors[]. ++despikeCount; int validNeighbors = 0; - RGBSpectrum neighbors[9]; + Spectrum neighbors[9]; for (int dy = -1; dy <= 1; ++dy) { if (y + dy < 0 || y + dy >= res.y) continue; for (int dx = -1; dx <= 1; ++dx) { if (x + dx < 0 || x + dx > res.x) continue; - int offset = (y + dy) * res.x + x + dx; - neighbors[validNeighbors++] = image[offset]; + neighbors[validNeighbors++] = + image.GetSpectrum({x + dx, y + dy}); } } @@ -694,10 +800,10 @@ int convert(int argc, char *argv[]) { int mid = validNeighbors / 2; std::nth_element( &neighbors[0], &neighbors[mid], &neighbors[validNeighbors], - [](const RGBSpectrum &a, const RGBSpectrum &b) -> bool { + [](const Spectrum &a, const Spectrum &b) -> bool { return a.y() < b.y(); }); - filteredImg[y * res.x + x] = neighbors[mid]; + filteredImg.SetSpectrum({x, y}, neighbors[mid]); } } std::swap(image, filteredImg); @@ -705,66 +811,111 @@ int convert(int argc, char *argv[]) { } if (bloomLevel < Infinity) - image = bloom(std::move(image), res, bloomLevel, bloomWidth, bloomScale, + image = bloom(std::move(image), bloomLevel, bloomWidth, bloomScale, bloomIters); if (tonemap) { - for (int i = 0; i < res.x * res.y; ++i) { - Float y = image[i].y(); - // Reinhard et al. photographic tone mapping operator. - Float scale = (1 + y / (maxY * maxY)) / (1 + y); - image[i] *= scale; - } + for (int y = 0; y < res.y; ++y) + for (int x = 0; x < res.x; ++x) { + Float lum = image.GetY({x, y}); + // Reinhard et al. photographic tone mapping operator. + Float scale = (1 + lum / (maxY * maxY)) / (1 + lum); + for (int c = 0; c < nc; ++c) + image.SetChannel({x, y}, c, + scale * image.GetChannel({x, y}, c)); + } } if (preserveColors) { - for (int i = 0; i < res.x * res.y; ++i) { - Float rgb[3]; - image[i].ToRGB(rgb); - Float m = std::max(rgb[0], std::max(rgb[1], rgb[2])); - if (m > 1) { - rgb[0] /= m; - rgb[1] /= m; - rgb[2] /= m; - image[i] = Spectrum::FromRGB(rgb); + for (int y = 0; y < res.y; ++y) + for (int x = 0; x < res.x; ++x) { + Float m = image.GetChannel({x, y}, 0); + for (int c = 1; c < nc; ++c) + m = std::max(m, image.GetChannel({x, y}, c)); + if (m > 1) { + for (int c = 0; c < nc; ++c) + image.SetChannel({x, y}, c, + image.GetChannel({x, y}, c) / m); + } } - } } if (repeat > 1) { - std::unique_ptr rscale( - new RGBSpectrum[repeat * res.x * repeat * res.y]); - RGBSpectrum *rsp = rscale.get(); + Image scaledImage(image.format, + Point2i(res.x * repeat, res.y * repeat)); for (int y = 0; y < repeat * res.y; ++y) { int yy = y / repeat; for (int x = 0; x < repeat * res.x; ++x) { int xx = x / repeat; - *rsp++ = image[yy * res.x + xx]; + for (int c = 0; c < nc; ++c) + scaledImage.SetChannel({x, y}, c, + image.GetChannel({xx, yy}, c)); } } - res.x *= repeat; - res.y *= repeat; - image = std::move(rscale); + image = std::move(scaledImage); + res = image.resolution; } - if (flipy) { - for (int y = 0; y < res.y / 2; ++y) { - int yo = res.y - 1 - y; - for (int x = 0; x < res.x; ++x) - std::swap(image[y * res.x + x], image[yo * res.x + x]); - } + if (flipy) image.FlipY(); + + if (!image.Write(outFilename)) { + fprintf(stderr, "imgtool: couldn't write to \"%s\".\n", outFilename); + return 1; } - // FIXME: another bad RGBSpectrum -> Float cast. - WriteImage(outFilename, (Float *)image.get(), Bounds2i(Point2i(0, 0), res), - res); + return 0; +} + +int maketiled(int argc, char *argv[]) { + ParallelInit(); + WrapMode wrapMode = WrapMode::Clamp; + int i; + for (i = 0; i < argc; ++i) { + if (argv[i][0] != '-') break; + if (!strcmp(argv[i], "--wrapmode") || !strcmp(argv[i], "-wrapmode")) { + if (i + 1 == argc) + usage("missing wrap mode after %s option", argv[i]); + ++i; + if (!ParseWrapMode(argv[i], &wrapMode)) + usage( + "unknown wrap mode %s. Expected \"clamp\", \"repeat\", " + "or \"black\".", + argv[i]); + } else if (!strncmp(argv[i], "--wrapmode=", 11)) { + if (!ParseWrapMode(argv[i] + 11, &wrapMode)) + usage( + "unknown wrap mode %s. Expected \"clamp\", \"repeat\", " + "or \"black\".", + argv[i] + 11); + } else + usage("unknown \"maketiled\" option \"%s\"", argv[i]); + } + if (i + 2 != argc) { + usage("expecting input and output filenames as arguments"); + } + const char *infile = argv[i], *outfile = argv[i + 1]; + + Image image; + if (!Image::Read(infile, &image)) { + fprintf(stderr, "imgtool: unable to read image \"%s\"\n", infile); + return 1; + } + + std::vector mips = image.GenerateMIPMap(wrapMode); + int tileSize = TextureCache::TileSize(image.format); + if (!TiledImagePyramid::Create(std::move(mips), outfile, wrapMode, tileSize)) { + fprintf(stderr, "imgtool: unable to create tiled image \"%s\"\n", + outfile); + return 1; + } + ParallelCleanup(); return 0; } int main(int argc, char *argv[]) { google::InitGoogleLogging(argv[0]); - FLAGS_stderrthreshold = 1; // Warning and above. + FLAGS_stderrthreshold = 1; // Warning and above. if (argc < 2) usage(); @@ -780,6 +931,8 @@ int main(int argc, char *argv[]) { return info(argc - 2, argv + 2); else if (!strcmp(argv[1], "makesky")) return makesky(argc - 2, argv + 2); + else if (!strcmp(argv[1], "maketiled")) + return maketiled(argc - 2, argv + 2); else usage("unknown command \"%s\"", argv[1]);