diff --git a/CMakeLists.txt b/CMakeLists.txt index d3f55771e0..9457beda7c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -288,6 +288,7 @@ if (NOT ANDROID) add_subdirectory(${TOOLS}/filamesh) add_subdirectory(${TOOLS}/matc) add_subdirectory(${TOOLS}/matinfo) + add_subdirectory(${TOOLS}/mipgen) add_subdirectory(${TOOLS}/normal-blending) add_subdirectory(${TOOLS}/roughness-prefilter) add_subdirectory(${TOOLS}/skygen) diff --git a/README.md b/README.md index 1febd4cf19..f93ca51c5f 100644 --- a/README.md +++ b/README.md @@ -115,6 +115,7 @@ Many other features have been either prototyped or planned: - `filamesh`: Mesh converter - `matc`: Material compiler - `matinfo` Displays information about materials compiled with `matc` + - `mipgen` Generates a series of miplevels from a source image. - `normal-blending`: Tool to blend normal maps - `roughness-prefilter`: Pre-filters a roughness map from a normal map to reduce aliasing - `skygen`: Physically-based sky environment texture generator diff --git a/libs/image/include/image/ImageSampler.h b/libs/image/include/image/ImageSampler.h index 7fc2f5251d..f09eff9640 100644 --- a/libs/image/include/image/ImageSampler.h +++ b/libs/image/include/image/ImageSampler.h @@ -129,6 +129,28 @@ LinearImage resampleImage(const LinearImage& source, uint32_t width, uint32_t he void computeSingleSample(const LinearImage& source, float x, float y, SingleSample* result, Filter filter = Filter::BOX); +/** + * Generates a sequence of miplevels using the requested filter. To determine the number of mips + * it would take to get down to 1x1, see getMipmapCount. + * + * Source image need not be power-of-two. In the result vector, the half-size image is returned at + * index 0, the quarter-size image is at index 1, etc. Please note that the original-sized image is + * not included. + */ +void generateMipmaps(const LinearImage& source, Filter, LinearImage* result, uint32_t mipCount); + +/** + * Returns the number of miplevels it would take to downsample the given image down to 1x1. This + * number does not include the original image (i.e. mip 0). + */ +uint32_t getMipmapCount(const LinearImage& source); + +/** + * Given the string name of a filter, converts it to uppercase and returns the corresponding + * enum value. If no corresponding enumerant exists, returns DEFAULT. + */ +Filter filterFromString(const char* name); + } // namespace image #endif /* IMAGE_IMAGESAMPLER_H */ diff --git a/libs/image/src/ImageSampler.cpp b/libs/image/src/ImageSampler.cpp index 52258c92a1..ab6fd4ebba 100644 --- a/libs/image/src/ImageSampler.cpp +++ b/libs/image/src/ImageSampler.cpp @@ -19,9 +19,11 @@ #include #include +#include #include #include +#include using namespace image; @@ -320,4 +322,48 @@ void computeSingleSample(const LinearImage& source, float x, float y, SingleSamp } } +// Unlike traditional mipmap generation, our implementation generates all levels from the original +// image, under the premise that this produces a higher quality result. +void generateMipmaps(const LinearImage& source, Filter filter, LinearImage* result, uint32_t mips) { + mips = std::min(mips, getMipmapCount(source)); + uint32_t width = source.getWidth(); + uint32_t height = source.getHeight(); + for (uint32_t n = 0; n < mips; ++n) { + width = std::max(width >> 1, 1u); + height = std::max(height >> 1, 1u); + result[n] = resampleImage(source, width, height, filter); + } +} + +uint32_t getMipmapCount(const LinearImage& source) { + uint32_t width = source.getWidth(); + uint32_t height = source.getHeight(); + uint32_t count = 0; + while (width > 1 || height > 1) { + ++count; + width = std::max(width >> 1, 1u); + height = std::max(height >> 1, 1u); + } + return count; +} + +Filter filterFromString(const char* rawname) { + using namespace utils; + using namespace std; + static const unordered_map map = { + { "BOX", Filter::BOX}, + { "NEAREST", Filter::NEAREST}, + { "HERMITE", Filter::HERMITE}, + { "GAUSSIAN", Filter::GAUSSIAN_SCALARS}, + { "NORMALS", Filter::GAUSSIAN_NORMALS}, + { "MITCHELL", Filter::MITCHELL}, + { "LANCZOS", Filter::LANCZOS}, + { "MINIMUM", Filter::MINIMUM}, + }; + string name = rawname; + for (auto& c: name) c = toupper((unsigned char) c); + auto iter = map.find({ name.c_str(), name.size() }); + return iter == map.end() ? Filter::DEFAULT : iter->second; +} + } // namespace image diff --git a/libs/image/tests/reference/index.html b/libs/image/tests/reference/index.html new file mode 100644 index 0000000000..13eab9671a --- /dev/null +++ b/libs/image/tests/reference/index.html @@ -0,0 +1,47 @@ + + + +test_image + + + + +

