Browse Source

Merge pull request #76895 from aaronfranke/gltf-webp

Add support for extending GLTF with more texture formats and support WebP
Rémi Verschelde 2 years ago
parent
commit
cb711a9950

+ 21 - 0
modules/gltf/doc_classes/GLTFDocumentExtension.xml

@@ -103,6 +103,17 @@
 				The return value is used to determine if this [GLTFDocumentExtension] instance should be used for importing a given GLTF file. If [constant OK], the import will use this [GLTFDocumentExtension] instance. If not overridden, [constant OK] is returned.
 			</description>
 		</method>
+		<method name="_parse_image_data" qualifiers="virtual">
+			<return type="int" enum="Error" />
+			<param index="0" name="state" type="GLTFState" />
+			<param index="1" name="image_data" type="PackedByteArray" />
+			<param index="2" name="mime_type" type="String" />
+			<param index="3" name="ret_image" type="Image" />
+			<description>
+				Part of the import process. This method is run after [method _parse_node_extensions] and before [method _parse_texture_json].
+				Runs when parsing image data from a GLTF file. The data could be sourced from a separate file, a URI, or a buffer, and then is passed as a byte array.
+			</description>
+		</method>
 		<method name="_parse_node_extensions" qualifiers="virtual">
 			<return type="int" enum="Error" />
 			<param index="0" name="state" type="GLTFState" />
@@ -113,5 +124,15 @@
 				Runs when parsing the node extensions of a GLTFNode. This method can be used to process the extension JSON data into a format that can be used by [method _generate_scene_node]. The return value should be a member of the [enum Error] enum.
 			</description>
 		</method>
+		<method name="_parse_texture_json" qualifiers="virtual">
+			<return type="int" enum="Error" />
+			<param index="0" name="state" type="GLTFState" />
+			<param index="1" name="texture_json" type="Dictionary" />
+			<param index="2" name="ret_gltf_texture" type="GLTFTexture" />
+			<description>
+				Part of the import process. This method is run after [method _parse_image_data] and before [method _generate_scene_node].
+				Runs when parsing the texture JSON from the GLTF textures array. This can be used to set the source image index to use as the texture.
+			</description>
+		</method>
 	</methods>
 </class>

+ 2 - 0
modules/gltf/doc_classes/GLTFState.xml

@@ -70,6 +70,7 @@
 		<method name="get_images">
 			<return type="Texture2D[]" />
 			<description>
+				Gets the images of the GLTF file as an array of [Texture2D]s. These are the images that the [member GLTFTexture.src_image] index refers to.
 			</description>
 		</method>
 		<method name="get_lights">
@@ -182,6 +183,7 @@
 			<return type="void" />
 			<param index="0" name="images" type="Texture2D[]" />
 			<description>
+				Sets the images in the state stored as an array of [Texture2D]s. This can be used during export. These are the images that the [member GLTFTexture.src_image] index refers to.
 			</description>
 		</method>
 		<method name="set_lights">

+ 2 - 1
modules/gltf/doc_classes/GLTFTexture.xml

@@ -10,7 +10,8 @@
 		<member name="sampler" type="int" setter="set_sampler" getter="get_sampler" default="-1">
 			ID of the texture sampler to use when sampling the image. If -1, then the default texture sampler is used (linear filtering, and repeat wrapping in both axes).
 		</member>
-		<member name="src_image" type="int" setter="set_src_image" getter="get_src_image" default="0">
+		<member name="src_image" type="int" setter="set_src_image" getter="get_src_image" default="-1">
+			The index of the image associated with this texture, see [method GLTFState.get_images]. If -1, then this texture does not have an image assigned.
 		</member>
 	</members>
 </class>

+ 18 - 0
modules/gltf/extensions/gltf_document_extension.cpp

