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 @@