+Modern CSS lets you specify nearest-neighbor scaling, so a nice way of examining the results from +our filtering tests is this tiny web page. It can be refreshed at the touch of a button. Moreover +Chrome respects the color space attribute of PNG images, which cannot be said of many image viewers. +You can also use the ColorZilla +extension to scrub over color values. +

+ +

+ + + + +

+ +

+ + + + + + + + +

+ + + diff --git a/libs/image/tests/reference/mip0_200x100.png b/libs/image/tests/reference/mip0_200x100.png new file mode 100644 index 0000000000..cb8c1f80ed Binary files /dev/null and b/libs/image/tests/reference/mip0_200x100.png differ diff --git a/libs/image/tests/reference/mip0_300x100.png b/libs/image/tests/reference/mip0_300x100.png new file mode 100644 index 0000000000..ad84abdadd Binary files /dev/null and b/libs/image/tests/reference/mip0_300x100.png differ diff --git a/libs/image/tests/reference/mip0_5x10.png b/libs/image/tests/reference/mip0_5x10.png new file mode 100644 index 0000000000..864fd98619 Binary files /dev/null and b/libs/image/tests/reference/mip0_5x10.png differ diff --git a/libs/image/tests/reference/mip1_200x100.png b/libs/image/tests/reference/mip1_200x100.png new file mode 100644 index 0000000000..f34ff8f4fc Binary files /dev/null and b/libs/image/tests/reference/mip1_200x100.png differ diff --git a/libs/image/tests/reference/mip1_300x100.png b/libs/image/tests/reference/mip1_300x100.png new file mode 100644 index 0000000000..e8aa4c4eb8 Binary files /dev/null and b/libs/image/tests/reference/mip1_300x100.png differ diff --git a/libs/image/tests/reference/mip1_5x10.png b/libs/image/tests/reference/mip1_5x10.png new file mode 100644 index 0000000000..aa7deb2f3b Binary files /dev/null and b/libs/image/tests/reference/mip1_5x10.png differ diff --git a/libs/image/tests/reference/mip2_200x100.png b/libs/image/tests/reference/mip2_200x100.png new file mode 100644 index 0000000000..10115ce5cd Binary files /dev/null and b/libs/image/tests/reference/mip2_200x100.png differ diff --git a/libs/image/tests/reference/mip2_300x100.png b/libs/image/tests/reference/mip2_300x100.png new file mode 100644 index 0000000000..34c7fc381a Binary files /dev/null and b/libs/image/tests/reference/mip2_300x100.png differ diff --git a/libs/image/tests/reference/mip2_5x10.png b/libs/image/tests/reference/mip2_5x10.png new file mode 100644 index 0000000000..abec14345d Binary files /dev/null and b/libs/image/tests/reference/mip2_5x10.png differ diff --git a/libs/image/tests/reference/mip3_200x100.png b/libs/image/tests/reference/mip3_200x100.png new file mode 100644 index 0000000000..4dc959437c Binary files /dev/null and b/libs/image/tests/reference/mip3_200x100.png differ diff --git a/libs/image/tests/reference/mip3_300x100.png b/libs/image/tests/reference/mip3_300x100.png new file mode 100644 index 0000000000..bc53cf2181 Binary files /dev/null and b/libs/image/tests/reference/mip3_300x100.png differ diff --git a/libs/image/tests/reference/mip3_5x10.png b/libs/image/tests/reference/mip3_5x10.png new file mode 100644 index 0000000000..2c830b501e Binary files /dev/null and b/libs/image/tests/reference/mip3_5x10.png differ diff --git a/libs/image/tests/reference/mip4_200x100.png b/libs/image/tests/reference/mip4_200x100.png new file mode 100644 index 0000000000..42bd3553df Binary files /dev/null and b/libs/image/tests/reference/mip4_200x100.png differ diff --git a/libs/image/tests/reference/mip4_300x100.png b/libs/image/tests/reference/mip4_300x100.png new file mode 100644 index 0000000000..c9f9f891e9 Binary files /dev/null and b/libs/image/tests/reference/mip4_300x100.png differ diff --git a/libs/image/tests/reference/mip5_200x100.png b/libs/image/tests/reference/mip5_200x100.png new file mode 100644 index 0000000000..e750aa19d4 Binary files /dev/null and b/libs/image/tests/reference/mip5_200x100.png differ diff --git a/libs/image/tests/reference/mip5_300x100.png b/libs/image/tests/reference/mip5_300x100.png new file mode 100644 index 0000000000..f431919e69 Binary files /dev/null and b/libs/image/tests/reference/mip5_300x100.png differ diff --git a/libs/image/tests/reference/mip6_200x100.png b/libs/image/tests/reference/mip6_200x100.png new file mode 100644 index 0000000000..0f44e3c094 Binary files /dev/null and b/libs/image/tests/reference/mip6_200x100.png differ diff --git a/libs/image/tests/reference/mip6_300x100.png b/libs/image/tests/reference/mip6_300x100.png new file mode 100644 index 0000000000..f8f3d9dfb0 Binary files /dev/null and b/libs/image/tests/reference/mip6_300x100.png differ diff --git a/libs/image/tests/reference/mip7_200x100.png b/libs/image/tests/reference/mip7_200x100.png new file mode 100644 index 0000000000..90a9b01bf1 Binary files /dev/null and b/libs/image/tests/reference/mip7_200x100.png differ diff --git a/libs/image/tests/reference/mip7_300x100.png b/libs/image/tests/reference/mip7_300x100.png new file mode 100644 index 0000000000..34497b3ff0 Binary files /dev/null and b/libs/image/tests/reference/mip7_300x100.png differ diff --git a/libs/image/tests/reference/mip8_300x100.png b/libs/image/tests/reference/mip8_300x100.png new file mode 100644 index 0000000000..84f20de7c9 Binary files /dev/null and b/libs/image/tests/reference/mip8_300x100.png differ diff --git a/libs/image/tests/test_image.cpp b/libs/image/tests/test_image.cpp index 50ad9a81db..6b8279bc17 100644 --- a/libs/image/tests/test_image.cpp +++ b/libs/image/tests/test_image.cpp @@ -278,6 +278,35 @@ TEST_F(ImageTest, ColorTransformRGBA) { // NOLINT ASSERT_NEAR(pixels[3].w, 0.99183642f, 0.001f); } +TEST_F(ImageTest, Mipmaps) { // NOLINT + Filter filter = filterFromString("HERMITE"); + ASSERT_EQ(filter, Filter::HERMITE); + + // Miplevels: 5x10, 2x5, 1x2, 1x1. + LinearImage src = createColorFromAscii( + "44444 41014 40704 41014 44444 44444 41014 40704 41014 44444"); + uint32_t count = getMipmapCount(src); + ASSERT_EQ(count, 3); + std::vector mips(count); + generateMipmaps(src, filter, mips.data(), count); + updateOrCompare(src, "mip0_5x10.png"); + for (uint32_t index = 0; index < count; ++index) { + updateOrCompare(mips[index], "mip" + std::to_string(index + 1) + "_5x10.png"); + } + + // Test color space with a classic RED => GREEN color gradient. + src = createColorFromAscii("12"); + src = resampleImage(src, 200, 100, Filter::NEAREST); + count = getMipmapCount(src); + ASSERT_EQ(count, 7); + mips.resize(count); + generateMipmaps(src, filter, mips.data(), count); + updateOrCompare(src, "mip0_200x100.png"); + for (uint32_t index = 0; index < count; ++index) { + updateOrCompare(mips[index], "mip" + std::to_string(index + 1) + "_200x100.png"); + } +} + static void printUsage(const char* name) { string exec_name(utils::Path(name).getName()); string usage( diff --git a/tools/mipgen/CMakeLists.txt b/tools/mipgen/CMakeLists.txt new file mode 100644 index 0000000000..999d61fec8 --- /dev/null +++ b/tools/mipgen/CMakeLists.txt @@ -0,0 +1,28 @@ +cmake_minimum_required(VERSION 3.1) +project(mipgen) + +set(TARGET mipgen) + +# ================================================================================================== +# Source files +# ================================================================================================== +set(SRCS src/main.cpp) + +# ================================================================================================== +# Target definitions +# ================================================================================================== +add_executable(${TARGET} ${SRCS}) +target_link_libraries(${TARGET} PRIVATE math utils z image imageio getopt) + +# ================================================================================================= +# Licenses +# ================================================================================================== +set(MODULE_LICENSES getopt libpng tinyexr libz) +set(GENERATION_ROOT ${CMAKE_CURRENT_BINARY_DIR}/generated) +list_licenses(${GENERATION_ROOT}/licenses/licenses.inc ${MODULE_LICENSES}) +target_include_directories(${TARGET} PRIVATE ${GENERATION_ROOT}) + +# ================================================================================================== +# Installation +# ================================================================================================== +install(TARGETS ${TARGET} RUNTIME DESTINATION bin) diff --git a/tools/mipgen/src/main.cpp b/tools/mipgen/src/main.cpp new file mode 100644 index 0000000000..4664c000cb --- /dev/null +++ b/tools/mipgen/src/main.cpp @@ -0,0 +1,255 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include + +#include +#include + +#include + +#include + +#include +#include + +using namespace image; +using namespace std; +using namespace utils; + +static ImageEncoder::Format g_format = ImageEncoder::Format::PNG_LINEAR; +static bool g_formatSpecified = false; +static bool g_createGallery = false; +static string g_compression = ""; +static Filter g_filter = Filter::DEFAULT; + +static const char* USAGE = R"TXT( +MIPGEN generates mipmaps for an image down to the 1x1 level. + +Output filenames are generated using the specified printf pattern. +For example, "mip%2d.png" would generate mip01.png, mip02.png, etc. +Note that miplevel 0 is not generated since it is the original image. + +Usage: + MIPGEN [options] + +Options: + --help, -h + print this message + --license + print copyright and license information + --gallery, -g + generate HTML gallery for review purposes (mipmap.html) + --format=[exr|hdr|rgbm|psd|png|dds], -f [exr|hdr|rgbm|psd|png|dds] + specify output file format, inferred from output pattern if omitted + --kernel=[box|nearest|hermite|gaussian|normals|mitchell|lanczos|min], -k [filter] + specify filter kernel type (defaults to LANCZOS) + --compression=COMPRESSION, -c COMPRESSION + format specific compression: + PNG: Ignored + Radiance: Ignored + Photoshop: 16 (default), 32 + OpenEXR: RAW, RLE, ZIPS, ZIP, PIZ (default) + DDS: 8, 16 (default), 32 + +Example: + MIPGEN -g --kernel=hermite grassland.png mip_%03d.png +)TXT"; + +static const char* HTML_PREFIX = R"HTML( + + + + + +)HTML"; + +static const char* HTML_SUFFIX = R"HTML( + +)HTML"; + +static void printUsage(const char* name) { + string execName(Path(name).getName()); + const string from("MIPGEN"); + string usage(USAGE); + for (size_t pos = usage.find(from); pos != string::npos; pos = usage.find(from, pos)) { + usage.replace(pos, from.length(), execName); + } + puts(usage.c_str()); +} + +static void license() { + cout << + #include "licenses/licenses.inc" + ; +} + +static int handleArguments(int argc, char* argv[]) { + static constexpr const char* OPTSTR = "hlgf:c:k:"; + static const struct option OPTIONS[] = { + { "help", no_argument, 0, 'h' }, + { "license", no_argument, 0, 'l' }, + { "gallery", no_argument, 0, 'g' }, + { "format", required_argument, 0, 'f' }, + { "compression", required_argument, 0, 'c' }, + { "kernel", required_argument, 0, 'k' }, + { 0, 0, 0, 0 } // termination of the option list + }; + + int opt; + int optionIndex = 0; + + while ((opt = getopt_long(argc, argv, OPTSTR, OPTIONS, &optionIndex)) >= 0) { + string arg(optarg ? optarg : ""); + switch (opt) { + default: + case 'h': + printUsage(argv[0]); + exit(0); + case 'l': + license(); + exit(0); + case 'g': + g_createGallery = true; + break; + case 'k': { + bool isvalid; + g_filter = filterFromString(arg.c_str()); + if (g_filter == Filter::DEFAULT) { + cerr << "Warning: unrecognized filter, falling back to DEFAULT." << endl; + } + break; + } + case 'f': + if (arg == "png") { + g_format = ImageEncoder::Format::PNG; + g_formatSpecified = true; + } + if (arg == "hdr") { + g_format = ImageEncoder::Format::HDR; + g_formatSpecified = true; + } + if (arg == "rgbm") { + g_format = ImageEncoder::Format::RGBM; + g_formatSpecified = true; + } + if (arg == "exr") { + g_format = ImageEncoder::Format::EXR; + g_formatSpecified = true; + } + if (arg == "psd") { + g_format = ImageEncoder::Format::PSD; + g_formatSpecified = true; + } + if (arg == "dds") { + g_format = ImageEncoder::Format::DDS_LINEAR; + g_formatSpecified = true; + } + break; + case 'c': + g_compression = arg; + break; + } + } + + return optind; +} + +int main(int argc, char* argv[]) { + int optionIndex = handleArguments(argc, argv); + int numArgs = argc - optionIndex; + if (numArgs < 2) { + printUsage(argv[0]); + return 1; + } + Path inputPath(argv[optionIndex++]); + string outputPattern(argv[optionIndex]); + if (!g_formatSpecified) { + constexpr bool forceLinear = true; + g_format = ImageEncoder::chooseFormat(outputPattern, forceLinear); + } + + puts("Reading image..."); + ifstream inputStream(inputPath.getPath(), ios::binary); + LinearImage sourceImage = ImageDecoder::decode(inputStream, inputPath.getPath()); + if (!sourceImage.isValid()) { + cerr << "Unable to open image: " << inputPath.getPath() << endl; + exit(1); + } + + puts("Generating miplevels..."); + uint32_t count = getMipmapCount(sourceImage); + vector miplevels(count); + generateMipmaps(sourceImage, g_filter, miplevels.data(), count); + + puts("Writing image files to disk..."); + char path[256]; + uint32_t mip = 1; // start at 1 because 0 is the original image + for (auto image: miplevels) { + int result = snprintf(path, sizeof(path), outputPattern.c_str(), mip++); + if (result < 0 || result >= sizeof(path)) { + cerr << "Output pattern is too long." << endl; + exit(1); + } + ofstream outputStream(path, ios::binary | ios::trunc); + if (!outputStream) { + cerr << "The output file cannot be opened: " << path << endl; + } else { + ImageEncoder::encode(outputStream, g_format, image, g_compression, path); + outputStream.close(); + if (!outputStream) { + cerr << "An error occurred while writing the output file: " << path << endl; + } + } + } + + if (g_createGallery) { + puts("Generating mipmaps.html..."); + char tag[256]; + mip = 1; + const char* pattern = R"()"; + const uint32_t width = sourceImage.getWidth(); + const uint32_t height = sourceImage.getHeight(); + ofstream html("mipmaps.html", ios::trunc); + html << HTML_PREFIX; + int result = snprintf(tag, sizeof(tag), pattern, inputPath.c_str(), width, height); + if (result < 0 || result >= sizeof(tag)) { + cerr << "Output pattern is too long." << endl; + exit(1); + } + html << tag << std::endl; + for (auto image: miplevels) { + snprintf(path, sizeof(path), outputPattern.c_str(), mip++); + result = snprintf(tag, sizeof(tag), pattern, path, width, height); + if (result < 0 || result >= sizeof(tag)) { + cerr << "Output pattern is too long." << endl; + exit(1); + } + html << tag << std::endl; + } + html << HTML_SUFFIX; + } + + puts("Done."); +}