Repository: zeux/meshoptimizer (language: cpp)

Issue:
gltfpack removes existing KHR_textures_basisu extension
Running `gltfpack` on a file already using the `KHR_textures_basisu` extension removes all traces of the extension in the output file


Discussion / hints:
This is not a bug and rather a current limitation - you can pass `-tc` to recompress source textures if they are present. If `KHR_texture_basisu` is a required extension, gltfpack currently will refuse to process the file, because it assumes the role of a tool that converts a "vanilla" glTF file (that doesn't use various compression extensions) to an optimized one (that might).
For context, our use case was to use `gltf-transform` (or custom tool) for compression since it gave us a bit more flexibility there and `gltfpack` for geometry. 
Got it, thanks. Out of curiosity, what prevents you from using gltfpack to compress the textures as well?
`gltf-transform` offers more granularity and more options for texture processing; we want to be able to resize certain class of textures and also have a bit more control over compression parameters.

Fix patch:
diff --git a/gltf/README.md b/gltf/README.md
index 3e528ebc8..a6be16405 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
 
diff --git a/gltf/basisenc.cpp b/gltf/basisenc.cpp
index a5deef918..c10809497 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";
diff --git a/gltf/gltfpack.cpp b/gltf/gltfpack.cpp
index 7918326d2..2db6eff77 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/image.cpp b/gltf/image.cpp
index e45e223aa..129938a0e 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;
 }
diff --git a/gltf/material.cpp b/gltf/material.cpp
index 0ae2abf93..8d017a187 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;
 }
 
@@ -435,6 +441,17 @@ bool hasValidTransform(const cgltf_texture_view& view)
 	return false;
 }
 
+static const cgltf_image* getTextureImage(const cgltf_texture* texture)
+{
+	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<TextureInfo>& textures, std::vector<ImageInfo>& images)
 {
 	mi.usesTextureTransform |= hasValidTransform(view);
@@ -442,9 +459,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 +566,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<ImageInfo>& images)
diff --git a/gltf/parsegltf.cpp b/gltf/parsegltf.cpp
index 9b22dd43f..91f0c020f 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 eb45a11b0..abcae6d42 100644
--- a/gltf/write.cpp
+++ b/gltf/write.cpp
@@ -952,27 +952,38 @@ void writeImage(std::string& json, std::vector<BufferView>& views, const cgltf_i
 
 void writeTexture(std::string& json, const cgltf_texture& texture, const ImageInfo* info, cgltf_data* data, const Settings& settings)
 {
-	if (texture.image)
+	if (texture.sampler)
 	{
-		if (texture.sampler)
-		{
-			append(json, "\"sampler\":");
-			append(json, size_t(texture.sampler - data->samplers));
-			append(json, ",");
-		}
+		append(json, "\"sampler\":");
+		append(json, size_t(texture.sampler - data->samplers));
+	}
 
+	if (texture.image)
+	{
 		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, "}}");
+
+			return; // skip input basisu image if present, as we replace it with the one we encoded
 		}
 		else
 		{
+			comma(json);
 			append(json, "\"source\":");
 			append(json, size_t(texture.image - data->images));
 		}
 	}
+
+	if (texture.has_basisu && texture.basisu_image)
+	{
+		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<BufferView>& views, std::string& json_accessors, size_t& accr_offset, const Mesh& mesh, int target, const QuantizationPosition& qp, const QuantizationTexture& qt, const Settings& settings)


Test patch: