diff --git a/CMakeLists.txt b/CMakeLists.txt index 7320cdf8d3..0ddb38e60d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -654,6 +654,7 @@ add_subdirectory(${LIBRARIES}/utils) add_subdirectory(${LIBRARIES}/viewer) add_subdirectory(${FILAMENT}/filament) add_subdirectory(${FILAMENT}/shaders) +add_subdirectory(${EXTERNAL}/basisu/tnt) add_subdirectory(${EXTERNAL}/civetweb/tnt) add_subdirectory(${EXTERNAL}/hat-trie/tnt) add_subdirectory(${EXTERNAL}/imgui/tnt) @@ -706,9 +707,6 @@ if (IS_HOST_PLATFORM) add_subdirectory(${FILAMENT}/samples) - add_subdirectory(${EXTERNAL}/basisu/tnt) - add_subdirectory(${EXTERNAL}/astcenc/tnt) - add_subdirectory(${EXTERNAL}/etc2comp) add_subdirectory(${EXTERNAL}/libassimp/tnt) add_subdirectory(${EXTERNAL}/libpng/tnt) add_subdirectory(${EXTERNAL}/libsdl2/tnt) diff --git a/android/filament-utils-android/src/main/cpp/Utils.cpp b/android/filament-utils-android/src/main/cpp/Utils.cpp index 2af710846e..7dfdaeb65b 100644 --- a/android/filament-utils-android/src/main/cpp/Utils.cpp +++ b/android/filament-utils-android/src/main/cpp/Utils.cpp @@ -99,8 +99,8 @@ JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void*) { int rc; - // KTXLoader - jclass ktxloaderClass = env->FindClass("com/google/android/filament/utils/KTXLoader"); + // KTX1Loader + jclass ktxloaderClass = env->FindClass("com/google/android/filament/utils/KTX1Loader"); if (ktxloaderClass == nullptr) return JNI_ERR; static const JNINativeMethod ktxMethods[] = { {(char*)"nCreateKTXTexture", (char*)"(JLjava/nio/Buffer;IZ)J", reinterpret_cast(nCreateKTXTexture)}, diff --git a/android/filament-utils-android/src/main/java/com/google/android/filament/utils/KTXLoader.kt b/android/filament-utils-android/src/main/java/com/google/android/filament/utils/KTXLoader.kt deleted file mode 100644 index ebb8e7a749..0000000000 --- a/android/filament-utils-android/src/main/java/com/google/android/filament/utils/KTXLoader.kt +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright (C) 2020 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. - */ - -package com.google.android.filament.utils - -import com.google.android.filament.Engine -import com.google.android.filament.IndirectLight -import com.google.android.filament.Skybox -import com.google.android.filament.Texture - -import java.nio.Buffer - -/** - * Utilities for consuming KTX files and producing Filament textures, IBLs, and sky boxes. - * - * KTX is a simple container format that makes it easy to bundle miplevels and cubemap faces - * into a single file. - */ -object KTXLoader { - class Options { - var srgb = false - } - - /** - * Consumes the content of a KTX file and produces a [Texture] object. - * - * @param engine Gets passed to the builder. - * @param buffer The content of the KTX File. - * @param options Loader options. - * @return The resulting Filament texture, or null on failure. - */ - fun createTexture(engine: Engine, buffer: Buffer, options: Options = Options()): Texture { - val nativeEngine = engine.nativeObject - val nativeTexture = nCreateKTXTexture(nativeEngine, buffer, buffer.remaining(), options.srgb) - return Texture(nativeTexture) - } - - /** - * Consumes the content of a KTX file and produces an [IndirectLight] object. - * - * @param engine Gets passed to the builder. - * @param buffer The content of the KTX File. - * @param options Loader options. - * @return The resulting Filament texture, or null on failure. - */ - fun createIndirectLight(engine: Engine, buffer: Buffer, options: Options = Options()): IndirectLight { - val nativeEngine = engine.nativeObject - val nativeIndirectLight = nCreateIndirectLight(nativeEngine, buffer, buffer.remaining(), options.srgb) - return IndirectLight(nativeIndirectLight) - } - - /** - * Consumes the content of a KTX file and produces a [Skybox] object. - * - * @param engine Gets passed to the builder. - * @param buffer The content of the KTX File. - * @param options Loader options. - * @return The resulting Filament texture, or null on failure. - */ - fun createSkybox(engine: Engine, buffer: Buffer, options: Options = Options()): Skybox { - val nativeEngine = engine.nativeObject - val nativeSkybox = nCreateSkybox(nativeEngine, buffer, buffer.remaining(), options.srgb) - return Skybox(nativeSkybox) - } - - /** - * Retrieves spherical harmonics from the content of a KTX file. - * - * @param buffer The content of the KTX File. - * @return The resulting array of 9 * 3 floats, or null on failure. - */ - fun getSphericalHarmonics(buffer: Buffer): FloatArray? { - val sphericalHarmonics = FloatArray(9 * 3) - val success = nGetSphericalHarmonics(buffer, buffer.remaining(), sphericalHarmonics) - return if (success) sphericalHarmonics else null - } - - private external fun nCreateKTXTexture(nativeEngine: Long, buffer: Buffer, remaining: Int, srgb: Boolean): Long - private external fun nCreateIndirectLight(nativeEngine: Long, buffer: Buffer, remaining: Int, srgb: Boolean): Long - private external fun nGetSphericalHarmonics(buffer: Buffer, remaining: Int, outSphericalHarmonics: FloatArray): Boolean - private external fun nCreateSkybox(nativeEngine: Long, buffer: Buffer, remaining: Int, srgb: Boolean): Long -} \ No newline at end of file diff --git a/android/samples/sample-gltf-viewer/src/main/java/com/google/android/filament/gltf/MainActivity.kt b/android/samples/sample-gltf-viewer/src/main/java/com/google/android/filament/gltf/MainActivity.kt index 531ada9d11..08ff0f36a2 100644 --- a/android/samples/sample-gltf-viewer/src/main/java/com/google/android/filament/gltf/MainActivity.kt +++ b/android/samples/sample-gltf-viewer/src/main/java/com/google/android/filament/gltf/MainActivity.kt @@ -150,12 +150,12 @@ class MainActivity : Activity() { val scene = modelViewer.scene val ibl = "default_env" readCompressedAsset("envs/$ibl/${ibl}_ibl.ktx").let { - scene.indirectLight = KTXLoader.createIndirectLight(engine, it) + scene.indirectLight = KTX1Loader.createIndirectLight(engine, it) scene.indirectLight!!.intensity = 30_000.0f viewerContent.indirectLight = modelViewer.scene.indirectLight } readCompressedAsset("envs/$ibl/${ibl}_skybox.ktx").let { - scene.skybox = KTXLoader.createSkybox(engine, it) + scene.skybox = KTX1Loader.createSkybox(engine, it) } } diff --git a/filament/backend/test/test_LoadImage.cpp b/filament/backend/test/test_LoadImage.cpp index 856717ff44..10067db0e2 100644 --- a/filament/backend/test/test_LoadImage.cpp +++ b/filament/backend/test/test_LoadImage.cpp @@ -24,11 +24,6 @@ #include #include -#ifndef IOS -#include -using namespace image; -#endif - using namespace filament; using namespace filament::backend; @@ -137,28 +132,6 @@ static void fillCheckerboard(void* buffer, size_t size, size_t stride, size_t co } } -#ifndef IOS -static PixelBufferDescriptor compressedCheckerboardPixelBuffer(size_t size) { - LinearImage uncompressed(size, size, 4); - fillCheckerboard(uncompressed.getPixelRef(), size, size, 4, 1.0f); - - S3tcConfig config { - .format = CompressedFormat::RGBA_S3TC_DXT1, - .srgb = false - }; - CompressedTexture compressed = s3tcCompress(uncompressed, config); - - void* buffer = malloc(compressed.size); - memcpy(buffer, compressed.data.get(), compressed.size); - - PixelBufferDescriptor descriptor(buffer, compressed.size, CompressedPixelDataType::DXT1_RGBA, - compressed.size, [](void* buffer, size_t size, void* user) { - free(buffer); - }, nullptr); - return descriptor; -} -#endif - static PixelBufferDescriptor checkerboardPixelBuffer(PixelDataFormat format, PixelDataType type, size_t size, size_t bufferPadding = 0) { size_t components; int bpp; @@ -355,11 +328,6 @@ TEST_F(BackendTest, UpdateImage2D) { testCases.emplace_back("RGBA, UBYTE -> RGBA8 (subregions, buffer padding)", PixelDataFormat::RGBA, PixelDataType::UBYTE, TextureFormat::RGBA8, 64u, true); testCases.emplace_back("RGB, FLOAT -> RGB32F (subregions, buffer padding)", PixelDataFormat::RGB, PixelDataType::FLOAT, TextureFormat::RGB32F, 64u, true); - // Test compresseed format upload. -#ifndef IOS - testCases.emplace_back("RGBA, DXT1_RGBA -> DXT1_RGBA", PixelDataFormat::RGBA, CompressedPixelDataType::DXT1_RGBA, TextureFormat::DXT1_RGBA); -#endif - auto& api = getDriverApi(); api.startCapture(); @@ -388,31 +356,21 @@ TEST_F(BackendTest, UpdateImage2D) { t.textureFormat, 1, 512, 512, 1u, usage); // Upload some pixel data. - if (t.compressed) { -#ifdef IOS - assert_invariant(false); -#else - assert_invariant(!t.uploadSubregions); - PixelBufferDescriptor descriptor = compressedCheckerboardPixelBuffer(512); - api.update2DImage(texture, 0, 0, 0, 512, 512, std::move(descriptor)); -#endif + if (t.uploadSubregions) { + const auto& pf = t.pixelFormat; + const auto& pt = t.pixelType; + PixelBufferDescriptor subregion1 = checkerboardPixelBuffer(pf, pt, 256, t.bufferPadding); + PixelBufferDescriptor subregion2 = checkerboardPixelBuffer(pf, pt, 256, t.bufferPadding); + PixelBufferDescriptor subregion3 = checkerboardPixelBuffer(pf, pt, 256, t.bufferPadding); + PixelBufferDescriptor subregion4 = checkerboardPixelBuffer(pf, pt, 256, t.bufferPadding); + api.update2DImage(texture, 0, 0, 0, 256, 256, std::move(subregion1)); + api.update2DImage(texture, 0, 256, 0, 256, 256, std::move(subregion2)); + api.update2DImage(texture, 0, 0, 256, 256, 256, std::move(subregion3)); + api.update2DImage(texture, 0, 256, 256, 256, 256, std::move(subregion4)); } else { - if (t.uploadSubregions) { - const auto& pf = t.pixelFormat; - const auto& pt = t.pixelType; - PixelBufferDescriptor subregion1 = checkerboardPixelBuffer(pf, pt, 256, t.bufferPadding); - PixelBufferDescriptor subregion2 = checkerboardPixelBuffer(pf, pt, 256, t.bufferPadding); - PixelBufferDescriptor subregion3 = checkerboardPixelBuffer(pf, pt, 256, t.bufferPadding); - PixelBufferDescriptor subregion4 = checkerboardPixelBuffer(pf, pt, 256, t.bufferPadding); - api.update2DImage(texture, 0, 0, 0, 256, 256, std::move(subregion1)); - api.update2DImage(texture, 0, 256, 0, 256, 256, std::move(subregion2)); - api.update2DImage(texture, 0, 0, 256, 256, 256, std::move(subregion3)); - api.update2DImage(texture, 0, 256, 256, 256, 256, std::move(subregion4)); - } else { - PixelBufferDescriptor descriptor - = checkerboardPixelBuffer(t.pixelFormat, t.pixelType, 512, t.bufferPadding); - api.update2DImage(texture, 0, 0, 0, 512, 512, std::move(descriptor)); - } + PixelBufferDescriptor descriptor + = checkerboardPixelBuffer(t.pixelFormat, t.pixelType, 512, t.bufferPadding); + api.update2DImage(texture, 0, 0, 0, 512, 512, std::move(descriptor)); } SamplerGroup samplers(1); diff --git a/libs/image/src/ImageSampler.cpp b/libs/image/src/ImageSampler.cpp index 00385e9323..98073c4894 100644 --- a/libs/image/src/ImageSampler.cpp +++ b/libs/image/src/ImageSampler.cpp @@ -335,6 +335,7 @@ void computeSingleSample(const LinearImage& source, float x, float y, SingleSamp } } +// Generates the given number of mipmaps (not including the base level) using the given filter. // 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) { diff --git a/libs/imageio/CMakeLists.txt b/libs/imageio/CMakeLists.txt index 25a417d899..dc1f6489f0 100644 --- a/libs/imageio/CMakeLists.txt +++ b/libs/imageio/CMakeLists.txt @@ -8,7 +8,7 @@ set(PUBLIC_HDR_DIR include) # Sources and headers # ================================================================================================== set(PUBLIC_HDRS - include/imageio/BlockCompression.h + include/imageio/BasisEncoder.h include/imageio/HDRDecoder.h include/imageio/ImageDecoder.h include/imageio/ImageDiffer.h @@ -16,7 +16,7 @@ set(PUBLIC_HDRS ) set(SRCS - src/BlockCompression.cpp + src/BasisEncoder.cpp src/HDRDecoder.cpp src/ImageDecoder.cpp src/ImageDiffer.cpp @@ -32,16 +32,11 @@ 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 stb EtcLib) +target_link_libraries(${TARGET} PUBLIC image math png tinyexr utils z stb basis_encoder) if (WIN32) target_link_libraries(${TARGET} PRIVATE wsock32) endif() -# ================================================================================================== -# Transitive macro definitions -# ================================================================================================== -target_compile_definitions(${TARGET} PUBLIC IMAGEIO_SUPPORTS_BLOCK_COMPRESSION) - # ================================================================================================== # Compiler flags # ================================================================================================== diff --git a/libs/imageio/include/imageio/BasisEncoder.h b/libs/imageio/include/imageio/BasisEncoder.h new file mode 100644 index 0000000000..b72385f9a4 --- /dev/null +++ b/libs/imageio/include/imageio/BasisEncoder.h @@ -0,0 +1,126 @@ +/* + * Copyright (C) 2022 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. + */ + +#ifndef IMAGE_BASISENCODER_H_ +#define IMAGE_BASISENCODER_H_ + +#include +#include +#include + +#include + +namespace image { + +struct BasisEncoderBuilderImpl; +struct BasisEncoderImpl; + +class UTILS_PUBLIC BasisEncoder { +public: + enum IntermediateFormat { + UASTC, + ETC1S, + }; + + class Builder { + public: + // The Ktx2 builder is constructed with a fixed number of miplevels and layers so + // that it can pre-allocate the appropriate BasisU input vectors. + // + // - mipCount: number of mipmap levels, including the base; must be at least 1. + // - layerCount: either 1 or the number of layers in an array texture. + // + // For cubemaps and cubemap arrays, multiply the layer count by 6 and pack the faces in + // standard GL order. + Builder(size_t mipCount, size_t layerCount) noexcept; + + ~Builder() noexcept; + Builder(Builder&& that) noexcept; + Builder& operator=(Builder&& that) noexcept; + + // Enables the linear flag, which does two things: + // (1) Specifies that the image should be encoded without a transfer function. + // (2) Adds a tag to the ktx file that tells the loader that no transfer function was used. + // + // Note that the tag does not actually affect the compression process, it's basically just a + // hint to the reader. At the time of this writing, BasisU does not make a distinction + // between sRGB targets and linear targets. + // + // default value: FALSE + Builder& linear(bool enabled) noexcept; + + // Enables cubemap (or cubemap array) mode. When this is enabled the number of layers + // should be divisible by 6. + // default value: FALSE + Builder& cubemap(bool enabled) noexcept; + + // Chooses the intermiediate format as described in the BasisU documentation. + // For highest quality, use UASTC. + // default value: UASTC + Builder& intermediateFormat(IntermediateFormat format) noexcept; + + // Honors only the first component of the incoming LinearImage. + // default value: FALSE + Builder& grayscale(bool enabled) noexcept; + + // Transforms the incoming image from [-1, +1] to [0, 1] before passing it to the encoder. + // default value: FALSE + Builder& normals(bool enabled) noexcept; + + // Initializes the basis encoder with the given number of jobs. + // default value: 4 + Builder& jobs(size_t count) noexcept; + + // Supresses status messages. + // default value: FALSE + Builder& quiet(bool enabled) noexcept; + + // Submits image data in linear floating-point format. + // This must be called for every miplevel. + Builder& miplevel(size_t mipIndex, size_t layerIndex, const LinearImage& image) noexcept; + + // Creates a BasisU encoder and returns null if an error occurred. + BasisEncoder* build(); + + private: + BasisEncoderBuilderImpl* mImpl; + Builder(const Builder&) = delete; + Builder& operator=(const Builder&) = delete; + }; + + ~BasisEncoder() noexcept; + BasisEncoder(BasisEncoder&& that) noexcept; + BasisEncoder& operator=(BasisEncoder&& that) noexcept; + + // Triggers compression of all miplevels and waits until all jobs are done. + // Returns false if an error occurred. The resulting KTX2 contents can be retrieved + // using the getters below. + bool encode(); + + size_t getKtx2ByteCount() const noexcept; + uint8_t const* getKtx2Data() const noexcept; + +private: + BasisEncoder(BasisEncoderImpl*) noexcept; + BasisEncoder(const BasisEncoder&) = delete; + BasisEncoder& operator=(const BasisEncoder&) = delete; + BasisEncoderImpl* mImpl; + friend struct BasisEncoderBuilderImpl; +}; + +} // namespace image + +#endif // IMAGE_BASISENCODER_H_ diff --git a/libs/imageio/src/BasisEncoder.cpp b/libs/imageio/src/BasisEncoder.cpp new file mode 100644 index 0000000000..25f3e5533c --- /dev/null +++ b/libs/imageio/src/BasisEncoder.cpp @@ -0,0 +1,267 @@ +/* + * Copyright (C) 2022 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 + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Warray-bounds" +#include +#pragma clang diagnostic pop + +namespace image { + +using Builder = BasisEncoder::Builder; + +struct BasisEncoderBuilderImpl { + basisu::basis_compressor_params params = {}; + bool grayscale = false; + bool linear = false; + bool normals = false; + bool quiet = false; + size_t jobs = 4; + bool error = false; +}; + +struct BasisEncoderImpl { + basisu::basis_compressor* encoder; + basisu::job_pool* jobs; + bool quiet; +}; + +Builder::Builder(size_t mipCount, size_t layerCount) noexcept : mImpl(new BasisEncoderBuilderImpl) { + const size_t mipsPerImage = mipCount > 0 ? mipCount : 1; + const bool multiple = mImpl->params.m_source_images.size() > 1; + mImpl->params.m_tex_type = multiple ? basist::cBASISTexType2DArray : basist::cBASISTexType2D; + mImpl->params.m_uastc = true; + mImpl->params.m_source_images.resize(layerCount); + mImpl->params.m_source_mipmap_images.resize(layerCount); + if (mipCount > 1) { + for (size_t layer = 0; layer < layerCount; ++layer) { + mImpl->params.m_source_mipmap_images[layer].resize(mipCount - 1); + } + } +} + +Builder::~Builder() noexcept { delete mImpl; } + +Builder::Builder(Builder&& that) noexcept { + std::swap(mImpl, that.mImpl); +} + +Builder& Builder::operator=(Builder&& that) noexcept { + std::swap(mImpl, that.mImpl); + return *this; +} + +Builder& Builder::miplevel(size_t level, size_t layer, const LinearImage& floatImage) noexcept { + if (layer >= mImpl->params.m_source_images.size()) { + assert_invariant(false); + mImpl->error = true; + return *this; + } + + auto& basisBaseLevel = mImpl->params.m_source_images[layer]; + auto& basisMipmaps = mImpl->params.m_source_mipmap_images[layer]; + + if (level >= basisMipmaps.size() + 1) { + assert_invariant(false); + mImpl->error = true; + return *this; + } + basisu::image* basisImage = level == 0 ? &basisBaseLevel : &basisMipmaps[level - 1]; + + LinearImage sourceImage = mImpl->normals ? vectorsToColors(floatImage) : floatImage; + + const bool applyTransferFunction = !mImpl->linear; + + if (mImpl->grayscale) { + std::unique_ptr data = applyTransferFunction ? + fromLinearTosRGB(sourceImage) : fromLinearToGrayscale(sourceImage); + basisImage->init(data.get(), floatImage.getWidth(), floatImage.getHeight(), 1); + } else { + std::unique_ptr data = applyTransferFunction ? + fromLinearTosRGB(sourceImage) : fromLinearToRGB(sourceImage); + basisImage->init(data.get(), floatImage.getWidth(), floatImage.getHeight(), 4); + } + + return *this; +} + +Builder& Builder::linear(bool enabled) noexcept { + mImpl->linear = enabled; + return *this; +} + +Builder& Builder::cubemap(bool enabled) noexcept { + if (enabled) { + mImpl->params.m_tex_type = basist::cBASISTexTypeCubemapArray; + } else { + const bool multi = mImpl->params.m_source_images.size() > 1; + mImpl->params.m_tex_type = multi ? basist::cBASISTexType2DArray : basist::cBASISTexType2D; + } + return *this; +} + +Builder& Builder::intermediateFormat(BasisEncoder::IntermediateFormat format) noexcept { + mImpl->params.m_uastc = format == IntermediateFormat::UASTC; + return *this; +} + +Builder& Builder::grayscale(bool enabled) noexcept { + mImpl->grayscale = enabled; + return *this; +} + +Builder& Builder::normals(bool enabled) noexcept { + mImpl->normals = enabled; + return *this; +} + +Builder& Builder::jobs(size_t count) noexcept { + mImpl->jobs = count; + return *this; +} + +Builder& Builder::quiet(bool enabled) noexcept { + mImpl->quiet = enabled; + return *this; +} + +BasisEncoder* Builder::build() { + if (mImpl->error) { + return nullptr; + } + + basisu::basisu_encoder_init(); + + auto& params = mImpl->params; + + params.m_status_output = !mImpl->quiet; + params.m_pJob_pool = new basisu::job_pool(mImpl->jobs); + params.m_create_ktx2_file = true; + params.m_ktx2_uastc_supercompression = basist::KTX2_SS_ZSTANDARD; + + // This sRGB flag doesn't actually affect the compression scheme or the basis format, it's just + // an annotation that gets stored in the KTX2 file, which enables the app to choose the right + // format when it loads and transcodes the texture. Technically however, the transcoder + // SHOULD know about this, since in some scenarios it needs to interpolate between colors. + params.m_ktx2_srgb_transfer_func = !mImpl->linear; + + // Select the same quality that the basis tool selects by default (midpoint of range). + params.m_quality_level = 128; + + // This is the default zstd compression level used by the basisu cmdline cool. + params.m_ktx2_zstd_supercompression_level = 6; + + // We do not want basis to read from files, we want it to read from "m_source_images" + params.m_read_source_images = false; + + // We do not want basis to write the file, we want to manually dump "get_output_ktx2_file()" + params.m_write_output_basis_files = false; + + basisu::basis_compressor* encoder = new basisu::basis_compressor(); + + if (!encoder->init(params)) { + assert_invariant(false); + delete encoder; + return nullptr; + } + + return new BasisEncoder(new BasisEncoderImpl { + .encoder = encoder, + .jobs = params.m_pJob_pool, + .quiet = mImpl->quiet, + }); +} + +BasisEncoder::BasisEncoder(BasisEncoderImpl* impl) noexcept : mImpl(impl) {} + +BasisEncoder::~BasisEncoder() noexcept { + delete mImpl->encoder; + delete mImpl->jobs; + delete mImpl; + basisu::basisu_encoder_deinit(); +} + +BasisEncoder::BasisEncoder(BasisEncoder&& that) noexcept { + std::swap(mImpl, that.mImpl); +} + +BasisEncoder& BasisEncoder::operator=(BasisEncoder&& that) noexcept { + std::swap(mImpl, that.mImpl); + return *this; +} + +bool BasisEncoder::encode() { + using namespace basisu; + basis_compressor::error_code ec = mImpl->encoder->process(); + switch (ec) + { + case basis_compressor::cECSuccess: + if (!mImpl->quiet) { + puts("Compression succeeded."); + } + return true; + case basis_compressor::cECFailedReadingSourceImages: + puts("Compressor failed reading a source image!"); + break; + case basis_compressor::cECFailedValidating: + puts("Compressor failed 2darray/cubemap/video validation checks!"); + break; + case basis_compressor::cECFailedEncodeUASTC: + puts("Compressor UASTC encode failed!"); + break; + case basis_compressor::cECFailedFrontEnd: + puts("Compressor frontend stage failed!"); + break; + case basis_compressor::cECFailedFontendExtract: + puts("Compressor frontend data extraction failed!"); + break; + case basis_compressor::cECFailedBackend: + puts("Compressor backend stage failed!"); + break; + case basis_compressor::cECFailedCreateBasisFile: + puts("Compressor failed creating Basis file data!"); + break; + case basis_compressor::cECFailedWritingOutput: + puts("Compressor failed writing to output Basis file!"); + break; + case basis_compressor::cECFailedUASTCRDOPostProcess: + puts("Compressor failed during the UASTC post process step!"); + break; + case basis_compressor::cECFailedCreateKTX2File: + puts("Compressor failed creating KTX2 file data!"); + break; + default: + puts("basis_compress::process() failed!"); + break; + } + return false; +} + +size_t BasisEncoder::getKtx2ByteCount() const noexcept { + return mImpl->encoder->get_output_ktx2_file().size(); +} + +uint8_t const* BasisEncoder::getKtx2Data() const noexcept { + return mImpl->encoder->get_output_ktx2_file().data(); +} + +} // namespace image diff --git a/libs/ktxreader/CMakeLists.txt b/libs/ktxreader/CMakeLists.txt index 6036c90a12..358e320906 100644 --- a/libs/ktxreader/CMakeLists.txt +++ b/libs/ktxreader/CMakeLists.txt @@ -9,10 +9,12 @@ set(PUBLIC_HDR_DIR include) # ================================================================================================== set(PUBLIC_HDRS include/ktxreader/Ktx1Reader.h + include/ktxreader/Ktx2Reader.h ) set(SRCS src/Ktx1Reader.cpp + src/Ktx2Reader.cpp ) # ================================================================================================== @@ -22,7 +24,7 @@ include_directories(${PUBLIC_HDR_DIR}) add_library(${TARGET} STATIC ${PUBLIC_HDRS} ${SRCS}) -target_link_libraries(${TARGET} PUBLIC utils image filament) +target_link_libraries(${TARGET} PUBLIC utils image filament basis_transcoder) target_include_directories(${TARGET} PUBLIC ${PUBLIC_HDR_DIR}) diff --git a/libs/ktxreader/include/ktxreader/Ktx1Reader.h b/libs/ktxreader/include/ktxreader/Ktx1Reader.h index 216c1da54b..6516daaaf4 100644 --- a/libs/ktxreader/include/ktxreader/Ktx1Reader.h +++ b/libs/ktxreader/include/ktxreader/Ktx1Reader.h @@ -14,14 +14,17 @@ * limitations under the License. */ -#ifndef IMAGE_KTXUTILITY_H -#define IMAGE_KTXUTILITY_H - -#include -#include +#ifndef KTXREADER_KTX1READER_H +#define KTXREADER_KTX1READER_H #include +#include + +namespace filament { + class Engine; +} + namespace ktxreader { using KtxInfo = image::KtxInfo; diff --git a/libs/ktxreader/include/ktxreader/Ktx2Reader.h b/libs/ktxreader/include/ktxreader/Ktx2Reader.h new file mode 100644 index 0000000000..88ff399e8d --- /dev/null +++ b/libs/ktxreader/include/ktxreader/Ktx2Reader.h @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2022 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. + */ + +#ifndef KTXREADER_KTX2READER_H +#define KTXREADER_KTX2READER_H + +#include +#include + +#include + +#include + +namespace filament { + class Engine; +} + +namespace basist { + class ktx2_transcoder; +} + +namespace ktxreader { + +class Ktx2Reader { + public: + using Engine = filament::Engine; + using Texture = filament::Texture; + enum TransferFunction { LINEAR, sRGB }; + + Ktx2Reader(Engine& engine, bool quiet = false); + ~Ktx2Reader(); + + /** + * Requests that the reader constructs Filament textures with given internal format. + * + * This MUST be called at least once before calling load(). + * + * As a reminder, a basis-encoded KTX2 can be quickly transcoded to any number of formats, + * so you need to tell it what formats your hw supports. That's why this method exists. + * + * Call requestFormat as many times as needed; formats that are submitted early are + * considered higher priority. + * + * If BasisU knows a priori that the given format is not available (e.g. if the build has + * disabled it), this returns false and the format is not added to the list. + * + * Returns false if the given format has already been requested. + * + * Hint: BasisU supports the following uncompressed formats: RGBA8, RGB565, RGBA4. + */ + bool requestFormat(Texture::InternalFormat format); + + /** + * Removes the given format from the list, or does nothing if it hasn't been requested. + */ + void unrequestFormat(Texture::InternalFormat format); + + /** + * Attempts to create a Filament texture from the given KTX2 blob. If none of the requested + * formats can be extracted from the data, this returns null. + */ + Texture* load(const uint8_t* data, size_t size, TransferFunction transfer = LINEAR); + + private: + Ktx2Reader(const Ktx2Reader&) = delete; + Ktx2Reader& operator=(const Ktx2Reader&) = delete; + Ktx2Reader(Ktx2Reader&& that) noexcept = delete; + Ktx2Reader& operator=(Ktx2Reader&& that) noexcept = delete; + + Engine& mEngine; + bool mQuiet; + basist::ktx2_transcoder* const mTranscoder; + + utils::FixedCapacityVector mRequestedFormats; +}; + +} // namespace ktxreader + +#endif diff --git a/libs/ktxreader/src/Ktx1Reader.cpp b/libs/ktxreader/src/Ktx1Reader.cpp index 6433e08cec..855e5afa6c 100644 --- a/libs/ktxreader/src/Ktx1Reader.cpp +++ b/libs/ktxreader/src/Ktx1Reader.cpp @@ -17,6 +17,9 @@ #include #include +#include +#include + namespace ktxreader { namespace Ktx1Reader { diff --git a/libs/ktxreader/src/Ktx2Reader.cpp b/libs/ktxreader/src/Ktx2Reader.cpp new file mode 100644 index 0000000000..1a93a0af7d --- /dev/null +++ b/libs/ktxreader/src/Ktx2Reader.cpp @@ -0,0 +1,276 @@ +/* + * Copyright (C) 2022 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 + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Warray-bounds" +#include +#pragma clang diagnostic pop + +using namespace basist; +using namespace filament; + +using TransferFunction = ktxreader::Ktx2Reader::TransferFunction; + +namespace { +struct FinalFormatInfo { + bool isSupported; + bool isCompressed; + TransferFunction transferFunction; + transcoder_texture_format basisFormat; + Texture::CompressedType compressedPixelDataType; + Texture::Type pixelDataType; + Texture::Format pixelDataFormat; +}; +} + +// This function returns various information about a Filament internal format, most notably its +// equivalent BasisU enumerant. +// +// Return by value isn't expensive here due to copy elision. +// +// Note that Filament's internal format list mimics the Vulkan format list, which +// embeds transfer function information (i.e. sRGB or not) into the format, whereas +// the basis format list does not. +// +// The following formats supported by BasisU but are not supported by Filament. +// +// transcoder_texture_format::cTFETC1_RGB +// transcoder_texture_format::cTFATC_RGB +// transcoder_texture_format::cTFATC_RGBA +// transcoder_texture_format::cTFFXT1_RGB +// transcoder_texture_format::cTFPVRTC2_4_RGB +// transcoder_texture_format::cTFPVRTC2_4_RGBA +// transcoder_texture_format::cTFPVRTC1_4_RGB +// transcoder_texture_format::cTFPVRTC1_4_RGBA +// transcoder_texture_format::cTFBC4_R +// transcoder_texture_format::cTFBC5_RG +// transcoder_texture_format::cTFBC7_RGBA (this format would add size bloat to the transcoder) +// transcoder_texture_format::cTFBGR565 (note the blue/red swap) +// +static FinalFormatInfo getFinalFormatInfo(Texture::InternalFormat fmt) { + using tif = Texture::InternalFormat; + using tct = Texture::CompressedType; + using tt = Texture::Type; + using tf = Texture::Format; + using ttf = transcoder_texture_format; + const auto sRGB = ktxreader::Ktx2Reader::sRGB; + const auto LINEAR = ktxreader::Ktx2Reader::LINEAR; + switch (fmt) { + case tif::ETC2_EAC_SRGBA8: return {true, true, sRGB, ttf::cTFETC2_RGBA, tct::ETC2_EAC_RGBA8}; + case tif::ETC2_EAC_RGBA8: return {true, true, LINEAR, ttf::cTFETC2_RGBA, tct::ETC2_EAC_SRGBA8}; + case tif::DXT1_SRGB: return {true, true, sRGB, ttf::cTFBC1_RGB, tct::DXT1_RGB}; + case tif::DXT1_RGB: return {true, true, LINEAR, ttf::cTFBC1_RGB, tct::DXT1_SRGB}; + case tif::DXT3_SRGBA: return {true, true, sRGB, ttf::cTFBC3_RGBA, tct::DXT3_RGBA}; + case tif::DXT3_RGBA: return {true, true, LINEAR, ttf::cTFBC3_RGBA, tct::DXT3_SRGBA}; + case tif::SRGB8_ALPHA8_ASTC_4x4: return {true, true, sRGB, ttf::cTFASTC_4x4_RGBA, tct::RGBA_ASTC_4x4}; + case tif::RGBA_ASTC_4x4: return {true, true, LINEAR, ttf::cTFASTC_4x4_RGBA, tct::SRGB8_ALPHA8_ASTC_4x4}; + case tif::EAC_R11: return {true, true, LINEAR, ttf::cTFETC2_EAC_R11, tct::EAC_R11}; + + // The following format is useful for normal maps. + // Note that BasisU supports only the unsigned variant. + case tif::EAC_RG11: return {true, true, LINEAR, ttf::cTFETC2_EAC_RG11, tct::EAC_RG11}; + + // Uncompressed formats. + case tif::SRGB8_A8: return {true, false, sRGB, ttf::cTFRGBA32, {}, tt::UBYTE, tf::RGBA}; + case tif::RGBA8: return {true, false, LINEAR, ttf::cTFRGBA32, {}, tt::UBYTE, tf::RGBA}; + case tif::RGB565: return {true, false, LINEAR, ttf::cTFRGB565, {}, tt::USHORT_565, tf::RGB}; + case tif::RGBA4: return {true, false, LINEAR, ttf::cTFRGBA4444, {}, tt::USHORT, tf::RGBA}; + + default: return {false}; + } +} + +namespace ktxreader { + +Ktx2Reader::Ktx2Reader(Engine& engine, bool quiet) : + mEngine(engine), + mQuiet(quiet), + mTranscoder(new ktx2_transcoder()) { + mRequestedFormats.reserve((size_t) transcoder_texture_format::cTFTotalTextureFormats); + basisu_transcoder_init(); +} + +Ktx2Reader::~Ktx2Reader() { + delete mTranscoder; +} + +bool Ktx2Reader::requestFormat(Texture::InternalFormat format) { + if (!getFinalFormatInfo(format).isSupported) { + return false; + } + for (Texture::InternalFormat fmt : mRequestedFormats) { + if (fmt == format) { + return false; + } + } + mRequestedFormats.push_back(format); + return true; +} + +void Ktx2Reader::unrequestFormat(Texture::InternalFormat format) { + for (auto iter = mRequestedFormats.begin(); iter != mRequestedFormats.end(); ++iter) { + if (*iter == format) { + mRequestedFormats.erase(iter); + return; + } + } +} + +Texture* Ktx2Reader::load(const uint8_t* data, size_t size, TransferFunction transfer) { + if (!mTranscoder->init(data, size)) { + if (!mQuiet) { + utils::slog.e << "BasisU transcoder init failed." << utils::io::endl; + } + return nullptr; + } + + if (mTranscoder->get_dfd_transfer_func() == KTX2_KHR_DF_TRANSFER_LINEAR && transfer == sRGB) { + if (!mQuiet) { + utils::slog.e << "Source texture is marked linear, but client is requesting sRGB." + << utils::io::endl; + } + return nullptr; + } + + if (mTranscoder->get_dfd_transfer_func() == KTX2_KHR_DF_TRANSFER_SRGB && transfer == LINEAR) { + if (!mQuiet) { + utils::slog.e << "Source texture is marked sRGB, but client is requesting linear." + << utils::io::endl; + } + return nullptr; + } + + if (!mTranscoder->start_transcoding()) { + if (!mQuiet) { + utils::slog.e << "BasisU start_transcoding failed." << utils::io::endl; + } + return nullptr; + } + + // TODO: support cubemaps. For now we use KTX1 for cubemaps because basisu does not support HDR. + if (mTranscoder->get_faces() == 6) { + if (!mQuiet) { + utils::slog.e << "Cubemaps are not yet supported." << utils::io::endl; + } + return nullptr; + } + + // TODO: support texture arrays. + if (mTranscoder->get_layers() > 1) { + if (!mQuiet) { + utils::slog.e << "Texture arrays are not yet supported." << utils::io::endl; + } + return nullptr; + } + + // Fierst pass through, just to make sure we can transcode it. + bool found = false; + Texture::InternalFormat resolvedFormat; + for (Texture::InternalFormat requestedFormat : mRequestedFormats) { + if (!Texture::isTextureFormatSupported(mEngine, requestedFormat)) { + continue; + } + const auto info = getFinalFormatInfo(requestedFormat); + if (!info.isSupported || info.transferFunction != transfer) { + continue; + } + if (!basis_is_format_supported(info.basisFormat, mTranscoder->get_format())) { + continue; + } + const uint32_t layerIndex = 0; + const uint32_t faceIndex = 0; + for (uint32_t levelIndex = 0; levelIndex < mTranscoder->get_levels(); levelIndex++) { + basist::ktx2_image_level_info info; + if (!mTranscoder->get_image_level_info(info, levelIndex, layerIndex, faceIndex)) { + continue; + } + } + found = true; + resolvedFormat = requestedFormat; + break; + } + + if (!found) { + if (!mQuiet) { + utils::slog.e << "Unable to decode any of the requested formats." << utils::io::endl; + } + return nullptr; + } + + const auto formatInfo = getFinalFormatInfo(resolvedFormat); + + Texture* texture = Texture::Builder() + .width(mTranscoder->get_width()) + .height(mTranscoder->get_height()) + .levels(mTranscoder->get_levels()) + .sampler(Texture::Sampler::SAMPLER_2D) + .format(resolvedFormat) + .build(mEngine); + + // In theory we could pass "free" directly into the callback but that triggers ASAN warnings. + Texture::PixelBufferDescriptor::Callback cb = [](void* buf, size_t, void* userdata) { + free(buf); + }; + + const uint32_t layerIndex = 0; + const uint32_t faceIndex = 0; + for (uint32_t levelIndex = 0; levelIndex < mTranscoder->get_levels(); levelIndex++) { + basist::ktx2_image_level_info levelInfo; + mTranscoder->get_image_level_info(levelInfo, levelIndex, layerIndex, faceIndex); + const basisu::texture_format destFormat = + basis_get_basisu_texture_format(formatInfo.basisFormat); + if (formatInfo.isCompressed) { + const uint32_t qwordsPerBlock = basisu::get_qwords_per_block(destFormat); + const size_t byteCount = sizeof(uint64_t) * qwordsPerBlock * levelInfo.m_total_blocks; + uint64_t* const blocks = (uint64_t*) malloc(byteCount); + const uint32_t flags = 0; + if (!mTranscoder->transcode_image_level(levelIndex, layerIndex, faceIndex, blocks, + levelInfo.m_total_blocks, formatInfo.basisFormat, flags)) { + utils::slog.e << "Failed to transcode level " << levelIndex << utils::io::endl; + return nullptr; + } + Texture::PixelBufferDescriptor pbd(blocks, byteCount, + formatInfo.compressedPixelDataType, byteCount, cb, nullptr); + texture->setImage(mEngine, levelIndex, std::move(pbd)); + } else { + // The transcoder still does work even for uncompressed formats, because of zstd. + const uint32_t rowCount = levelInfo.m_orig_height; + const uint32_t bytesPerPix = basis_get_bytes_per_block_or_pixel(formatInfo.basisFormat); + const size_t byteCount = bytesPerPix * levelInfo.m_orig_width * rowCount; + uint64_t* const rows = (uint64_t*) malloc(byteCount); + const uint32_t flags = 0; + if (!mTranscoder->transcode_image_level(levelIndex, layerIndex, faceIndex, rows, + byteCount / bytesPerPix, formatInfo.basisFormat, flags)) { + utils::slog.e << "Failed to transcode level " << levelIndex << utils::io::endl; + return nullptr; + } + Texture::PixelBufferDescriptor pbd(rows, byteCount, formatInfo.pixelDataFormat, + formatInfo.pixelDataType, cb, nullptr); + texture->setImage(mEngine, levelIndex, std::move(pbd)); + } + } + + return texture; +} + +} // namespace ktxreader diff --git a/libs/ktxreader/tests/test_ktxreader.cpp b/libs/ktxreader/tests/test_ktxreader.cpp index f66a961bd0..c583045232 100644 --- a/libs/ktxreader/tests/test_ktxreader.cpp +++ b/libs/ktxreader/tests/test_ktxreader.cpp @@ -15,6 +15,7 @@ */ #include +#include #include #include @@ -71,7 +72,22 @@ TEST_F(KtxReaderTest, Ktx2) { const auto contents = readFile(parent + "color_grid_uastc_zstd.ktx2"); ASSERT_EQ(contents.size(), 170512); - // TODO: create Filament texture from the KTX2 file. + ktxreader::Ktx2Reader reader(*engine); + + reader.requestFormat(Texture::InternalFormat::DXT3_SRGBA); + reader.requestFormat(Texture::InternalFormat::DXT3_RGBA); + + // Uncompressed formats are lower priority, so they get added last. + reader.requestFormat(Texture::InternalFormat::SRGB8_A8); + reader.requestFormat(Texture::InternalFormat::RGBA8); + + Texture* tex = reader.load(contents.data(), contents.size()); + + ASSERT_TRUE(tex != nullptr); + ASSERT_EQ(tex->getFormat(), Texture::InternalFormat::DXT3_RGBA); + ASSERT_EQ(tex->getWidth(), 1024); + + engine->destroy(tex); } int main(int argc, char** argv) { diff --git a/samples/CMakeLists.txt b/samples/CMakeLists.txt index dae5b24f2a..bd110bc3ff 100644 --- a/samples/CMakeLists.txt +++ b/samples/CMakeLists.txt @@ -148,12 +148,11 @@ endfunction() add_mesh("assets/models/monkey/monkey.obj" "suzanne.filamesh") -# Use a RGBA compression format for Metal and Vulkan support. -set (COMPRESSION "--compression=s3tc_rgba_dxt5") -add_ktxfiles("assets/models/monkey/albedo.png" "albedo_s3tc.ktx" "${COMPRESSION}") -add_ktxfiles("assets/models/monkey/roughness.png" "roughness.ktx" "${COMPRESSION};--grayscale;--linear") -add_ktxfiles("assets/models/monkey/metallic.png" "metallic.ktx" "${COMPRESSION};--grayscale;--linear") -add_ktxfiles("assets/models/monkey/ao.png" "ao.ktx" "${COMPRESSION};--grayscale;--linear") +set (COMPRESSION "--compression=uastc") +add_ktxfiles("assets/models/monkey/albedo.png" "albedo.ktx2" "${COMPRESSION}") +add_ktxfiles("assets/models/monkey/roughness.png" "roughness.ktx2" "${COMPRESSION};--grayscale;--linear") +add_ktxfiles("assets/models/monkey/metallic.png" "metallic.ktx2" "${COMPRESSION};--grayscale;--linear") +add_ktxfiles("assets/models/monkey/ao.png" "ao.ktx2" "${COMPRESSION};--grayscale;--linear") add_pngfile("assets/models/monkey/normal.png" "normal.png") diff --git a/samples/suzanne.cpp b/samples/suzanne.cpp index b7a44bc878..52298a0f2e 100644 --- a/samples/suzanne.cpp +++ b/samples/suzanne.cpp @@ -25,10 +25,11 @@ #include #include +#include #include -#include +#include #include #include @@ -66,7 +67,7 @@ static const char* IBL_FOLDER = "assets/ibl/lightroom_14b"; static void printUsage(char* name) { std::string exec_name(utils::Path(name).getName()); std::string usage( - "SHOWCASE renders a Suzanne model with S3TC textures.\n" + "SHOWCASE renders a Suzanne model with compressed textures.\n" "Usage:\n" " SHOWCASE [options]\n" "Options:\n" @@ -144,15 +145,28 @@ int main(int argc, char** argv) { auto& rcm = engine->getRenderableManager(); auto& em = utils::EntityManager::get(); - // Create textures. The KTX bundles are freed by KtxUtility. - auto albedo = new image::Ktx1Bundle(MONKEY_ALBEDO_S3TC_DATA, MONKEY_ALBEDO_S3TC_SIZE); - auto ao = new image::Ktx1Bundle(MONKEY_AO_DATA, MONKEY_AO_SIZE); - auto metallic = new image::Ktx1Bundle(MONKEY_METALLIC_DATA, MONKEY_METALLIC_SIZE); - auto roughness = new image::Ktx1Bundle(MONKEY_ROUGHNESS_DATA, MONKEY_ROUGHNESS_SIZE); - app.albedo = Ktx1Reader::createTexture(engine, albedo, true); - app.ao = Ktx1Reader::createTexture(engine, ao, false); - app.metallic = Ktx1Reader::createTexture(engine, metallic, false); - app.roughness = Ktx1Reader::createTexture(engine, roughness, false); + Ktx2Reader reader(*engine); + + reader.requestFormat(Texture::InternalFormat::DXT3_SRGBA); + reader.requestFormat(Texture::InternalFormat::DXT3_RGBA); + + // Uncompressed formats are lower priority, so they get added last. + reader.requestFormat(Texture::InternalFormat::SRGB8_A8); + reader.requestFormat(Texture::InternalFormat::RGBA8); + + app.albedo = reader.load(MONKEY_ALBEDO_DATA, MONKEY_ALBEDO_SIZE, Ktx2Reader::sRGB); + app.ao = reader.load(MONKEY_AO_DATA, MONKEY_AO_SIZE); + app.metallic = reader.load(MONKEY_METALLIC_DATA, MONKEY_METALLIC_SIZE); + app.roughness = reader.load(MONKEY_ROUGHNESS_DATA, MONKEY_ROUGHNESS_SIZE); + +#if !defined(NDEBUG) + using namespace utils; + slog.i << "Resolved format for albedo: " << app.albedo->getFormat() << io::endl; + slog.i << "Resolved format for ambient occlusion: " << app.ao->getFormat() << io::endl; + slog.i << "Resolved format for metallic: " << app.metallic->getFormat() << io::endl; + slog.i << "Resolved format for roughness: " << app.roughness->getFormat() << io::endl; +#endif + app.normal = loadNormalMap(engine, MONKEY_NORMAL_DATA, MONKEY_NORMAL_SIZE); TextureSampler sampler(TextureSampler::MinFilter::LINEAR_MIPMAP_LINEAR, TextureSampler::MagFilter::LINEAR); diff --git a/third_party/basisu/tnt/CMakeLists.txt b/third_party/basisu/tnt/CMakeLists.txt index 9cae80aafe..36a7de2756 100644 --- a/third_party/basisu/tnt/CMakeLists.txt +++ b/third_party/basisu/tnt/CMakeLists.txt @@ -40,6 +40,10 @@ set (BASIS_CONFIG BASISD_SUPPORT_FXT1=0 ) +# The following BasisU setting is useful when diagnosing issues, but we're leaving it turned off +# even for debug builds, since it is quite verbose. +# set (BASIS_CONFIG ${BASIS_CONFIG} $<$:BASISU_FORCE_DEVEL_MESSAGES=1>) + # DXT5A and DXT1 are both required for cTFBC3_RGBA aka DXT3_RGBA. if (NOT IS_MOBILE_TARGET) set (BASIS_CONFIG ${BASIS_CONFIG} BASISD_SUPPORT_DXT5A=1 BASISD_SUPPORT_DXT1=1) diff --git a/tools/cmgen/src/cmgen.cpp b/tools/cmgen/src/cmgen.cpp index 5e67779abe..ed8e7f14ad 100644 --- a/tools/cmgen/src/cmgen.cpp +++ b/tools/cmgen/src/cmgen.cpp @@ -23,10 +23,6 @@ #include #include -#ifdef IMAGEIO_SUPPORTS_BLOCK_COMPRESSION -#include -#endif - #include #include @@ -167,20 +163,13 @@ static void printUsage(char* name) { " Quiet mode. Suppress all non-error output\n\n" " --type=[cubemap|equirect|octahedron|ktx], -t [cubemap|equirect|octahedron|ktx]\n" " Specify output type (default: cubemap)\n\n" - " --format=[exr|hdr|psd|rgbm|rgb32f|png|dds|ktx], -f [exr|hdr|psd|rgbm|rgb32f|png|dds|ktx]\n" + " --format=[exr|hdr|psd|rgbm|rgb32f|png|dds|ktx], -f [format]\n" " Specify output file format. ktx implies -type=ktx.\n" - " KTX files are always encoded with 3-channel RGB_10_11_11_REV data\n\n" + " KTX files are always KTX1 files, not KTX2.\n" + " They are encoded with 3-channel RGB_10_11_11_REV data\n\n" " --compression=COMPRESSION, -c COMPRESSION\n" " Format specific compression:\n" -#ifdef IMAGEIO_SUPPORTS_BLOCK_COMPRESSION - " KTX:\n" - " astc_[fast|thorough]_[ldr|hdr]_WxH, where WxH is a valid block size\n" - " s3tc_rgba_dxt5\n" - " etc_FORMAT_METRIC_EFFORT\n" - " FORMAT is rgb8_alpha, srgb8_alpha, rgba8, or srgb8_alpha8\n" - " METRIC is rgba, rgbx, rec709, numeric, or normalxyz\n" - " EFFORT is an integer between 0 and 100\n" -#endif + " KTX: ignored\n" " PNG: Ignored\n" " PNG RGBM: Ignored\n" " Radiance: Ignored\n" @@ -1270,31 +1259,6 @@ static void saveImage(const std::string& path, ImageEncoder::Format format, cons static void exportKtxFaces(Ktx1Bundle& container, uint32_t miplevel, const Cubemap& cm) { auto& info = container.info(); - -#ifdef IMAGEIO_SUPPORTS_BLOCK_COMPRESSION - CompressionConfig compression {}; - if (!g_compression.empty()) { - bool valid = parseOptionString(g_compression, &compression); - if (!valid) { - std::cerr << "Unrecognized compression: " << g_compression << std::endl; - exit(1); - } - // The KTX spec says the following for compressed textures: glTypeSize should 1, - // glFormat should be 0, and glBaseInternalFormat should be RED, RG, RGB, or RGBA. - // The glInternalFormat field is the only field that specifies the actual format. - info.glTypeSize = 1; - info.glFormat = 0; - // FIXME: not sure this is always correct to use RGB here, does this work with HDR formats? - info.glBaseInternalFormat = Ktx1Bundle::RGB; - info.glInternalFormat = Ktx1Bundle::RGB; - } -#else - if (!g_compression.empty()) { - std::cerr << "Block compression is not supported in this build." << std::endl; - exit(1); - } -#endif - const uint32_t dim = (const uint32_t) cm.getDimensions(); for (uint32_t j = 0; j < 6; j++) { KtxBlobIndex blobIndex {(uint32_t) miplevel, 0, j}; @@ -1309,16 +1273,6 @@ static void exportKtxFaces(Ktx1Bundle& container, uint32_t miplevel, const Cubem default: face = Cubemap::Face::PX; break; // make linters happy } LinearImage image = toLinearImage(cm.getImageForFace(face)); - -#ifdef IMAGEIO_SUPPORTS_BLOCK_COMPRESSION - if (compression.type != CompressionConfig::INVALID) { - CompressedTexture tex = compressTexture(compression, image); - container.setBlob(blobIndex, tex.data.get(), tex.size); - info.glInternalFormat = (uint32_t) tex.format; - continue; - } -#endif - auto uintData = fromLinearToRGB_10_11_11_REV(image); container.setBlob(blobIndex, uintData.get(), dim * dim * 4); } diff --git a/tools/mipgen/src/main.cpp b/tools/mipgen/src/main.cpp index e829ada83f..c7afabf6de 100644 --- a/tools/mipgen/src/main.cpp +++ b/tools/mipgen/src/main.cpp @@ -20,10 +20,7 @@ #include #include -#ifdef IMAGEIO_SUPPORTS_BLOCK_COMPRESSION -#include -#endif - +#include #include #include @@ -39,15 +36,25 @@ using namespace image; using namespace std; using namespace utils; +enum KtxCompression { + NONE, + UASTC, + ETC1S, + UASTC_NORMALS, + ETC1S_NORMALS, +}; + static ImageEncoder::Format g_format = ImageEncoder::Format::PNG; static bool g_formatSpecified = false; static bool g_createGallery = false; -static std::string g_compression = ""; +static KtxCompression g_ktxCompression = NONE; +static std::string g_compressionString; static Filter g_filter = Filter::DEFAULT; static bool g_addAlpha = false; static bool g_stripAlpha = false; static bool g_grayscale = false; -static bool g_ktxContainer = false; +static bool g_ktx1Container = false; +static bool g_ktx2Container = false; static bool g_sourceIsLinear = false; static bool g_quietMode = false; static uint32_t g_mipLevelCount = 0; @@ -78,11 +85,10 @@ Options: suppress console output from the mipgen tool --grayscale, -g create a single-channel image - --format=[exr|hdr|rgbm|psd|png|dds|ktx], -f [exr|hdr|rgbm|psd|png|dds|ktx] + --format=[exr|hdr|rgbm|psd|png|dds|ktx|ktx2], -f [extension] 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) - the "normals" filter may automatically change the compression scheme --add-alpha if the source image has 3 channels, this adds a fourth channel filled with 1.0 --strip-alpha @@ -92,30 +98,16 @@ Options: if 0 (default), all levels are generated --compression=COMPRESSION, -c COMPRESSION format specific compression: -)TXT" -#ifdef IMAGEIO_SUPPORTS_BLOCK_COMPRESSION -R"TXT( - KTX: - astc_[fast|thorough]_[ldr|hdr]_WxH, where WxH is a valid block size - s3tc_rgb_dxt1, s3tc_rgba_dxt5 - etc_FORMAT_METRIC_EFFORT - FORMAT is r11, signed_r11, rg11, signed_rg11, rgb8, srgb8, rgb8_alpha - srgb8_alpha, rgba8, or srgb8_alpha8 - METRIC is rgba, rgbx, rec709, numeric, or normalxyz - EFFORT is an integer between 0 and 100 -)TXT" -#endif -R"TXT( - PNG: Ignored - Radiance: Ignored + KTX, PNG, Radiance: Ignored + KTX2: uastc, etc1s, uastc_normals, or etc1s_normals Photoshop: 16 (default), 32 OpenEXR: RAW, RLE, ZIPS, ZIP, PIZ (default) DDS: 8, 16 (default), 32 Examples: MIPGEN -g --kernel=hermite grassland.png mip_%03d.png - MIPGEN -f ktx --compression=astc_fast_ldr_4x4 grassland.png mips.ktx - MIPGEN -f ktx --compression=etc_rgb_rgba_40 grassland.png mips.ktx + MIPGEN -f ktx2 --compression=uastc grassland.png mips.ktx + MIPGEN -f ktx grassland.png mips.ktx )TXT"; static const char* HTML_PREFIX = R"HTML( @@ -240,12 +232,25 @@ static int handleArguments(int argc, char* argv[]) { g_formatSpecified = true; } if (arg == "ktx") { - g_ktxContainer = true; + g_ktx1Container = true; + g_formatSpecified = true; + } + if (arg == "ktx2") { + g_ktx2Container = true; g_formatSpecified = true; } break; case 'c': - g_compression = arg; + if (arg == "uastc") { + g_ktxCompression = UASTC; + } else if (arg == "etc1s") { + g_ktxCompression = ETC1S; + } else if (arg == "uastc_normals") { + g_ktxCompression = UASTC_NORMALS; + } else if (arg == "etc1s_normals") { + g_ktxCompression = ETC1S_NORMALS; + } + g_compressionString = arg; break; case 'm': try { @@ -270,7 +275,10 @@ int main(int argc, char* argv[]) { Path inputPath(argv[optionIndex++]); std::string outputPattern(argv[optionIndex]); if (Path(outputPattern).getExtension() == "ktx") { - g_ktxContainer = true; + g_ktx1Container = true; + g_formatSpecified = true; + } else if (Path(outputPattern).getExtension() == "ktx2") { + g_ktx2Container = true; g_formatSpecified = true; } else if (!g_formatSpecified) { g_format = ImageEncoder::chooseFormat(outputPattern, g_sourceIsLinear); @@ -318,7 +326,7 @@ int main(int argc, char* argv[]) { vector miplevels(count); generateMipmaps(sourceImage, g_filter, miplevels.data(), count); - if (g_ktxContainer) { + if (g_ktx1Container) { if (!g_quietMode) { puts("Writing KTX file to disk..."); } @@ -357,47 +365,16 @@ int main(int argc, char* argv[]) { cerr << "Bad component count." << endl; return 1; } -#ifdef IMAGEIO_SUPPORTS_BLOCK_COMPRESSION - CompressionConfig config {}; - if (!g_compression.empty()) { - bool valid = parseOptionString(g_compression, &config); - if (!valid) { - cerr << "Unrecognized compression: " << g_compression << endl; - return 1; - } - // The KTX spec says the following for compressed textures: glTypeSize should 1, - // glFormat should be 0, and glBaseInternalFormat should be RED, RG, RGB, or RGBA. - // The glInternalFormat field is the only field that specifies the actual format. - info.glFormat = 0; - destIsLinear = config.isLinear(); - } -#else - if (!g_compression.empty()) { - cerr << "Compression not supported in this build." << endl; + if (g_ktxCompression != NONE) { + cerr << "Compression not supported with KTX1." << endl; return 1; } -#endif uint32_t mip = 0; auto addLevel = [&](LinearImage image) { if (g_filter == Filter::GAUSSIAN_NORMALS) { image = vectorsToColors(image); } std::unique_ptr data; -#ifdef IMAGEIO_SUPPORTS_BLOCK_COMPRESSION - if (config.type != CompressionConfig::INVALID) { - // Some encoders call exit(1) upon failure, so it's very useful to print some - // source image information here for when this is invoked from a build script. - // Note that some encoders also have limitations in terms of image size. - if (!g_quietMode) { - printf("Starting compression for %s (%dx%d)\n", inputPath.getName().c_str(), - image.getWidth(), image.getHeight()); - } - CompressedTexture tex = compressTexture(config, image); - container.setBlob({mip++}, tex.data.get(), tex.size); - info.glInternalFormat = (uint32_t) tex.format; - return; - } -#endif if (g_grayscale && destIsLinear) { data = fromLinearToGrayscale(image); } else if (g_grayscale) { @@ -434,6 +411,51 @@ int main(int argc, char* argv[]) { return 0; } + if (g_ktx2Container) { + if (!g_quietMode) { + puts("Writing KTX2 file to disk..."); + } + + BasisEncoder::Builder builder(miplevels.size() + 1, 1); + using IntermediateFormat = BasisEncoder::IntermediateFormat; + + size_t mipIndex = 0; + builder + .intermediateFormat((g_ktxCompression == UASTC || g_ktxCompression == UASTC_NORMALS) ? + IntermediateFormat::UASTC : IntermediateFormat::ETC1S) + .grayscale(g_grayscale) + .linear(g_sourceIsLinear) + .quiet(g_quietMode) + .normals(g_ktxCompression == ETC1S_NORMALS || g_ktxCompression == UASTC_NORMALS) + .miplevel(mipIndex++, 0, sourceImage); + + for (auto image : miplevels) { + builder.miplevel(mipIndex++, 0, image); + } + + BasisEncoder* encoder = builder.build(); + if (!encoder) { + puts("Error while creating BasisU encoder."); + return 1; + } + + bool success = encoder->encode(); + if (!success) { + // Error message has already been printed. + return 1; + } + + Path(outputPattern).getParent().mkdirRecursive(); + ofstream outputStream(outputPattern, ios::out | ios::binary); + outputStream.write((const char*) encoder->getKtx2Data(), encoder->getKtx2ByteCount()); + outputStream.close(); + if (!g_quietMode) { + printf("Wrote %zu bytes to %s.\n", encoder->getKtx2ByteCount(), outputPattern.c_str()); + } + delete encoder; + return 0; + } + if (!g_quietMode) { puts("Writing image files to disk..."); } @@ -454,7 +476,7 @@ int main(int argc, char* argv[]) { if (g_filter == Filter::GAUSSIAN_NORMALS) { image = vectorsToColors(image); } - if (!ImageEncoder::encode(outputStream, g_format, image, g_compression, path)) { + if (!ImageEncoder::encode(outputStream, g_format, image, g_compressionString, path)) { cerr << "An error occurred while encoding the image." << endl; return 1; } diff --git a/web/filament-js/extensions.js b/web/filament-js/extensions.js index f4baec28fc..519095e4fa 100644 --- a/web/filament-js/extensions.js +++ b/web/filament-js/extensions.js @@ -115,36 +115,64 @@ Filament.loadClassExtensions = function() { return result; }; - /// createTextureFromKtx ::method:: Utility function that creates a [Texture] from a KTX file. - /// buffer ::argument:: asset string, or Uint8Array, or [Buffer] with KTX file contents + /// createTextureFromKtx1 ::method:: Utility function that creates a [Texture] from a KTX1 file. + /// buffer ::argument:: asset string, or Uint8Array, or [Buffer] with KTX1 file contents /// options ::argument:: Options dictionary. /// ::retval:: [Texture] - Filament.Engine.prototype.createTextureFromKtx = function(buffer, options) { + Filament.Engine.prototype.createTextureFromKtx1 = function(buffer, options) { buffer = getBufferDescriptor(buffer); - const result = Filament._createTextureFromKtx(buffer, this, options); + const result = Filament._createTextureFromKtx1(buffer, this, options); buffer.delete(); return result; }; - /// createIblFromKtx ::method:: Utility that creates an [IndirectLight] from a KTX file. + /// createTextureFromKtx2 ::method:: Utility function that creates a [Texture] from a KTX2 file. + /// buffer ::argument:: asset string, or Uint8Array, or [Buffer] with KTX2 file contents + /// options ::argument:: Options dictionary. + /// ::retval:: [Texture] + Filament.Engine.prototype.createTextureFromKtx2 = function(buffer, options) { + options = options || {}; + buffer = getBufferDescriptor(buffer); + + const engine = this; + const quiet = false; + const reader = new Filament.Ktx2Reader(engine, quiet); + + reader.requestFormat(Filament.Texture$InternalFormat.RGBA8); + reader.requestFormat(Filament.Texture$InternalFormat.SRGB8_A8); + + const formats = options.formats || []; + for (const format of formats) { + reader.requestFormat(format); + } + + result = reader.load(buffer, options.srgb ? Filament.TransferFunction.sRGB : + Filament.TransferFunction.LINEAR); + + reader.delete(); + buffer.delete(); + return result; + }; + + /// createIblFromKtx1 ::method:: Utility that creates an [IndirectLight] from a KTX file. /// NOTE: To prevent a leak, please be sure to destroy the associated reflections texture. /// buffer ::argument:: asset string, or Uint8Array, or [Buffer] with KTX file contents /// options ::argument:: Options dictionary. /// ::retval:: [IndirectLight] - Filament.Engine.prototype.createIblFromKtx = function(buffer, options) { + Filament.Engine.prototype.createIblFromKtx1 = function(buffer, options) { buffer = getBufferDescriptor(buffer); - const result = Filament._createIblFromKtx(buffer, this, options); + const result = Filament._createIblFromKtx1(buffer, this, options); buffer.delete(); return result; }; - /// createSkyFromKtx ::method:: Utility function that creates a [Skybox] from a KTX file. + /// createSkyFromKtx1 ::method:: Utility function that creates a [Skybox] from a KTX file. /// NOTE: To prevent a leak, please be sure to destroy the associated texture. /// buffer ::argument:: asset string, or Uint8Array, or [Buffer] with KTX file contents /// options ::argument:: Options dictionary. /// ::retval:: [Skybox] - Filament.Engine.prototype.createSkyFromKtx = function(buffer, options) { - const skytex = this.createTextureFromKtx(buffer, options); + Filament.Engine.prototype.createSkyFromKtx1 = function(buffer, options) { + const skytex = this.createTextureFromKtx1(buffer, options); return Filament.Skybox.Builder().environment(skytex).build(this); }; diff --git a/web/filament-js/filament-viewer.js b/web/filament-js/filament-viewer.js index 24039b192d..2ac3043210 100644 --- a/web/filament-js/filament-viewer.js +++ b/web/filament-js/filament-viewer.js @@ -246,7 +246,7 @@ class FilamentViewer extends LitElement { return response.arrayBuffer(); }).then(arrayBuffer => { const ktxData = new Uint8Array(arrayBuffer); - this.indirectLight = this.engine.createIblFromKtx(ktxData); + this.indirectLight = this.engine.createIblFromKtx1(ktxData); this.indirectLight.setIntensity(this.intensity); this.scene.setIndirectLight(this.indirectLight); }); @@ -264,7 +264,7 @@ class FilamentViewer extends LitElement { return response.arrayBuffer(); }).then(arrayBuffer => { const ktxData = new Uint8Array(arrayBuffer); - this.skybox = this.engine.createSkyFromKtx(ktxData); + this.skybox = this.engine.createSkyFromKtx1(ktxData); this.scene.setSkybox(this.skybox); }); } diff --git a/web/filament-js/filament.d.ts b/web/filament-js/filament.d.ts index 865788ddb7..69b14cad7d 100644 --- a/web/filament-js/filament.d.ts +++ b/web/filament-js/filament.d.ts @@ -563,15 +563,18 @@ export class Engine { public static create(canvas: HTMLCanvasElement, contextOptions?: object): Engine; public execute(): void; public createCamera(entity: Entity): Camera; - public createIblFromKtx(urlOrBuffer: BufferReference): IndirectLight; public createMaterial(urlOrBuffer: BufferReference): Material; public createRenderer(): Renderer; public createScene(): Scene; - public createSkyFromKtx(urlOrBuffer: BufferReference): Skybox; public createSwapChain(): SwapChain; public createTextureFromJpeg(urlOrBuffer: BufferReference, options?: object): Texture; public createTextureFromPng(urlOrBuffer: BufferReference, options?: object): Texture; - public createTextureFromKtx(urlOrBuffer: BufferReference, options?: object): Texture; + + public createIblFromKtx1(urlOrBuffer: BufferReference): IndirectLight; + public createSkyFromKtx1(urlOrBuffer: BufferReference): Skybox; + public createTextureFromKtx1(urlOrBuffer: BufferReference, options?: object): Texture; + public createTextureFromKtx2(urlOrBuffer: BufferReference, options?: object): Texture; + public createView(): View; public createAssetLoader(): gltfio$AssetLoader; @@ -601,6 +604,13 @@ export class Engine { public loadFilamesh(urlOrBuffer: BufferReference, definstance?: MaterialInstance, matinstances?: object): Filamesh; } +export class Ktx2Reader { + constructor(engine: Engine, quiet: boolean) + public requestFormat(format: Texture$InternalFormat): void; + public unrequestFormat(format: Texture$InternalFormat): void; + public load(urlOrBuffer: BufferReference, transfer: TransferFunction): Texture|null; +} + export class gltfio$AssetLoader { public createAssetFromJson(urlOrBuffer: BufferReference): gltfio$FilamentAsset; public createAssetFromBinary(urlOrBuffer: BufferReference): gltfio$FilamentAsset; @@ -718,6 +728,10 @@ export enum CompressedPixelDataType { DXT1_RGBA, DXT3_RGBA, DXT5_RGBA, + DXT1_SRGB, + DXT1_SRGBA, + DXT3_SRGBA, + DXT5_SRGBA, RGBA_ASTC_4x4, RGBA_ASTC_5x4, RGBA_ASTC_5x5, @@ -921,6 +935,10 @@ export enum Texture$InternalFormat { DXT1_RGBA, DXT3_RGBA, DXT5_RGBA, + DXT1_SRGB, + DXT1_SRGBA, + DXT3_SRGBA, + DXT5_SRGBA, RGBA_ASTC_4x4, RGBA_ASTC_5x4, RGBA_ASTC_5x5, diff --git a/web/filament-js/jsbindings.cpp b/web/filament-js/jsbindings.cpp index ce3a952436..3272cdd551 100644 --- a/web/filament-js/jsbindings.cpp +++ b/web/filament-js/jsbindings.cpp @@ -69,6 +69,7 @@ #include #include +#include #include #include @@ -1616,12 +1617,21 @@ class_("Ktx1Bundle") return std::string(self->getMetadata(key.c_str())); }), allow_raw_pointers()); -function("ktx$createTexture", EMBIND_LAMBDA(Texture*, +function("ktx1reader$createTexture", EMBIND_LAMBDA(Texture*, (Engine* engine, const Ktx1Bundle& ktx, bool srgb), { return Ktx1Reader::createTexture(engine, ktx, srgb, nullptr, nullptr); }), allow_raw_pointers()); -/// KtxInfo ::class:: Property accessor for KTX header. +class_("Ktx2Reader") + .constructor() + .function("requestFormat", &Ktx2Reader::requestFormat) + .function("unrequestFormat", &Ktx2Reader::unrequestFormat) + .function("load", EMBIND_LAMBDA(Texture*, (Ktx2Reader* self, BufferDescriptor bd, + Ktx2Reader::TransferFunction transfer), { + return self->load((uint8_t*) bd.bd->buffer, (uint32_t) bd.bd->size, transfer); + }), allow_raw_pointers()); + +/// KtxInfo ::class:: Property accessor for KTX1 header. /// For example, `Ktx1Bundle.info().pixelWidth`. See the /// [KTX spec](https://www.khronos.org/opengles/sdk/tools/KTX/file_format_spec/) for the list of /// properties. diff --git a/web/filament-js/jsenums.cpp b/web/filament-js/jsenums.cpp index ed9d16fe55..bcc86a043f 100644 --- a/web/filament-js/jsenums.cpp +++ b/web/filament-js/jsenums.cpp @@ -28,6 +28,8 @@ #include #include +#include + #include #include @@ -251,6 +253,10 @@ enum_("Texture$InternalFormat") // aka backend::Texture .value("DXT1_RGBA", Texture::InternalFormat::DXT1_RGBA) .value("DXT3_RGBA", Texture::InternalFormat::DXT3_RGBA) .value("DXT5_RGBA", Texture::InternalFormat::DXT5_RGBA) + .value("DXT1_SRGB", Texture::InternalFormat::DXT1_SRGB) + .value("DXT1_SRGBA", Texture::InternalFormat::DXT1_SRGBA) + .value("DXT3_SRGBA", Texture::InternalFormat::DXT3_SRGBA) + .value("DXT5_SRGBA", Texture::InternalFormat::DXT5_SRGBA) .value("RGBA_ASTC_4x4", Texture::InternalFormat::RGBA_ASTC_4x4) .value("RGBA_ASTC_5x4", Texture::InternalFormat::RGBA_ASTC_5x4) .value("RGBA_ASTC_5x5", Texture::InternalFormat::RGBA_ASTC_5x5) @@ -345,6 +351,10 @@ enum_("CompressedPixelDataType") .value("DXT1_RGBA", backend::CompressedPixelDataType::DXT1_RGBA) .value("DXT3_RGBA", backend::CompressedPixelDataType::DXT3_RGBA) .value("DXT5_RGBA", backend::CompressedPixelDataType::DXT5_RGBA) + .value("DXT1_SRGB", backend::CompressedPixelDataType::DXT1_SRGB) + .value("DXT1_SRGBA", backend::CompressedPixelDataType::DXT1_SRGBA) + .value("DXT3_SRGBA", backend::CompressedPixelDataType::DXT3_SRGBA) + .value("DXT5_SRGBA", backend::CompressedPixelDataType::DXT5_SRGBA) .value("RGBA_ASTC_4x4", backend::CompressedPixelDataType::RGBA_ASTC_4x4) .value("RGBA_ASTC_5x4", backend::CompressedPixelDataType::RGBA_ASTC_5x4) .value("RGBA_ASTC_5x5", backend::CompressedPixelDataType::RGBA_ASTC_5x5) @@ -411,4 +421,8 @@ enum_("CullingMode") .value("BACK", backend::CullingMode::BACK) .value("FRONT_AND_BACK", backend::CullingMode::FRONT_AND_BACK); +enum_("TransferFunction") + .value("LINEAR", ktxreader::Ktx2Reader::TransferFunction::LINEAR) + .value("sRGB", ktxreader::Ktx2Reader::TransferFunction::sRGB); + } diff --git a/web/filament-js/utilities.js b/web/filament-js/utilities.js index 0c639b6429..af0f5a985a 100644 --- a/web/filament-js/utilities.js +++ b/web/filament-js/utilities.js @@ -240,14 +240,14 @@ Filament.loadMathExtensions = function() { // Texture helpers // --------------- -Filament._createTextureFromKtx = function(ktxdata, engine, options) { +Filament._createTextureFromKtx1 = function(ktxdata, engine, options) { options = options || {}; const ktx = options['ktx'] || new Filament.Ktx1Bundle(ktxdata); const srgb = !!options['srgb']; - return Filament.ktx$createTexture(engine, ktx, srgb); + return Filament.ktx1reader$createTexture(engine, ktx, srgb); }; -Filament._createIblFromKtx = function(ktxdata, engine, options) { +Filament._createIblFromKtx1 = function(ktxdata, engine, options) { options = options || {}; const iblktx = options['ktx'] = new Filament.Ktx1Bundle(ktxdata); @@ -258,7 +258,7 @@ Filament._createIblFromKtx = function(ktxdata, engine, options) { ' which is not an expected floating-point format. Please use cmgen to generate IBL.'); } - const ibltex = Filament._createTextureFromKtx(ktxdata, engine, options); + const ibltex = Filament._createTextureFromKtx1(ktxdata, engine, options); const shstring = iblktx.getMetadata("sh"); const ibl = Filament.IndirectLight.Builder() .reflections(ibltex) diff --git a/web/samples/CMakeLists.txt b/web/samples/CMakeLists.txt index 1f4773ced2..91c194365e 100644 --- a/web/samples/CMakeLists.txt +++ b/web/samples/CMakeLists.txt @@ -71,22 +71,11 @@ function(add_rawfile SOURCE TARGET) MAIN_DEPENDENCY ${source_path}) endfunction() -set(ETC_R11_ARGS "--grayscale;--compression=etc_r11_numeric_40") - -# TODO: Instead of "rgb8" we should be using "rg11", but that causes an assertion in etc2comp. -set(ETC_NORMALS_ARGS "--kernel=NORMALS;--linear;--compression=etc_rgb8_normalxyz_40") - -add_ktxfiles("assets/models/monkey/albedo.png" "albedo.ktx" "") -add_ktxfiles("assets/models/monkey/albedo.png" "albedo_astc.ktx" "--compression=astc_fast_ldr_4x4") -add_ktxfiles("assets/models/monkey/albedo.png" "albedo_s3tc_srgb.ktx" "--compression=s3tc_rgb_dxt1") -add_ktxfiles("assets/models/monkey/normal.png" "normal.ktx" "--kernel=NORMALS;--linear") -add_ktxfiles("assets/models/monkey/normal.png" "normal_etc.ktx" "${ETC_NORMALS_ARGS}") -add_ktxfiles("assets/models/monkey/roughness.png" "roughness.ktx" "--grayscale") -add_ktxfiles("assets/models/monkey/roughness.png" "roughness_etc.ktx" "${ETC_R11_ARGS}") -add_ktxfiles("assets/models/monkey/metallic.png" "metallic.ktx" "--grayscale") -add_ktxfiles("assets/models/monkey/metallic.png" "metallic_etc.ktx" "${ETC_R11_ARGS}") -add_ktxfiles("assets/models/monkey/ao.png" "ao.ktx" "--grayscale") -add_ktxfiles("assets/models/monkey/ao.png" "ao_etc.ktx" "${ETC_R11_ARGS}") +add_ktxfiles("assets/models/monkey/albedo.png" "albedo.ktx2" "--compression=uastc") +add_ktxfiles("assets/models/monkey/normal.png" "normal.ktx2" "--compression=uastc_normals;--kernel=NORMALS;--linear") +add_ktxfiles("assets/models/monkey/roughness.png" "roughness.ktx2" "--compression=uastc;--grayscale;--linear") +add_ktxfiles("assets/models/monkey/metallic.png" "metallic.ktx2" "--compression=uastc;--grayscale;--linear") +add_ktxfiles("assets/models/monkey/ao.png" "ao.ktx2" "--compression=uastc;--grayscale;--linear") add_rawfile("third_party/models/FlightHelmet/FlightHelmet_baseColor.png" "FlightHelmet_baseColor.png") add_rawfile("third_party/models/FlightHelmet/FlightHelmet_baseColor1.png" "FlightHelmet_baseColor1.png") diff --git a/web/samples/helmet.html b/web/samples/helmet.html index 87189df41e..ed0d039eab 100644 --- a/web/samples/helmet.html +++ b/web/samples/helmet.html @@ -43,7 +43,7 @@ class App { const scene = this.scene = engine.createScene(); this.trackball = new Trackball(canvas, {startSpin: 0.035}); - const indirectLight = this.ibl = engine.createIblFromKtx(ibl_url); + const indirectLight = this.ibl = engine.createIblFromKtx1(ibl_url); this.scene.setIndirectLight(indirectLight); const iblDirection = IndirectLight.getDirectionEstimate(indirectLight.shfloats); @@ -58,7 +58,7 @@ class App { mat3.fromRotation(mat, radians, [0, 1, 0]); indirectLight.setRotation(mat); - const skybox = engine.createSkyFromKtx(sky_url); + const skybox = engine.createSkyFromKtx1(sky_url); this.scene.setSkybox(skybox); const sunlight = Filament.EntityManager.get().create(); diff --git a/web/samples/parquet.html b/web/samples/parquet.html index 3be2491dfc..585cba2f79 100644 --- a/web/samples/parquet.html +++ b/web/samples/parquet.html @@ -54,14 +54,14 @@ class App { .build(engine, sunlight); this.scene.addEntity(sunlight); - const indirectLight = this.ibl = engine.createIblFromKtx(iblfile); + const indirectLight = this.ibl = engine.createIblFromKtx1(iblfile); this.scene.setIndirectLight(indirectLight); const radians = 1.0; indirectLight.setRotation(mat3.fromRotation(mat3.create(), radians, [0, 1, 0])) indirectLight.setIntensity(10000); - const skybox = engine.createSkyFromKtx(skyfile); + const skybox = engine.createSkyFromKtx1(skyfile); this.scene.setSkybox(skybox); const material = engine.createMaterial('parquet.filamat'); diff --git a/web/samples/suzanne.html b/web/samples/suzanne.html index 4d6491c884..f9b0b25168 100644 --- a/web/samples/suzanne.html +++ b/web/samples/suzanne.html @@ -16,17 +16,14 @@ canvas { touch-action: none; width: 100%; height: 100%; }