From e32cf375b8e6c581c544f6b2cb6f61c80c1504b4 Mon Sep 17 00:00:00 2001 From: Arseny Kapoulkine Date: Wed, 22 May 2024 09:18:39 -0700 Subject: [PATCH 1/6] gltfpack: Implement initial support for KHR_texture_basisu inputs When the input file has KHR_texture_basisu extension, we now will preserve the images along with the extension in the output file instead of discarding it. --- gltf/gltfpack.cpp | 4 +++- gltf/material.cpp | 13 ++++++++++--- gltf/parsegltf.cpp | 2 -- gltf/write.cpp | 22 +++++++++++++++------- 4 files changed, 28 insertions(+), 13 deletions(-) diff --git a/gltf/gltfpack.cpp b/gltf/gltfpack.cpp index 7918326d..2db6eff7 100644 --- a/gltf/gltfpack.cpp +++ b/gltf/gltfpack.cpp @@ -452,6 +452,7 @@ static void process(cgltf_data* data, const char* input_path, const char* output bool ext_unlit = false; bool ext_instancing = false; bool ext_texture_transform = false; + bool ext_texture_basisu = false; size_t accr_offset = 0; size_t node_offset = 0; @@ -509,6 +510,7 @@ static void process(cgltf_data* data, const char* input_path, const char* output assert(textures[i].remap == int(texture_offset)); texture_offset++; + ext_texture_basisu = ext_texture_basisu || texture.has_basisu; } for (size_t i = 0; i < data->materials_count; ++i) @@ -834,7 +836,7 @@ static void process(cgltf_data* data, const char* input_path, const char* output {"KHR_materials_unlit", ext_unlit, false}, {"KHR_materials_variants", data->variants_count > 0, false}, {"KHR_lights_punctual", data->lights_count > 0, false}, - {"KHR_texture_basisu", !json_textures.empty() && settings.texture_ktx2, true}, + {"KHR_texture_basisu", (!json_textures.empty() && settings.texture_ktx2) || ext_texture_basisu, true}, {"EXT_mesh_gpu_instancing", ext_instancing, true}, }; diff --git a/gltf/material.cpp b/gltf/material.cpp index 0ae2abf9..83c83a97 100644 --- a/gltf/material.cpp +++ b/gltf/material.cpp @@ -435,6 +435,11 @@ bool hasValidTransform(const cgltf_texture_view& view) return false; } +static const cgltf_image* getTextureImage(const cgltf_texture* texture) +{ + return texture ? (texture->has_basisu ? texture->basisu_image : texture->image) : NULL; +} + static void analyzeMaterialTexture(const cgltf_texture_view& view, TextureKind kind, MaterialInfo& mi, cgltf_data* data, std::vector& textures, std::vector& images) { mi.usesTextureTransform |= hasValidTransform(view); @@ -442,9 +447,9 @@ static void analyzeMaterialTexture(const cgltf_texture_view& view, TextureKind k if (view.texture) textures[view.texture - data->textures].keep = true; - if (view.texture && view.texture->image) + if (const cgltf_image* image = getTextureImage(view.texture)) { - ImageInfo& info = images[view.texture->image - data->images]; + ImageInfo& info = images[image - data->images]; mi.textureSetMask |= 1u << view.texcoord; mi.needsTangents |= (kind == TextureKind_Normal); @@ -549,7 +554,9 @@ static bool shouldKeepAlpha(const cgltf_texture_view& color, float alpha, cgltf_ if (alpha != 1.f) return true; - return color.texture && color.texture->image && getChannels(*color.texture->image, images[color.texture->image - data->images], input_path) == 4; + const cgltf_image* image = getTextureImage(color.texture); + + return image && getChannels(*image, images[image - data->images], input_path) == 4; } void optimizeMaterials(cgltf_data* data, const char* input_path, std::vector& images) diff --git a/gltf/parsegltf.cpp b/gltf/parsegltf.cpp index 9b22dd43..91f0c020 100644 --- a/gltf/parsegltf.cpp +++ b/gltf/parsegltf.cpp @@ -530,8 +530,6 @@ static cgltf_data* parseGltf(cgltf_data* data, cgltf_result result, std::vector< *error = getError(result, data); else if (requiresExtension(data, "KHR_draco_mesh_compression")) *error = "file requires Draco mesh compression support"; - else if (requiresExtension(data, "KHR_texture_basisu")) - *error = "file requires BasisU texture support"; else if (requiresExtension(data, "EXT_mesh_gpu_instancing")) *error = "file requires mesh instancing support"; else if (needsDummyBuffers(data)) diff --git a/gltf/write.cpp b/gltf/write.cpp index eb45a11b..e3b7e7d9 100644 --- a/gltf/write.cpp +++ b/gltf/write.cpp @@ -952,27 +952,35 @@ void writeImage(std::string& json, std::vector& views, const cgltf_i void writeTexture(std::string& json, const cgltf_texture& texture, const ImageInfo* info, cgltf_data* data, const Settings& settings) { + if (texture.sampler) + { + append(json, "\"sampler\":"); + append(json, size_t(texture.sampler - data->samplers)); + } + if (texture.image) { - if (texture.sampler) - { - append(json, "\"sampler\":"); - append(json, size_t(texture.sampler - data->samplers)); - append(json, ","); - } - if (info && settings.texture_mode[info->kind] != TextureMode_Raw) { + comma(json); append(json, "\"extensions\":{\"KHR_texture_basisu\":{\"source\":"); append(json, size_t(texture.image - data->images)); append(json, "}}"); } else { + comma(json); append(json, "\"source\":"); append(json, size_t(texture.image - data->images)); } } + else if (texture.has_basisu) + { + comma(json); + append(json, "\"extensions\":{\"KHR_texture_basisu\":{\"source\":"); + append(json, size_t(texture.basisu_image - data->images)); + append(json, "}}"); + } } void writeMeshAttributes(std::string& json, std::vector& views, std::string& json_accessors, size_t& accr_offset, const Mesh& mesh, int target, const QuantizationPosition& qp, const QuantizationTexture& qt, const Settings& settings) From dd7ac4562e7216d036b7cce2014362afecfc6692 Mon Sep 17 00:00:00 2001 From: Arseny Kapoulkine Date: Wed, 22 May 2024 09:27:50 -0700 Subject: [PATCH 2/6] gltfpack: Implement transparency analysis for KTX2 images Without this, if the source is a KTX2 image, the material will get its alpha mode removed unconditionally. --- gltf/image.cpp | 63 +++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 55 insertions(+), 8 deletions(-) diff --git a/gltf/image.cpp b/gltf/image.cpp index e45e223a..129938a0 100644 --- a/gltf/image.cpp +++ b/gltf/image.cpp @@ -107,12 +107,12 @@ bool readImage(const cgltf_image& image, const char* input_path, std::string& da } } -static int readInt16(const std::string& data, size_t offset) +static int readInt16BE(const std::string& data, size_t offset) { return (unsigned char)data[offset] * 256 + (unsigned char)data[offset + 1]; } -static int readInt32(const std::string& data, size_t offset) +static int readInt32BE(const std::string& data, size_t offset) { return (unsigned((unsigned char)data[offset]) << 24) | (unsigned((unsigned char)data[offset + 1]) << 16) | @@ -120,6 +120,14 @@ static int readInt32(const std::string& data, size_t offset) unsigned((unsigned char)data[offset + 3]); } +static int readInt32LE(const std::string& data, size_t offset) +{ + return unsigned((unsigned char)data[offset]) | + (unsigned((unsigned char)data[offset + 1]) << 8) | + (unsigned((unsigned char)data[offset + 2]) << 16) | + (unsigned((unsigned char)data[offset + 3]) << 24); +} + static bool getDimensionsPng(const std::string& data, int& width, int& height) { if (data.size() < 8 + 8 + 13 + 4) @@ -132,8 +140,8 @@ static bool getDimensionsPng(const std::string& data, int& width, int& height) if (data.compare(12, 4, "IHDR") != 0) return false; - width = readInt32(data, 16); - height = readInt32(data, 20); + width = readInt32BE(data, 16); + height = readInt32BE(data, 20); return true; } @@ -169,13 +177,13 @@ static bool getDimensionsJpeg(const std::string& data, int& width, int& height) if (offset + 10 > data.size()) return false; - width = readInt16(data, offset + 7); - height = readInt16(data, offset + 5); + width = readInt16BE(data, offset + 7); + height = readInt16BE(data, offset + 5); return true; } - offset += 2 + readInt16(data, offset + 2); + offset += 2 + readInt16BE(data, offset + 2); } return false; @@ -202,7 +210,7 @@ static bool hasTransparencyPng(const std::string& data) while (offset + 12 <= data.size()) { - int length = readInt32(data, offset); + int length = readInt32BE(data, offset); if (length < 0) return false; @@ -216,10 +224,49 @@ static bool hasTransparencyPng(const std::string& data) return false; } +static bool hasTransparencyKtx2(const std::string& data) +{ + if (data.size() < 12 + 17 * 4) + return false; + + const char* signature = "\xabKTX 20\xbb\r\n\x1a\n"; + if (data.compare(0, 12, signature) != 0) + return false; + + int dfdOffset = readInt32LE(data, 48); + int dfdLength = readInt32LE(data, 52); + + if (dfdLength < 4 + 24 + 16 || unsigned(dfdOffset) > data.size() || unsigned(dfdLength) > data.size() - dfdOffset) + return false; + + const int KDF_DF_MODEL_ETC1S = 163; + const int KDF_DF_MODEL_UASTC = 166; + const int KHR_DF_CHANNEL_UASTC_RGBA = 3; + const int KHR_DF_CHANNEL_ETC1S_RGB = 0; + const int KHR_DF_CHANNEL_ETC1S_AAA = 15; + + int colorModel = readInt32LE(data, dfdOffset + 12) & 0xff; + int channelType1 = (readInt32LE(data, dfdOffset + 28) >> 24) & 0xf; + int channelType2 = dfdLength >= 4 + 24 + 16 * 2 ? (readInt32LE(data, dfdOffset + 44) >> 24) & 0xf : channelType1; + + if (colorModel == KDF_DF_MODEL_ETC1S) + { + return channelType1 == KHR_DF_CHANNEL_ETC1S_RGB && channelType2 == KHR_DF_CHANNEL_ETC1S_AAA; + } + else if (colorModel == KDF_DF_MODEL_UASTC) + { + return channelType1 == KHR_DF_CHANNEL_UASTC_RGBA; + } + + return false; +} + bool hasAlpha(const std::string& data, const char* mime_type) { if (strcmp(mime_type, "image/png") == 0) return hasTransparencyPng(data); + else if (strcmp(mime_type, "image/ktx2") == 0) + return hasTransparencyKtx2(data); else return false; } From 35d1dcd808921e40f3bbd7259b40ca6674f9c7e6 Mon Sep 17 00:00:00 2001 From: Arseny Kapoulkine Date: Wed, 22 May 2024 09:32:24 -0700 Subject: [PATCH 3/6] gltfpack: Fix texture merging when textures only use KTX2 images This is necessary for KTX2 inputs to be fully preserved correctly. --- gltf/material.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/gltf/material.cpp b/gltf/material.cpp index 83c83a97..bfe63b9c 100644 --- a/gltf/material.cpp +++ b/gltf/material.cpp @@ -11,6 +11,12 @@ static bool areTexturesEqual(const cgltf_texture& lhs, const cgltf_texture& rhs) if (lhs.sampler != rhs.sampler) return false; + if (lhs.has_basisu != rhs.has_basisu) + return false; + + if (lhs.basisu_image != rhs.basisu_image) + return false; + return true; } From 2a4590b176950fd42a02b551cc2bacd5ef569495 Mon Sep 17 00:00:00 2001 From: Arseny Kapoulkine Date: Wed, 22 May 2024 09:40:42 -0700 Subject: [PATCH 4/6] gltfpack: Skip recompression of KTX2 textures with -tc We now restrict -tc to only handle image/png and image/jpeg mime types, which exludes image/ktx2 and any other future formats that we might support. --- gltf/basisenc.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/gltf/basisenc.cpp b/gltf/basisenc.cpp index a5deef91..c1080949 100644 --- a/gltf/basisenc.cpp +++ b/gltf/basisenc.cpp @@ -98,11 +98,13 @@ static const char* prepareEncode(basisu::basis_compressor_params& params, const { std::string img_data; std::string mime_type; - std::string result; if (!readImage(image, input_path, img_data, mime_type)) return "error reading source file"; + if (mime_type != "image/png" && mime_type != "image/jpeg") + return NULL; + int width = 0, height = 0; if (!getDimensions(img_data, mime_type.c_str(), width, height)) return "error parsing image header"; From 3bd6e7c349d52c1a7fec91b862dda5dc949960db Mon Sep 17 00:00:00 2001 From: Arseny Kapoulkine Date: Wed, 22 May 2024 09:50:41 -0700 Subject: [PATCH 5/6] gltfpack: Update README.md We now support KHR_texture_basisu as an input. --- gltf/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/gltf/README.md b/gltf/README.md index 3e528ebc..a6be1640 100644 --- a/gltf/README.md +++ b/gltf/README.md @@ -81,6 +81,7 @@ gltfpack supports most Khronos extensions and some multi-vendor extensions in th - KHR_materials_variants - KHR_materials_volume - KHR_mesh_quantization +- KHR_texture_basisu - KHR_texture_transform - EXT_meshopt_compression From d3ce45721b28d8235d70447b9fd5a2fbb0080306 Mon Sep 17 00:00:00 2001 From: Arseny Kapoulkine Date: Wed, 22 May 2024 21:58:30 -0700 Subject: [PATCH 6/6] gltfpack: Adjust handling of textures with redundant images When a texture has both source and encoded image, we prefer to extract various metadata from the source image, but preserve both when writing the texture object. When -tc is used, we encode the source image and leave the pre-compressed image unused (this is a little redundant but requires unused image filtering to fix). --- gltf/material.cpp | 8 +++++++- gltf/write.cpp | 5 ++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/gltf/material.cpp b/gltf/material.cpp index bfe63b9c..8d017a18 100644 --- a/gltf/material.cpp +++ b/gltf/material.cpp @@ -443,7 +443,13 @@ bool hasValidTransform(const cgltf_texture_view& view) static const cgltf_image* getTextureImage(const cgltf_texture* texture) { - return texture ? (texture->has_basisu ? texture->basisu_image : texture->image) : NULL; + if (texture && texture->image) + return texture->image; + + if (texture && texture->has_basisu && texture->basisu_image) + return texture->basisu_image; + + return NULL; } static void analyzeMaterialTexture(const cgltf_texture_view& view, TextureKind kind, MaterialInfo& mi, cgltf_data* data, std::vector& textures, std::vector& images) diff --git a/gltf/write.cpp b/gltf/write.cpp index e3b7e7d9..abcae6d4 100644 --- a/gltf/write.cpp +++ b/gltf/write.cpp @@ -966,6 +966,8 @@ void writeTexture(std::string& json, const cgltf_texture& texture, const ImageIn append(json, "\"extensions\":{\"KHR_texture_basisu\":{\"source\":"); append(json, size_t(texture.image - data->images)); append(json, "}}"); + + return; // skip input basisu image if present, as we replace it with the one we encoded } else { @@ -974,7 +976,8 @@ void writeTexture(std::string& json, const cgltf_texture& texture, const ImageIn append(json, size_t(texture.image - data->images)); } } - else if (texture.has_basisu) + + if (texture.has_basisu && texture.basisu_image) { comma(json); append(json, "\"extensions\":{\"KHR_texture_basisu\":{\"source\":");