From 3185d0f237cd94e3756694435884bdb7be424ac8 Mon Sep 17 00:00:00 2001 From: Philip Rideout Date: Mon, 1 Oct 2018 11:16:31 -0700 Subject: [PATCH] Add S3TC support to imageio and Suzanne. At build time, mipgen now creates several compressed and non-compressed variants for albedo. For ASTC we use the ARM encoder, for S3TC we use the STB encoder. The latter is somewhat limited so we may wish to investigate other libraries in the future (e.g., AMD Compressenator). At run time, we detect which of these variants to download, based on available WebGL extensions. In practice, this means that desktop browsers will use the S3TC variant and mobile browsers will use the ASTC variant. The makes unzipped albedo go from ~4 MB to 682 KB using S3TC. Mobile support (ASTC and ETC2) is coming in a future PR, stay tuned. --- filament/src/driver/opengl/OpenGLDriver.cpp | 1 + libs/image/include/image/KtxBundle.h | 5 + libs/imageio/CMakeLists.txt | 2 +- .../include/imageio/BlockCompression.h | 36 +++- libs/imageio/src/BlockCompression.cpp | 176 ++++++++++++------ samples/web/CMakeLists.txt | 2 + samples/web/filaweb.cpp | 28 ++- samples/web/filaweb.h | 3 + samples/web/filaweb.js | 35 ++-- samples/web/suzanne.cpp | 20 ++ samples/web/suzanne.html | 3 +- tools/mipgen/CMakeLists.txt | 2 +- tools/mipgen/src/main.cpp | 35 +++- 13 files changed, 265 insertions(+), 83 deletions(-) diff --git a/filament/src/driver/opengl/OpenGLDriver.cpp b/filament/src/driver/opengl/OpenGLDriver.cpp index 6c68c91c9a..dc9fd80fe8 100644 --- a/filament/src/driver/opengl/OpenGLDriver.cpp +++ b/filament/src/driver/opengl/OpenGLDriver.cpp @@ -235,6 +235,7 @@ void OpenGLDriver::initExtensionsGLES(GLint major, GLint minor, std::set const& exts) { diff --git a/libs/image/include/image/KtxBundle.h b/libs/image/include/image/KtxBundle.h index fe6de95ee6..2327cf5f38 100644 --- a/libs/image/include/image/KtxBundle.h +++ b/libs/image/include/image/KtxBundle.h @@ -155,6 +155,11 @@ public: static constexpr uint32_t ENDIAN_DEFAULT = 0x04030201; + static constexpr uint32_t RGB_S3TC_DXT1 = 0x83F0; + static constexpr uint32_t RGBA_S3TC_DXT1 = 0x83F1; + static constexpr uint32_t RGBA_S3TC_DXT3 = 0x83F2; + static constexpr uint32_t RGBA_S3TC_DXT5 = 0x83F3; + private: image::KtxInfo mInfo = {}; uint32_t mNumMipLevels; diff --git a/libs/imageio/CMakeLists.txt b/libs/imageio/CMakeLists.txt index 828921b160..60733dc2a7 100644 --- a/libs/imageio/CMakeLists.txt +++ b/libs/imageio/CMakeLists.txt @@ -30,7 +30,7 @@ add_library(${TARGET} STATIC ${PUBLIC_HDRS} ${SRCS}) target_include_directories(${TARGET} PUBLIC ${PUBLIC_HDR_DIR}) -target_link_libraries(${TARGET} PUBLIC image math png tinyexr utils z astcenc) +target_link_libraries(${TARGET} PUBLIC image math png tinyexr utils z astcenc stb) if (WIN32) target_link_libraries(${TARGET} PRIVATE wsock32) endif() diff --git a/libs/imageio/include/imageio/BlockCompression.h b/libs/imageio/include/imageio/BlockCompression.h index 42022c589e..731214cce0 100644 --- a/libs/imageio/include/imageio/BlockCompression.h +++ b/libs/imageio/include/imageio/BlockCompression.h @@ -56,7 +56,18 @@ struct AstcConfig { bool srgb; }; -enum class AstcFormat { +enum class CompressedFormat { + INVALID = 0, + + RGB_S3TC_DXT1 = 0x83F0, + RGBA_S3TC_DXT1 = 0x83F1, + RGBA_S3TC_DXT3 = 0x83F2, + RGBA_S3TC_DXT5 = 0x83F3, + SRGB_S3TC_DXT1 = 0x8C4C, + SRGB_ALPHA_S3TC_DXT1 = 0x8C4D, + SRGB_ALPHA_S3TC_DXT3 = 0x8C4E, + SRGB_ALPHA_S3TC_DXT5 = 0x8C4F, + RGBA_ASTC_4x4 = 0x93B0, RGBA_ASTC_5x4 = 0x93B1, RGBA_ASTC_5x5 = 0x93B2, @@ -87,17 +98,16 @@ enum class AstcFormat { SRGB8_ALPHA8_ASTC_12x12 = 0x93DD, }; -// Represents the result of compression, including which of the 28 internal formats that the -// encoder finally settled on, based on the hints supplied in AstcConfig. -struct AstcTexture { - const AstcFormat format; +// Represents the opaque result of compression and the chosen texture format. +struct CompressedTexture { + const CompressedFormat format; const uint32_t size; std::unique_ptr data; }; // Uses the CPU to compress a linear image (1 to 4 channels) into an ASTC texture. The 16-byte // header block that ARM uses in their file format is not included. -AstcTexture astcCompress(const LinearImage& source, AstcConfig config); +CompressedTexture astcCompress(const LinearImage& source, AstcConfig config); // Parses a simple underscore-delimited string to produce an ASTC compression configuration. This // makes it easy to incorporate the compression API into command-line tools. If the string is @@ -105,6 +115,20 @@ AstcTexture astcCompress(const LinearImage& source, AstcConfig config); // thorough_normals_6x6, veryfast_hdr_12x10 AstcConfig astcParseOptionString(const std::string& options); +// Informs the S3TC encoder of the desired output. +struct S3tcConfig { + CompressedFormat format; + bool srgb; +}; + +// Uses the CPU to compress a linear image (1 to 4 channels) into an S3TC texture. +CompressedTexture s3tcCompress(const LinearImage& source, S3tcConfig config); + +// Parses an underscore-delimited string to produce an S3TC compression configuration. Currently +// this only accepts "rgb_dxt1" and "rgba_dxt5". If the string is malformed, this returns a config +// with an invalid format. +S3tcConfig s3tcParseOptionString(const std::string& options); + } // namespace image #endif /* IMAGEIO_BLOCKCOMPRESSION_H_ */ diff --git a/libs/imageio/src/BlockCompression.cpp b/libs/imageio/src/BlockCompression.cpp index 00045e0960..efee7410e7 100644 --- a/libs/imageio/src/BlockCompression.cpp +++ b/libs/imageio/src/BlockCompression.cpp @@ -22,13 +22,18 @@ #include +#define STB_DXT_IMPLEMENTATION +#include + using namespace image; using std::string; namespace image { -AstcTexture astcCompress(const LinearImage& original, AstcConfig config) { +static LinearImage extendToFourChannels(LinearImage source); + +CompressedTexture astcCompress(const LinearImage& original, AstcConfig config) { // If this is the first time, initialize the ARM encoder tables. @@ -42,35 +47,36 @@ AstcTexture astcCompress(const LinearImage& original, AstcConfig config) { // Check the validity of the given block size. - AstcFormat format; + using Format = CompressedFormat; + Format format; if (config.blocksize == math::ushort2 {4, 4}) { - format = config.srgb ? AstcFormat::SRGB8_ALPHA8_ASTC_4x4 : AstcFormat::RGBA_ASTC_4x4; + format = config.srgb ? Format::SRGB8_ALPHA8_ASTC_4x4 : Format::RGBA_ASTC_4x4; } else if (config.blocksize == math::ushort2 {5, 4}) { - format = config.srgb ? AstcFormat::SRGB8_ALPHA8_ASTC_5x4 : AstcFormat::RGBA_ASTC_5x4; + format = config.srgb ? Format::SRGB8_ALPHA8_ASTC_5x4 : Format::RGBA_ASTC_5x4; } else if (config.blocksize == math::ushort2 {5, 5}) { - format = config.srgb ? AstcFormat::SRGB8_ALPHA8_ASTC_5x5 : AstcFormat::RGBA_ASTC_5x5; + format = config.srgb ? Format::SRGB8_ALPHA8_ASTC_5x5 : Format::RGBA_ASTC_5x5; } else if (config.blocksize == math::ushort2 {6, 5}) { - format = config.srgb ? AstcFormat::SRGB8_ALPHA8_ASTC_6x5 : AstcFormat::RGBA_ASTC_6x5; + format = config.srgb ? Format::SRGB8_ALPHA8_ASTC_6x5 : Format::RGBA_ASTC_6x5; } else if (config.blocksize == math::ushort2 {6, 6}) { - format = config.srgb ? AstcFormat::SRGB8_ALPHA8_ASTC_6x6 : AstcFormat::RGBA_ASTC_6x6; + format = config.srgb ? Format::SRGB8_ALPHA8_ASTC_6x6 : Format::RGBA_ASTC_6x6; } else if (config.blocksize == math::ushort2 {8, 5}) { - format = config.srgb ? AstcFormat::SRGB8_ALPHA8_ASTC_8x5 : AstcFormat::RGBA_ASTC_8x5; + format = config.srgb ? Format::SRGB8_ALPHA8_ASTC_8x5 : Format::RGBA_ASTC_8x5; } else if (config.blocksize == math::ushort2 {8, 6}) { - format = config.srgb ? AstcFormat::SRGB8_ALPHA8_ASTC_8x6 : AstcFormat::RGBA_ASTC_8x6; + format = config.srgb ? Format::SRGB8_ALPHA8_ASTC_8x6 : Format::RGBA_ASTC_8x6; } else if (config.blocksize == math::ushort2 {8, 8}) { - format = config.srgb ? AstcFormat::SRGB8_ALPHA8_ASTC_8x8 : AstcFormat::RGBA_ASTC_8x8; + format = config.srgb ? Format::SRGB8_ALPHA8_ASTC_8x8 : Format::RGBA_ASTC_8x8; } else if (config.blocksize == math::ushort2 {10, 5}) { - format = config.srgb ? AstcFormat::SRGB8_ALPHA8_ASTC_10x5 : AstcFormat::RGBA_ASTC_10x5; + format = config.srgb ? Format::SRGB8_ALPHA8_ASTC_10x5 : Format::RGBA_ASTC_10x5; } else if (config.blocksize == math::ushort2 {10, 6}) { - format = config.srgb ? AstcFormat::SRGB8_ALPHA8_ASTC_10x6 : AstcFormat::RGBA_ASTC_10x6; + format = config.srgb ? Format::SRGB8_ALPHA8_ASTC_10x6 : Format::RGBA_ASTC_10x6; } else if (config.blocksize == math::ushort2 {10, 8}) { - format = config.srgb ? AstcFormat::SRGB8_ALPHA8_ASTC_10x8 : AstcFormat::RGBA_ASTC_10x8; + format = config.srgb ? Format::SRGB8_ALPHA8_ASTC_10x8 : Format::RGBA_ASTC_10x8; } else if (config.blocksize == math::ushort2 {10, 10}) { - format = config.srgb ? AstcFormat::SRGB8_ALPHA8_ASTC_10x10 : AstcFormat::RGBA_ASTC_10x10; + format = config.srgb ? Format::SRGB8_ALPHA8_ASTC_10x10 : Format::RGBA_ASTC_10x10; } else if (config.blocksize == math::ushort2 {12, 10}) { - format = config.srgb ? AstcFormat::SRGB8_ALPHA8_ASTC_12x10 : AstcFormat::RGBA_ASTC_12x10; + format = config.srgb ? Format::SRGB8_ALPHA8_ASTC_12x10 : Format::RGBA_ASTC_12x10; } else if (config.blocksize == math::ushort2 {12, 12}) { - format = config.srgb ? AstcFormat::SRGB8_ALPHA8_ASTC_12x12 : AstcFormat::RGBA_ASTC_12x12; + format = config.srgb ? Format::SRGB8_ALPHA8_ASTC_12x12 : Format::RGBA_ASTC_12x12; } else { return {}; } @@ -79,43 +85,9 @@ AstcTexture astcCompress(const LinearImage& original, AstcConfig config) { // It expects four-channel data, so we extend or curtail the channel count in a reasonable way. // The encoder can take half-floats or bytes, but we always give it half-floats. - LinearImage source = original; + LinearImage source = extendToFourChannels(original); const uint32_t width = source.getWidth(); const uint32_t height = source.getHeight(); - auto createEmptyImage = [width, height](float value) { - auto result = LinearImage(width, height, 1); - float* pixels = result.getPixelRef(0, 0); - std::fill(pixels, pixels + width * height, value); - return result; - }; - switch (source.getChannels()) { - case 4: break; - case 1: { - auto l = image::extractChannel(source, 0); - auto a = createEmptyImage(1.0f); - source = image::combineChannels({l, l, l, a}); - } - case 2: { - auto l = image::extractChannel(source, 0); - auto a = image::extractChannel(source, 1); - source = image::combineChannels({l, l, l, a}); - } - case 3: { - auto r = image::extractChannel(source, 0); - auto g = image::extractChannel(source, 1); - auto b = image::extractChannel(source, 2); - auto a = createEmptyImage(1.0f); - source = image::combineChannels({r, g, b, a}); - } - default: { - auto r = image::extractChannel(source, 0); - auto g = image::extractChannel(source, 1); - auto b = image::extractChannel(source, 2); - auto a = image::extractChannel(source, 3); - source = image::combineChannels({r, g, b, a}); - } - } - astc_codec_image* input_image = allocate_image(16, width, height, 1, 0); for (int y = 0; y < height; y++) { auto imagedata16 = input_image->imagedata16[0][y]; @@ -236,7 +208,7 @@ AstcTexture astcCompress(const LinearImage& original, AstcConfig config) { return { .format = format, .size = size, - .data = decltype(AstcTexture::data)(buffer) + .data = decltype(CompressedTexture::data)(buffer) }; } @@ -282,4 +254,104 @@ AstcConfig astcParseOptionString(const string& configString) { return config; } +static uint32_t imin(uint32_t a, uint32_t b) { + return (a < b) ? a : b; +} + +static void extract4x4RGBA(uint8_t* dst, const LinearImage& source, uint32_t x0, uint32_t y0) { + const uint32_t maxx = source.getWidth() - 1; + const uint32_t maxy = source.getHeight() - 1; + for (uint32_t y = y0, y1 = y0 + 4; y < y1; ++y) { + for (uint32_t x = x0, x1 = x0 + 4; x < x1; ++x, dst += 4) { + int clamped_x = imin(maxx, x); + int clamped_y = imin(maxy, y); + float const* rgba = source.getPixelRef(clamped_x, clamped_y); + dst[0] = (uint8_t) (rgba[0] * 255.0f); + dst[1] = (uint8_t) (rgba[1] * 255.0f); + dst[2] = (uint8_t) (rgba[2] * 255.0f); + dst[3] = (uint8_t) (rgba[3] * 255.0f); + } + } +} + +// Our S3TC / DXT encoder uses the STB implementation by Fabian Giesen. +// +// Due to limitations in STB, this only supports the following formats: +// - DXT1 with no alpha (16 input pixels in 64 bits of output, 6:1) +// - DXT5 with alpha (16 input pixels into 128 bits of output, 4:1) +// +// TODO: investigate using something more capable than STB (eg AMD Compressenator, bimg, libsquish) +CompressedTexture s3tcCompress(const LinearImage& original, S3tcConfig config) { + const bool dxt5 = config.format == CompressedFormat::RGBA_S3TC_DXT5; + LinearImage source = extendToFourChannels(original); + uint8_t block[64]; + uint32_t xblocks = (source.getWidth() + 3) / 4; + uint32_t yblocks = (source.getHeight() + 3) / 4; + uint32_t size = xblocks * yblocks * (dxt5 ? 16 : 8); + uint8_t* buffer = new uint8_t[size]; + uint8_t* dst = buffer; + for (int y = 0, h = source.getHeight(); y < h; y += 4) { + for (int x = 0, w = source.getWidth(); x < w; x += 4) { + extract4x4RGBA(block, source, x, y); + stb_compress_dxt_block(dst, block, dxt5, 8); + dst += dxt5 ? 16 : 8; + } + } + return { + .format = config.format, + .size = size, + .data = decltype(CompressedTexture::data)(buffer) + }; +} + +S3tcConfig s3tcParseOptionString(const std::string& options) { + if (options == "rgb_dxt1") { + return {CompressedFormat::RGB_S3TC_DXT1, false}; + } + if (options == "rgba_dxt5") { + return {CompressedFormat::RGBA_S3TC_DXT5, false}; + } + return {}; +} + +static LinearImage extendToFourChannels(LinearImage original) { + LinearImage source = original; + const uint32_t width = source.getWidth(); + const uint32_t height = source.getHeight(); + auto createEmptyImage = [width, height](float value) { + auto result = LinearImage(width, height, 1); + float* pixels = result.getPixelRef(0, 0); + std::fill(pixels, pixels + width * height, value); + return result; + }; + switch (source.getChannels()) { + case 4: break; + case 1: { + auto l = image::extractChannel(source, 0); + auto a = createEmptyImage(1.0f); + source = image::combineChannels({l, l, l, a}); + } + case 2: { + auto l = image::extractChannel(source, 0); + auto a = image::extractChannel(source, 1); + source = image::combineChannels({l, l, l, a}); + } + case 3: { + auto r = image::extractChannel(source, 0); + auto g = image::extractChannel(source, 1); + auto b = image::extractChannel(source, 2); + auto a = createEmptyImage(1.0f); + source = image::combineChannels({r, g, b, a}); + } + default: { + auto r = image::extractChannel(source, 0); + auto g = image::extractChannel(source, 1); + auto b = image::extractChannel(source, 2); + auto a = image::extractChannel(source, 3); + source = image::combineChannels({r, g, b, a}); + } + } + return source; +} + } // namespace image diff --git a/samples/web/CMakeLists.txt b/samples/web/CMakeLists.txt index 344aaf0df6..71f1f55e1e 100644 --- a/samples/web/CMakeLists.txt +++ b/samples/web/CMakeLists.txt @@ -84,6 +84,8 @@ function(add_ktxfiles SOURCE TARGET EXTRA_ARGS) endfunction() add_ktxfiles("assets/models/monkey/albedo.png" "monkey/albedo.ktx" "") +add_ktxfiles("assets/models/monkey/albedo.png" "monkey/albedo_astc.ktx" "--compression=astc_fast_ldr_4x4") +add_ktxfiles("assets/models/monkey/albedo.png" "monkey/albedo_s3tc.ktx" "--compression=s3tc_rgb_dxt1") add_ktxfiles("assets/models/monkey/normal.png" "monkey/normal.ktx" "--kernel=NORMALS;--linear") add_ktxfiles("assets/models/monkey/roughness.png" "monkey/roughness.ktx" "--grayscale") add_ktxfiles("assets/models/monkey/metallic.png" "monkey/metallic.ktx" "--grayscale") diff --git a/samples/web/filaweb.cpp b/samples/web/filaweb.cpp index b62ea6dc6f..d436796d51 100644 --- a/samples/web/filaweb.cpp +++ b/samples/web/filaweb.cpp @@ -16,9 +16,6 @@ #include "filaweb.h" -#include -#include - #include #include @@ -32,6 +29,9 @@ #include +#include +#include + using namespace filament; using namespace image; using namespace std; @@ -313,4 +313,26 @@ SkyLight getSkyLight(Engine& engine, const char* name) { return result; } +filament::driver::CompressedPixelDataType toPixelDataType(uint32_t format) { + using DstFormat = filament::driver::CompressedPixelDataType; + switch (format) { + case KtxBundle::RGB_S3TC_DXT1: return DstFormat::DXT1_RGB; + case KtxBundle::RGBA_S3TC_DXT1: return DstFormat::DXT1_RGBA; + case KtxBundle::RGBA_S3TC_DXT3: return DstFormat::DXT3_RGBA; + case KtxBundle::RGBA_S3TC_DXT5: return DstFormat::DXT5_RGBA; + } + return (filament::driver::CompressedPixelDataType) 0xffff; +} + +filament::driver::TextureFormat toTextureFormat(uint32_t format) { + using DstFormat = filament::driver::TextureFormat; + switch (format) { + case KtxBundle::RGB_S3TC_DXT1: return DstFormat::DXT1_RGB; + case KtxBundle::RGBA_S3TC_DXT1: return DstFormat::DXT1_RGBA; + case KtxBundle::RGBA_S3TC_DXT3: return DstFormat::DXT3_RGBA; + case KtxBundle::RGBA_S3TC_DXT5: return DstFormat::DXT5_RGBA; + } + return (filament::driver::TextureFormat) 0xffff; +} + } // namespace filaweb diff --git a/samples/web/filaweb.h b/samples/web/filaweb.h index 6e082f07eb..21dc616fc7 100644 --- a/samples/web/filaweb.h +++ b/samples/web/filaweb.h @@ -60,6 +60,9 @@ struct SkyLight { SkyLight getSkyLight(filament::Engine& engine, const char* name); +filament::driver::CompressedPixelDataType toPixelDataType(uint32_t format); +filament::driver::TextureFormat toTextureFormat(uint32_t format); + static const auto NoopCallback = [](filament::Engine*, filament::View*) {}; class Application { diff --git a/samples/web/filaweb.js b/samples/web/filaweb.js index c2cf85f600..7003e40ef7 100644 --- a/samples/web/filaweb.js +++ b/samples/web/filaweb.js @@ -4,6 +4,29 @@ let context_ready = false; let previous_mouse_buttons = 0; let queued_mouse_events = []; +let use_astc = false; +let use_s3tc = false; +let use_etc1 = false; + +let canvas = document.getElementById('filament-canvas'); +let ctx = GL.createContext(canvas, { + majorVersion: 2, + minorVersion: 0, + antialias: false, + depth: false, + alpha: false +}); +GL.makeContextCurrent(ctx); +for (let ext of Module.ctx.getSupportedExtensions()) { + if (ext == "WEBGL_compressed_texture_s3tc") { + use_s3tc = true; + } else if (ext == "WEBGL_compressed_texture_astc") { + use_astc = true; + } else if (ext == "WEBGL_compressed_texture_etc1") { + use_etc1 = true; + } +} + // This is usually (not always) called before the wasm has finished JIT compiling. async function load(promises) { for (let name in promises) { @@ -13,18 +36,8 @@ async function load(promises) { maybe_launch(); } -// This is called as soon as the wasm has finished JIT compilation. We also create the WebGL 2.0 -// context here. +// This is called as soon as the wasm has finished JIT compilation. Module.postRun = function() { - let canvas = document.getElementById('filament-canvas'); - let ctx = GL.createContext(canvas, { - majorVersion: 2, - minorVersion: 0, - antialias: false, - depth: false, - alpha: false - }); - GL.makeContextCurrent(ctx); context_ready = true; maybe_launch(); } diff --git a/samples/web/suzanne.cpp b/samples/web/suzanne.cpp index a32cc1944a..12dbd72ba4 100644 --- a/samples/web/suzanne.cpp +++ b/samples/web/suzanne.cpp @@ -77,6 +77,26 @@ static Texture* setTextureParameter(Engine& engine, filaweb::Asset& asset, strin uint8_t* data; uint32_t nbytes; asset.texture->getBlob({}, &data, &nbytes); + + // Compressed textures in KTX always have a glFormat of 0. + if (info.glFormat == 0) { + assert(info.pixelWidth == info.pixelHeight); + driver::CompressedPixelDataType datatype = filaweb::toPixelDataType(info.glInternalFormat); + driver::TextureFormat texformat = filaweb::toTextureFormat(info.glInternalFormat); + Texture::PixelBufferDescriptor pb(data, nbytes, datatype, nbytes, destructor, &asset); + + auto texture = Texture::Builder() + .width(info.pixelWidth) + .height(info.pixelHeight) + .sampler(Texture::Sampler::SAMPLER_2D) + .format(texformat) + .build(engine); + + texture->setImage(engine, 0, std::move(pb)); + app.mi->setParameter(name.c_str(), texture, sampler); + return texture; + } + Texture::PixelBufferDescriptor pb(data, nbytes, format, Texture::Type::UBYTE, destructor, &asset); diff --git a/samples/web/suzanne.html b/samples/web/suzanne.html index f93d4a171e..520f424aa8 100644 --- a/samples/web/suzanne.html +++ b/samples/web/suzanne.html @@ -23,7 +23,8 @@