@@ -35,6 +35,8 @@ void GLTFDocumentExtension::_bind_methods() {
 	GDVIRTUAL_BIND(_import_preflight, "state", "extensions");
 	GDVIRTUAL_BIND(_get_supported_extensions);
 	GDVIRTUAL_BIND(_parse_node_extensions, "state", "gltf_node", "extensions");
+	GDVIRTUAL_BIND(_parse_image_data, "state", "image_data", "mime_type", "ret_image");
+	GDVIRTUAL_BIND(_parse_texture_json, "state", "texture_json", "ret_gltf_texture");
 	GDVIRTUAL_BIND(_generate_scene_node, "state", "gltf_node", "scene_parent");
 	GDVIRTUAL_BIND(_import_post_parse, "state");
 	GDVIRTUAL_BIND(_import_node, "state", "gltf_node", "json", "node");
@@ -68,6 +70,22 @@ Error GLTFDocumentExtension::parse_node_extensions(Ref<GLTFState> p_state, Ref<G
 	return err;
 }
 
+Error GLTFDocumentExtension::parse_image_data(Ref<GLTFState> p_state, const PackedByteArray &p_image_data, const String &p_mime_type, Ref<Image> r_image) {
+	ERR_FAIL_NULL_V(p_state, ERR_INVALID_PARAMETER);
+	ERR_FAIL_NULL_V(r_image, ERR_INVALID_PARAMETER);
+	Error err = OK;
+	GDVIRTUAL_CALL(_parse_image_data, p_state, p_image_data, p_mime_type, r_image, err);
+	return err;
+}
+
+Error GLTFDocumentExtension::parse_texture_json(Ref<GLTFState> p_state, const Dictionary &p_texture_json, Ref<GLTFTexture> r_gltf_texture) {
+	ERR_FAIL_NULL_V(p_state, ERR_INVALID_PARAMETER);
+	ERR_FAIL_NULL_V(r_gltf_texture, ERR_INVALID_PARAMETER);
+	Error err = OK;
+	GDVIRTUAL_CALL(_parse_texture_json, p_state, p_texture_json, r_gltf_texture, err);
+	return err;
+}
+
 Node3D *GLTFDocumentExtension::generate_scene_node(Ref<GLTFState> p_state, Ref<GLTFNode> p_gltf_node, Node *p_scene_parent) {
 	ERR_FAIL_NULL_V(p_state, nullptr);
 	ERR_FAIL_NULL_V(p_gltf_node, nullptr);

+ 4 - 0
modules/gltf/extensions/gltf_document_extension.h

@@ -44,6 +44,8 @@ public:
 	virtual Error import_preflight(Ref<GLTFState> p_state, Vector<String> p_extensions);
 	virtual Vector<String> get_supported_extensions();
 	virtual Error parse_node_extensions(Ref<GLTFState> p_state, Ref<GLTFNode> p_gltf_node, Dictionary &p_extensions);
+	virtual Error parse_image_data(Ref<GLTFState> p_state, const PackedByteArray &p_image_data, const String &p_mime_type, Ref<Image> r_image);
+	virtual Error parse_texture_json(Ref<GLTFState> p_state, const Dictionary &p_texture_json, Ref<GLTFTexture> r_gltf_texture);
 	virtual Node3D *generate_scene_node(Ref<GLTFState> p_state, Ref<GLTFNode> p_gltf_node, Node *p_scene_parent);
 	virtual Error import_post_parse(Ref<GLTFState> p_state);
 	virtual Error import_node(Ref<GLTFState> p_state, Ref<GLTFNode> p_gltf_node, Dictionary &r_json, Node *p_node);
@@ -58,6 +60,8 @@ public:
 	GDVIRTUAL2R(Error, _import_preflight, Ref<GLTFState>, Vector<String>);
 	GDVIRTUAL0R(Vector<String>, _get_supported_extensions);
 	GDVIRTUAL3R(Error, _parse_node_extensions, Ref<GLTFState>, Ref<GLTFNode>, Dictionary);
+	GDVIRTUAL4R(Error, _parse_image_data, Ref<GLTFState>, PackedByteArray, String, Ref<Image>);
+	GDVIRTUAL3R(Error, _parse_texture_json, Ref<GLTFState>, Dictionary, Ref<GLTFTexture>);
 	GDVIRTUAL3R(Node3D *, _generate_scene_node, Ref<GLTFState>, Ref<GLTFNode>, Node *);
 	GDVIRTUAL1R(Error, _import_post_parse, Ref<GLTFState>);
 	GDVIRTUAL4R(Error, _import_node, Ref<GLTFState>, Ref<GLTFNode>, Dictionary, Node *);

+ 68 - 0
modules/gltf/extensions/gltf_document_extension_texture_webp.cpp

@@ -0,0 +1,68 @@
+/**************************************************************************/
+/*  gltf_document_extension_texture_webp.cpp                              */
+/**************************************************************************/
+/*                         This file is part of:                          */
+/*                             GODOT ENGINE                               */
+/*                        https://godotengine.org                         */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur.                  */
+/*                                                                        */
+/* Permission is hereby granted, free of charge, to any person obtaining  */
+/* a copy of this software and associated documentation files (the        */
+/* "Software"), to deal in the Software without restriction, including    */
+/* without limitation the rights to use, copy, modify, merge, publish,    */
+/* distribute, sublicense, and/or sell copies of the Software, and to     */
+/* permit persons to whom the Software is furnished to do so, subject to  */
+/* the following conditions:                                              */
+/*                                                                        */
+/* The above copyright notice and this permission notice shall be         */
+/* included in all copies or substantial portions of the Software.        */
+/*                                                                        */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,        */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF     */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY   */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,   */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE      */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                 */
+/**************************************************************************/
+
+#include "gltf_document_extension_texture_webp.h"
+
+#include "scene/3d/area_3d.h"
+
+// Import process.
+Error GLTFDocumentExtensionTextureWebP::import_preflight(Ref<GLTFState> p_state, Vector<String> p_extensions) {
+	if (!p_extensions.has("EXT_texture_webp")) {
+		return ERR_SKIP;
+	}
+	return OK;
+}
+
+Vector<String> GLTFDocumentExtensionTextureWebP::get_supported_extensions() {
+	Vector<String> ret;
+	ret.push_back("EXT_texture_webp");
+	return ret;
+}
+
+Error GLTFDocumentExtensionTextureWebP::parse_image_data(Ref<GLTFState> p_state, const PackedByteArray &p_image_data, const String &p_mime_type, Ref<Image> r_image) {
+	if (p_mime_type == "image/webp") {
+		return r_image->load_webp_from_buffer(p_image_data);
+	}
+	return OK;
+}
+
+Error GLTFDocumentExtensionTextureWebP::parse_texture_json(Ref<GLTFState> p_state, const Dictionary &p_texture_json, Ref<GLTFTexture> r_gltf_texture) {
+	if (!p_texture_json.has("extensions")) {
+		return OK;
+	}
+	const Dictionary &extensions = p_texture_json["extensions"];
+	if (!extensions.has("EXT_texture_webp")) {
+		return OK;
+	}
+	const Dictionary &texture_webp = extensions["EXT_texture_webp"];
+	ERR_FAIL_COND_V(!texture_webp.has("source"), ERR_PARSE_ERROR);
+	r_gltf_texture->set_src_image(texture_webp["source"]);
+	return OK;
+}

+ 47 - 0
modules/gltf/extensions/gltf_document_extension_texture_webp.h

@@ -0,0 +1,47 @@
+/**************************************************************************/
+/*  gltf_document_extension_texture_webp.h                                */
+/**************************************************************************/
+/*                         This file is part of:                          */
+/*                             GODOT ENGINE                               */
+/*                        https://godotengine.org                         */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur.                  */
+/*                                                                        */
+/* Permission is hereby granted, free of charge, to any person obtaining  */
+/* a copy of this software and associated documentation files (the        */
+/* "Software"), to deal in the Software without restriction, including    */
+/* without limitation the rights to use, copy, modify, merge, publish,    */
+/* distribute, sublicense, and/or sell copies of the Software, and to     */
+/* permit persons to whom the Software is furnished to do so, subject to  */
+/* the following conditions:                                              */
+/*                                                                        */
+/* The above copyright notice and this permission notice shall be         */
+/* included in all copies or substantial portions of the Software.        */
+/*                                                                        */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,        */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF     */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY   */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,   */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE      */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                 */
+/**************************************************************************/
+
+#ifndef GLTF_DOCUMENT_EXTENSION_TEXTURE_WEBP_H
+#define GLTF_DOCUMENT_EXTENSION_TEXTURE_WEBP_H
+
+#include "gltf_document_extension.h"
+
+class GLTFDocumentExtensionTextureWebP : public GLTFDocumentExtension {
+	GDCLASS(GLTFDocumentExtensionTextureWebP, GLTFDocumentExtension);
+
+public:
+	// Import process.
+	Error import_preflight(Ref<GLTFState> p_state, Vector<String> p_extensions) override;
+	Vector<String> get_supported_extensions() override;
+	Error parse_image_data(Ref<GLTFState> p_state, const PackedByteArray &p_image_data, const String &p_mime_type, Ref<Image> r_image) override;
+	Error parse_texture_json(Ref<GLTFState> p_state, const Dictionary &p_texture_json, Ref<GLTFTexture> r_gltf_texture) override;
+};
+
+#endif // GLTF_DOCUMENT_EXTENSION_TEXTURE_WEBP_H

+ 193 - 184
modules/gltf/gltf_document.cpp

@@ -678,11 +678,11 @@ void GLTFDocument::_compute_node_heights(Ref<GLTFState> p_state) {
 	}
 }
 
-static Vector<uint8_t> _parse_base64_uri(const String &uri) {
-	int start = uri.find(",");
+static Vector<uint8_t> _parse_base64_uri(const String &p_uri) {
+	int start = p_uri.find(",");
 	ERR_FAIL_COND_V(start == -1, Vector<uint8_t>());
 
-	CharString substr = uri.substr(start + 1).ascii();
+	CharString substr = p_uri.substr(start + 1).ascii();
 
 	int strlen = substr.length();
 
@@ -696,6 +696,7 @@ static Vector<uint8_t> _parse_base64_uri(const String &uri) {
 
 	return buf;
 }
+
 Error GLTFDocument::_encode_buffer_glb(Ref<GLTFState> p_state, const String &p_path) {
 	print_verbose("glTF: Total buffers: " + itos(p_state->buffers.size()));
 
@@ -3066,6 +3067,129 @@ Error GLTFDocument::_serialize_images(Ref<GLTFState> p_state, const String &p_pa
 	return OK;
 }
 
+Ref<Image> GLTFDocument::_parse_image_bytes_into_image(Ref<GLTFState> p_state, const Vector<uint8_t> &p_bytes, const String &p_mime_type, int p_index) {
+	Ref<Image> r_image;
+	r_image.instantiate();
+	// Check if any GLTFDocumentExtensions want to import this data as an image.
+	for (Ref<GLTFDocumentExtension> ext : document_extensions) {
+		ERR_CONTINUE(ext.is_null());
+		Error err = ext->parse_image_data(p_state, p_bytes, p_mime_type, r_image);
+		ERR_CONTINUE_MSG(err != OK, "GLTF: Encountered error " + itos(err) + " when parsing image " + itos(p_index) + " in file " + p_state->filename + ". Continuing.");
+		if (!r_image->is_empty()) {
+			return r_image;
+		}
+	}
+	// If no extension wanted to import this data as an image, try to load a PNG or JPEG.
+	// First we honor the mime types if they were defined.
+	if (p_mime_type == "image/png") { // Load buffer as PNG.
+		r_image->load_png_from_buffer(p_bytes);
+	} else if (p_mime_type == "image/jpeg") { // Loader buffer as JPEG.
+		r_image->load_jpg_from_buffer(p_bytes);
+	}
+	// If we didn't pass the above tests, we attempt loading as PNG and then JPEG directly.
+	// This covers URIs with base64-encoded data with application/* type but
+	// no optional mimeType property, or bufferViews with a bogus mimeType
+	// (e.g. `image/jpeg` but the data is actually PNG).
+	// That's not *exactly* what the spec mandates but this lets us be
+	// lenient with bogus glb files which do exist in production.
+	if (r_image->is_empty()) { // Try PNG first.
+		r_image->load_png_from_buffer(p_bytes);
+	}
+	if (r_image->is_empty()) { // And then JPEG.
+		r_image->load_jpg_from_buffer(p_bytes);
+	}
+	// If it still can't be loaded, give up and insert an empty image as placeholder.
+	if (r_image->is_empty()) {
+		ERR_PRINT(vformat("glTF: Couldn't load image index '%d' with its given mimetype: %s.", p_index, p_mime_type));
+	}
+	return r_image;
+}
+
+void GLTFDocument::_parse_image_save_image(Ref<GLTFState> p_state, const String &p_mime_type, int p_index, Ref<Image> p_image) {
+	GLTFState::GLTFHandleBinary handling = GLTFState::GLTFHandleBinary(p_state->handle_binary_image);
+	if (p_image->is_empty() || handling == GLTFState::GLTFHandleBinary::HANDLE_BINARY_DISCARD_TEXTURES) {
+		p_state->images.push_back(Ref<Texture2D>());
+		p_state->source_images.push_back(Ref<Image>());
+		return;
+	}
+#ifdef TOOLS_ENABLED
+	if (Engine::get_singleton()->is_editor_hint() && handling == GLTFState::GLTFHandleBinary::HANDLE_BINARY_EXTRACT_TEXTURES) {
+		if (p_state->base_path.is_empty()) {
+			p_state->images.push_back(Ref<Texture2D>());
+			p_state->source_images.push_back(Ref<Image>());
+		} else if (p_image->get_name().is_empty()) {
+			WARN_PRINT(vformat("glTF: Image index '%d' couldn't be named. Skipping it.", p_index));
+			p_state->images.push_back(Ref<Texture2D>());
+			p_state->source_images.push_back(Ref<Image>());
+		} else {
+			Error err = OK;
+			bool must_import = true;
+			Vector<uint8_t> img_data = p_image->get_data();
+			Dictionary generator_parameters;
+			String file_path = p_state->get_base_path() + "/" + p_state->filename.get_basename() + "_" + p_image->get_name() + ".png";
+			if (FileAccess::exists(file_path + ".import")) {
+				Ref<ConfigFile> config;
+				config.instantiate();
+				config->load(file_path + ".import");
+				if (config->has_section_key("remap", "generator_parameters")) {
+					generator_parameters = (Dictionary)config->get_value("remap", "generator_parameters");
+				}
+				if (!generator_parameters.has("md5")) {
+					must_import = false; // Didn't come from a gltf document; don't overwrite.
+				}
+				String existing_md5 = generator_parameters["md5"];
+				unsigned char md5_hash[16];
+				CryptoCore::md5(img_data.ptr(), img_data.size(), md5_hash);
+				String new_md5 = String::hex_encode_buffer(md5_hash, 16);
+				generator_parameters["md5"] = new_md5;
+				if (new_md5 == existing_md5) {
+					must_import = false;
+				}
+			}
+			if (must_import) {
+				err = p_image->save_png(file_path);
+				ERR_FAIL_COND(err != OK);
+				// ResourceLoader::import will crash if not is_editor_hint(), so this case is protected above and will fall through to uncompressed.
+				HashMap<StringName, Variant> custom_options;
+				custom_options[SNAME("mipmaps/generate")] = true;
+				// Will only use project settings defaults if custom_importer is empty.
+				EditorFileSystem::get_singleton()->update_file(file_path);
+				EditorFileSystem::get_singleton()->reimport_append(file_path, custom_options, String(), generator_parameters);
+			}
+			Ref<Texture2D> saved_image = ResourceLoader::load(file_path, "Texture2D");
+			if (saved_image.is_valid()) {
+				p_state->images.push_back(saved_image);
+				p_state->source_images.push_back(saved_image->get_image());
+			} else {
+				WARN_PRINT(vformat("glTF: Image index '%d' couldn't be loaded with the name: %s. Skipping it.", p_index, p_image->get_name()));
+				// Placeholder to keep count.
+				p_state->images.push_back(Ref<Texture2D>());
+				p_state->source_images.push_back(Ref<Image>());
+			}
+		}
+		return;
+	}
+#endif // TOOLS_ENABLED
+	if (handling == GLTFState::GLTFHandleBinary::HANDLE_BINARY_EMBED_AS_BASISU) {
+		Ref<PortableCompressedTexture2D> tex;
+		tex.instantiate();
+		tex->set_name(p_image->get_name());
+		tex->set_keep_compressed_buffer(true);
+		tex->create_from_image(p_image, PortableCompressedTexture2D::COMPRESSION_MODE_BASIS_UNIVERSAL);
+		p_state->images.push_back(tex);
+		p_state->source_images.push_back(p_image);
+		return;
+	}
+	// This handles the case of HANDLE_BINARY_EMBED_AS_UNCOMPRESSED, and it also serves
+	// as a fallback for HANDLE_BINARY_EXTRACT_TEXTURES when this is not the editor.
+	Ref<ImageTexture> tex;
+	tex.instantiate();
+	tex->set_name(p_image->get_name());
+	tex->set_image(p_image);
+	p_state->images.push_back(tex);
+	p_state->source_images.push_back(p_image);
+}
+
 Error GLTFDocument::_parse_images(Ref<GLTFState> p_state, const String &p_base_path) {
 	ERR_FAIL_NULL_V(p_state, ERR_INVALID_PARAMETER);
 	if (!p_state->json.has("images")) {
@@ -3077,7 +3201,7 @@ Error GLTFDocument::_parse_images(Ref<GLTFState> p_state, const String &p_base_p
 	const Array &images = p_state->json["images"];
 	HashSet<String> used_names;
 	for (int i = 0; i < images.size(); i++) {
-		const Dictionary &d = images[i];
+		const Dictionary &dict = images[i];
 
 		// glTF 2.0 supports PNG and JPEG types, which can be specified as (from spec):
 		// "- a URI to an external file in one of the supported images formats, or
@@ -3088,23 +3212,19 @@ Error GLTFDocument::_parse_images(Ref<GLTFState> p_state, const String &p_base_p
 
 		// We'll assume that we use either URI or bufferView, so let's warn the user
 		// if their image somehow uses both. And fail if it has neither.
-		ERR_CONTINUE_MSG(!d.has("uri") && !d.has("bufferView"), "Invalid image definition in glTF file, it should specify an 'uri' or 'bufferView'.");
-		if (d.has("uri") && d.has("bufferView")) {
+		ERR_CONTINUE_MSG(!dict.has("uri") && !dict.has("bufferView"), "Invalid image definition in glTF file, it should specify an 'uri' or 'bufferView'.");
+		if (dict.has("uri") && dict.has("bufferView")) {
 			WARN_PRINT("Invalid image definition in glTF file using both 'uri' and 'bufferView'. 'uri' will take precedence.");
 		}
 
-		String mimetype;
-		if (d.has("mimeType")) { // Should be "image/png" or "image/jpeg".
-			mimetype = d["mimeType"];
+		String mime_type;
+		if (dict.has("mimeType")) { // Should be "image/png", "image/jpeg", or something handled by an extension.
+			mime_type = dict["mimeType"];
 		}
 
-		Vector<uint8_t> data;
-		const uint8_t *data_ptr = nullptr;
-		int data_size = 0;
-
 		String image_name;
-		if (d.has("name")) {
-			image_name = d["name"];
+		if (dict.has("name")) {
+			image_name = dict["name"];
 			image_name = image_name.get_file().get_basename().validate_filename();
 		}
 		if (image_name.is_empty()) {
@@ -3114,31 +3234,17 @@ Error GLTFDocument::_parse_images(Ref<GLTFState> p_state, const String &p_base_p
 			image_name += "_" + itos(i);
 		}
 		used_names.insert(image_name);
-
-		if (d.has("uri")) {
+		// Load the image data. If we get a byte array, store here for later.
+		Vector<uint8_t> data;
+		if (dict.has("uri")) {
 			// Handles the first two bullet points from the spec (embedded data, or external file).
-			String uri = d["uri"];
-
+			String uri = dict["uri"];
 			if (uri.begins_with("data:")) { // Embedded data using base64.
-				// Validate data MIME types and throw a warning if it's one we don't know/support.
-				if (!uri.begins_with("data:application/octet-stream;base64") &&
-						!uri.begins_with("data:application/gltf-buffer;base64") &&
-						!uri.begins_with("data:image/png;base64") &&
-						!uri.begins_with("data:image/jpeg;base64")) {
-					WARN_PRINT(vformat("glTF: Image index '%d' uses an unsupported URI data type: %s. Skipping it.", i, uri));
-					p_state->images.push_back(Ref<Texture2D>()); // Placeholder to keep count.
-					continue;
-				}
 				data = _parse_base64_uri(uri);
-				data_ptr = data.ptr();
-				data_size = data.size();
 				// mimeType is optional, but if we have it defined in the URI, let's use it.
-				if (mimetype.is_empty()) {
-					if (uri.begins_with("data:image/png;base64")) {
-						mimetype = "image/png";
-					} else if (uri.begins_with("data:image/jpeg;base64")) {
-						mimetype = "image/jpeg";
-					}
+				if (mime_type.is_empty() && uri.contains(";")) {
+					// Trim "data:" prefix which is 5 characters long, and end at ";base64".
+					mime_type = uri.substr(5, uri.find(";base64") - 5);
 				}
 			} else { // Relative path to an external image file.
 				ERR_FAIL_COND_V(p_base_path.is_empty(), ERR_INVALID_PARAMETER);
@@ -3148,161 +3254,53 @@ Error GLTFDocument::_parse_images(Ref<GLTFState> p_state, const String &p_base_p
 				// The spec says that if mimeType is defined, it should take precedence (e.g.
 				// there could be a `.png` image which is actually JPEG), but there's no easy
 				// API for that in Godot, so we'd have to load as a buffer (i.e. embedded in
-				// the material), so we do this only as fallback.
+				// the material), so we only do that only as fallback.
 				Ref<Texture2D> texture = ResourceLoader::load(uri);
-				String extension = uri.get_extension().to_lower();
 				if (texture.is_valid()) {
 					p_state->images.push_back(texture);
 					p_state->source_images.push_back(texture->get_image());
 					continue;
-				} else if (mimetype == "image/png" || mimetype == "image/jpeg" || extension == "png" || extension == "jpg" || extension == "jpeg") {
-					// Fallback to loading as byte array.
-					// This enables us to support the spec's requirement that we honor mimetype
-					// regardless of file URI.
-					data = FileAccess::get_file_as_bytes(uri);
-					if (data.size() == 0) {
-						WARN_PRINT(vformat("glTF: Image index '%d' couldn't be loaded as a buffer of MIME type '%s' from URI: %s. Skipping it.", i, mimetype, uri));
-						p_state->images.push_back(Ref<Texture2D>()); // Placeholder to keep count.
-						continue;
-					}
-					data_ptr = data.ptr();
-					data_size = data.size();
-				} else {
-					WARN_PRINT(vformat("glTF: Image index '%d' couldn't be loaded from URI: %s. Skipping it.", i, uri));
+				}
+				// mimeType is optional, but if we have it in the file extension, let's use it.
+				// If the mimeType does not match with the file extension, either it should be
+				// specified in the file, or the GLTFDocumentExtension should handle it.
+				if (mime_type.is_empty()) {
+					mime_type = "image/" + uri.get_extension();
+				}
+				// Fallback to loading as byte array. This enables us to support the
+				// spec's requirement that we honor mimetype regardless of file URI.
+				data = FileAccess::get_file_as_bytes(uri);
+				if (data.size() == 0) {
+					WARN_PRINT(vformat("glTF: Image index '%d' couldn't be loaded as a buffer of MIME type '%s' from URI: %s because there was no data to load. Skipping it.", i, mime_type, uri));
 					p_state->images.push_back(Ref<Texture2D>()); // Placeholder to keep count.
+					p_state->source_images.push_back(Ref<Image>());
 					continue;
 				}
 			}
-		} else if (d.has("bufferView")) {
+		} else if (dict.has("bufferView")) {
 			// Handles the third bullet point from the spec (bufferView).
-			ERR_FAIL_COND_V_MSG(mimetype.is_empty(), ERR_FILE_CORRUPT,
-					vformat("glTF: Image index '%d' specifies 'bufferView' but no 'mimeType', which is invalid.", i));
-
-			const GLTFBufferViewIndex bvi = d["bufferView"];
-
+			ERR_FAIL_COND_V_MSG(mime_type.is_empty(), ERR_FILE_CORRUPT, vformat("glTF: Image index '%d' specifies 'bufferView' but no 'mimeType', which is invalid.", i));
+			const GLTFBufferViewIndex bvi = dict["bufferView"];
 			ERR_FAIL_INDEX_V(bvi, p_state->buffer_views.size(), ERR_PARAMETER_RANGE_ERROR);
-
 			Ref<GLTFBufferView> bv = p_state->buffer_views[bvi];
-
 			const GLTFBufferIndex bi = bv->buffer;
 			ERR_FAIL_INDEX_V(bi, p_state->buffers.size(), ERR_PARAMETER_RANGE_ERROR);
-
 			ERR_FAIL_COND_V(bv->byte_offset + bv->byte_length > p_state->buffers[bi].size(), ERR_FILE_CORRUPT);
-
-			data_ptr = &p_state->buffers[bi][bv->byte_offset];
-			data_size = bv->byte_length;
-		}
-
-		Ref<Image> img;
-
-		// First we honor the mime types if they were defined.
-		if (mimetype == "image/png") { // Load buffer as PNG.
-			ERR_FAIL_COND_V(Image::_png_mem_loader_func == nullptr, ERR_UNAVAILABLE);
-			img = Image::_png_mem_loader_func(data_ptr, data_size);
-		} else if (mimetype == "image/jpeg") { // Loader buffer as JPEG.
-			ERR_FAIL_COND_V(Image::_jpg_mem_loader_func == nullptr, ERR_UNAVAILABLE);
-			img = Image::_jpg_mem_loader_func(data_ptr, data_size);
-		}
-
-		// If we didn't pass the above tests, we attempt loading as PNG and then
-		// JPEG directly.
-		// This covers URIs with base64-encoded data with application/* type but
-		// no optional mimeType property, or bufferViews with a bogus mimeType
-		// (e.g. `image/jpeg` but the data is actually PNG).
-		// That's not *exactly* what the spec mandates but this lets us be
-		// lenient with bogus glb files which do exist in production.
-		if (img.is_null()) { // Try PNG first.
-			ERR_FAIL_COND_V(Image::_png_mem_loader_func == nullptr, ERR_UNAVAILABLE);
-			img = Image::_png_mem_loader_func(data_ptr, data_size);
-		}
-		if (img.is_null()) { // And then JPEG.
-			ERR_FAIL_COND_V(Image::_jpg_mem_loader_func == nullptr, ERR_UNAVAILABLE);
-			img = Image::_jpg_mem_loader_func(data_ptr, data_size);
-		}
-		// Now we've done our best, fix your scenes.
-		if (img.is_null()) {
-			ERR_PRINT(vformat("glTF: Couldn't load image index '%d' with its given mimetype: %s.", i, mimetype));
-			p_state->images.push_back(Ref<Texture2D>());
+			const PackedByteArray &buffer = p_state->buffers[bi];
+			data = buffer.slice(bv->byte_offset, bv->byte_offset + bv->byte_length);
+		}
+		// Done loading the image data bytes. Check that we actually got data to parse.
+		// Note: There are paths above that return early, so this point might not be reached.
+		if (data.is_empty()) {
+			WARN_PRINT(vformat("glTF: Image index '%d' couldn't be loaded, no data found. Skipping it.", i));
+			p_state->images.push_back(Ref<Texture2D>()); // Placeholder to keep count.
 			p_state->source_images.push_back(Ref<Image>());
 			continue;
 		}
+		// Parse the image data from bytes into an Image resource and save if needed.
+		Ref<Image> img = _parse_image_bytes_into_image(p_state, data, mime_type, i);
 		img->set_name(image_name);
-		if (GLTFState::GLTFHandleBinary(p_state->handle_binary_image) == GLTFState::GLTFHandleBinary::HANDLE_BINARY_DISCARD_TEXTURES) {
-			p_state->images.push_back(Ref<Texture2D>());
-			p_state->source_images.push_back(Ref<Image>());
-#ifdef TOOLS_ENABLED
-		} else if (Engine::get_singleton()->is_editor_hint() && GLTFState::GLTFHandleBinary(p_state->handle_binary_image) == GLTFState::GLTFHandleBinary::HANDLE_BINARY_EXTRACT_TEXTURES) {
-			if (p_state->base_path.is_empty()) {
-				p_state->images.push_back(Ref<Texture2D>());
-				p_state->source_images.push_back(Ref<Image>());
-			} else if (img->get_name().is_empty()) {
-				WARN_PRINT(vformat("glTF: Image index '%d' couldn't be named. Skipping it.", i));
-				p_state->images.push_back(Ref<Texture2D>());
-				p_state->source_images.push_back(Ref<Image>());
-			} else {
-				Error err = OK;
-				bool must_import = true;
-				Vector<uint8_t> img_data = img->get_data();
-				Dictionary generator_parameters;
-				String file_path = p_state->get_base_path() + "/" + p_state->filename.get_basename() + "_" + img->get_name() + ".png";
-				if (FileAccess::exists(file_path + ".import")) {
-					Ref<ConfigFile> config;
-					config.instantiate();
-					config->load(file_path + ".import");
-					if (config->has_section_key("remap", "generator_parameters")) {
-						generator_parameters = (Dictionary)config->get_value("remap", "generator_parameters");
-					}
-					if (!generator_parameters.has("md5")) {
-						must_import = false; // Didn't come form a gltf document; don't overwrite.
-					}
-					String existing_md5 = generator_parameters["md5"];
-					unsigned char md5_hash[16];
-					CryptoCore::md5(img_data.ptr(), img_data.size(), md5_hash);
-					String new_md5 = String::hex_encode_buffer(md5_hash, 16);
-					generator_parameters["md5"] = new_md5;
-					if (new_md5 == existing_md5) {
-						must_import = false;
-					}
-				}
-				if (must_import) {
-					err = img->save_png(file_path);
-					ERR_FAIL_COND_V(err != OK, err);
-					// ResourceLoader::import will crash if not is_editor_hint(), so this case is protected above and will fall through to uncompressed.
-					HashMap<StringName, Variant> custom_options;
-					custom_options[SNAME("mipmaps/generate")] = true;
-					// Will only use project settings defaults if custom_importer is empty.
-					EditorFileSystem::get_singleton()->update_file(file_path);
-					EditorFileSystem::get_singleton()->reimport_append(file_path, custom_options, String(), generator_parameters);
-				}
-				Ref<Texture2D> saved_image = ResourceLoader::load(file_path, "Texture2D");
-				if (saved_image.is_valid()) {
-					p_state->images.push_back(saved_image);
-					p_state->source_images.push_back(img);
-				} else {
-					WARN_PRINT(vformat("glTF: Image index '%d' couldn't be loaded with the name: %s. Skipping it.", i, img->get_name()));
-					// Placeholder to keep count.
-					p_state->images.push_back(Ref<Texture2D>());
-					p_state->source_images.push_back(Ref<Image>());
-				}
-			}
-#endif
-		} else if (GLTFState::GLTFHandleBinary(p_state->handle_binary_image) == GLTFState::GLTFHandleBinary::HANDLE_BINARY_EMBED_AS_BASISU) {
-			Ref<PortableCompressedTexture2D> tex;
-			tex.instantiate();
-			tex->set_name(img->get_name());
-			tex->set_keep_compressed_buffer(true);
-			tex->create_from_image(img, PortableCompressedTexture2D::COMPRESSION_MODE_BASIS_UNIVERSAL);
-			p_state->images.push_back(tex);
-			p_state->source_images.push_back(img);
-		} else {
-			// This handles two cases: if editor hint and HANDLE_BINARY_EXTRACT_TEXTURES; or if HANDLE_BINARY_EMBED_AS_UNCOMPRESSED
-			Ref<ImageTexture> tex;
-			tex.instantiate();
-			tex->set_name(img->get_name());
-			tex->set_image(img);
-			p_state->images.push_back(tex);
-			p_state->source_images.push_back(img);
-		}
+		_parse_image_save_image(p_state, mime_type, i, img);
 	}
 
 	print_verbose("glTF: Total images: " + itos(p_state->images.size()));
@@ -3340,19 +3338,28 @@ Error GLTFDocument::_parse_textures(Ref<GLTFState> p_state) {
 
 	const Array &textures = p_state->json["textures"];
 	for (GLTFTextureIndex i = 0; i < textures.size(); i++) {
-		const Dictionary &d = textures[i];
-
-		ERR_FAIL_COND_V(!d.has("source"), ERR_PARSE_ERROR);
-
-		Ref<GLTFTexture> t;
-		t.instantiate();
-		t->set_src_image(d["source"]);
-		if (d.has("sampler")) {
-			t->set_sampler(d["sampler"]);
-		} else {
-			t->set_sampler(-1);
+		const Dictionary &dict = textures[i];
+		Ref<GLTFTexture> texture;
+		texture.instantiate();
+		// Check if any GLTFDocumentExtensions want to handle this texture JSON.
+		for (Ref<GLTFDocumentExtension> ext : document_extensions) {
+			ERR_CONTINUE(ext.is_null());
+			Error err = ext->parse_texture_json(p_state, dict, texture);
+			ERR_CONTINUE_MSG(err != OK, "GLTF: Encountered error " + itos(err) + " when parsing texture JSON " + String(Variant(dict)) + " in file " + p_state->filename + ". Continuing.");
+			if (texture->get_src_image() != -1) {
+				break;
+			}
+		}
+		if (texture->get_src_image() == -1) {
+			// No extensions handled it, so use the base GLTF source.
+			// This may be the fallback, or the only option anyway.
+			ERR_FAIL_COND_V(!dict.has("source"), ERR_PARSE_ERROR);
+			texture->set_src_image(dict["source"]);
+		}
+		if (texture->get_sampler() == -1 && dict.has("sampler")) {
+			texture->set_sampler(dict["sampler"]);
 		}
-		p_state->textures.push_back(t);
+		p_state->textures.push_back(texture);
 	}
 
 	return OK;
@@ -3365,6 +3372,7 @@ GLTFTextureIndex GLTFDocument::_set_texture(Ref<GLTFState> p_state, Ref<Texture2
 	ERR_FAIL_COND_V(p_texture->get_image().is_null(), -1);
 	GLTFImageIndex gltf_src_image_i = p_state->images.size();
 	p_state->images.push_back(p_texture);
+	p_state->source_images.push_back(p_texture->get_image());
 	gltf_texture->set_src_image(gltf_src_image_i);
 	gltf_texture->set_sampler(_set_sampler_for_mode(p_state, p_filter_mode, p_repeats));
 	GLTFTextureIndex gltf_texture_i = p_state->textures.size();
@@ -3389,6 +3397,7 @@ Ref<Texture2D> GLTFDocument::_get_texture(Ref<GLTFState> p_state, const GLTFText
 			portable_texture->create_from_image(new_img, PortableCompressedTexture2D::COMPRESSION_MODE_BASIS_UNIVERSAL, false);
 		}
 		p_state->images.write[image] = portable_texture;
+		p_state->source_images.write[image] = new_img;
 	}
 	return p_state->images[image];
 }

+ 2 - 0
modules/gltf/gltf_document.h

@@ -151,6 +151,8 @@ private:
 	Error _serialize_texture_samplers(Ref<GLTFState> p_state);
 	Error _serialize_images(Ref<GLTFState> p_state, const String &p_path);
 	Error _serialize_lights(Ref<GLTFState> p_state);
+	Ref<Image> _parse_image_bytes_into_image(Ref<GLTFState> p_state, const Vector<uint8_t> &p_bytes, const String &p_mime_type, int p_index);
+	void _parse_image_save_image(Ref<GLTFState> p_state, const String &p_mime_type, int p_index, Ref<Image> p_image);
 	Error _parse_images(Ref<GLTFState> p_state, const String &p_base_path);
 	Error _parse_textures(Ref<GLTFState> p_state);
 	Error _parse_texture_samplers(Ref<GLTFState> p_state);

+ 2 - 0
modules/gltf/register_types.cpp

@@ -31,6 +31,7 @@
 #include "register_types.h"
 
 #include "extensions/gltf_document_extension_convert_importer_mesh.h"
+#include "extensions/gltf_document_extension_texture_webp.h"
 #include "extensions/gltf_spec_gloss.h"
 #include "extensions/physics/gltf_document_extension_physics.h"
 #include "gltf_document.h"
@@ -131,6 +132,7 @@ void initialize_gltf_module(ModuleInitializationLevel p_level) {
 		GDREGISTER_CLASS(GLTFTextureSampler);
 		// Register GLTFDocumentExtension classes with GLTFDocument.
 		GLTF_REGISTER_DOCUMENT_EXTENSION(GLTFDocumentExtensionPhysics);
+		GLTF_REGISTER_DOCUMENT_EXTENSION(GLTFDocumentExtensionTextureWebP);
 		bool is_editor = ::Engine::get_singleton()->is_editor_hint();
 		if (!is_editor) {
 			GLTF_REGISTER_DOCUMENT_EXTENSION(GLTFDocumentExtensionConvertImporterMesh);

+ 1 - 1
modules/gltf/structures/gltf_texture.h

@@ -38,7 +38,7 @@ class GLTFTexture : public Resource {
 	GDCLASS(GLTFTexture, Resource);
 
 private:
-	GLTFImageIndex src_image = 0;
+	GLTFImageIndex src_image = -1;
 	GLTFTextureSamplerIndex sampler = -1;
 
 protected:

+ 2 - 2
modules/webp/image_loader_webp.cpp

@@ -40,10 +40,10 @@
 #include <webp/decode.h>
 #include <webp/encode.h>
 
-static Ref<Image> _webp_mem_loader_func(const uint8_t *p_png, int p_size) {
+static Ref<Image> _webp_mem_loader_func(const uint8_t *p_webp_data, int p_size) {
 	Ref<Image> img;
 	img.instantiate();
-	Error err = WebPCommon::webp_load_image_from_buffer(img.ptr(), p_png, p_size);
+	Error err = WebPCommon::webp_load_image_from_buffer(img.ptr(), p_webp_data, p_size);
 	ERR_FAIL_COND_V(err, Ref<Image>());
 	return img;
 }