From 7da93a0bc06c421badd6396968954e28b5115d74 Mon Sep 17 00:00:00 2001 From: Aaron Franke Date: Tue, 9 May 2023 21:28:40 -0500 Subject: [PATCH] Add support for extending GLTF with more texture formats & support WebP --- .../doc_classes/GLTFDocumentExtension.xml | 21 + modules/gltf/doc_classes/GLTFState.xml | 2 + modules/gltf/doc_classes/GLTFTexture.xml | 3 +- .../extensions/gltf_document_extension.cpp | 18 + .../gltf/extensions/gltf_document_extension.h | 4 + .../gltf_document_extension_texture_webp.cpp | 68 ++++ .../gltf_document_extension_texture_webp.h | 47 +++ modules/gltf/gltf_document.cpp | 377 +++++++++--------- modules/gltf/gltf_document.h | 2 + modules/gltf/register_types.cpp | 2 + modules/gltf/structures/gltf_texture.h | 2 +- modules/webp/image_loader_webp.cpp | 4 +- 12 files changed, 362 insertions(+), 188 deletions(-) create mode 100644 modules/gltf/extensions/gltf_document_extension_texture_webp.cpp create mode 100644 modules/gltf/extensions/gltf_document_extension_texture_webp.h diff --git a/modules/gltf/doc_classes/GLTFDocumentExtension.xml b/modules/gltf/doc_classes/GLTFDocumentExtension.xml index 62030c5464fc..6b8f2aa90476 100644 --- a/modules/gltf/doc_classes/GLTFDocumentExtension.xml +++ b/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. + + + + + + + + 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. + + @@ -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. + + + + + + + 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. + + diff --git a/modules/gltf/doc_classes/GLTFState.xml b/modules/gltf/doc_classes/GLTFState.xml index b3823ef9704c..34dd1069f5ee 100644 --- a/modules/gltf/doc_classes/GLTFState.xml +++ b/modules/gltf/doc_classes/GLTFState.xml @@ -70,6 +70,7 @@ + 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. @@ -182,6 +183,7 @@ + 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. diff --git a/modules/gltf/doc_classes/GLTFTexture.xml b/modules/gltf/doc_classes/GLTFTexture.xml index df315edb9a16..768e7a8945d2 100644 --- a/modules/gltf/doc_classes/GLTFTexture.xml +++ b/modules/gltf/doc_classes/GLTFTexture.xml @@ -10,7 +10,8 @@ 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). - + + 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. diff --git a/modules/gltf/extensions/gltf_document_extension.cpp b/modules/gltf/extensions/gltf_document_extension.cpp index bedb42eb3218..2804a8b0a221 100644 --- a/modules/gltf/extensions/gltf_document_extension.cpp +++ b/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 p_state, Ref p_state, const PackedByteArray &p_image_data, const String &p_mime_type, Ref 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 p_state, const Dictionary &p_texture_json, Ref 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 p_state, Ref p_gltf_node, Node *p_scene_parent) { ERR_FAIL_NULL_V(p_state, nullptr); ERR_FAIL_NULL_V(p_gltf_node, nullptr); diff --git a/modules/gltf/extensions/gltf_document_extension.h b/modules/gltf/extensions/gltf_document_extension.h index 3531f81f6fa2..d922588a291a 100644 --- a/modules/gltf/extensions/gltf_document_extension.h +++ b/modules/gltf/extensions/gltf_document_extension.h @@ -44,6 +44,8 @@ class GLTFDocumentExtension : public Resource { virtual Error import_preflight(Ref p_state, Vector p_extensions); virtual Vector get_supported_extensions(); virtual Error parse_node_extensions(Ref p_state, Ref p_gltf_node, Dictionary &p_extensions); + virtual Error parse_image_data(Ref p_state, const PackedByteArray &p_image_data, const String &p_mime_type, Ref r_image); + virtual Error parse_texture_json(Ref p_state, const Dictionary &p_texture_json, Ref r_gltf_texture); virtual Node3D *generate_scene_node(Ref p_state, Ref p_gltf_node, Node *p_scene_parent); virtual Error import_post_parse(Ref p_state); virtual Error import_node(Ref p_state, Ref p_gltf_node, Dictionary &r_json, Node *p_node); @@ -58,6 +60,8 @@ class GLTFDocumentExtension : public Resource { GDVIRTUAL2R(Error, _import_preflight, Ref, Vector); GDVIRTUAL0R(Vector, _get_supported_extensions); GDVIRTUAL3R(Error, _parse_node_extensions, Ref, Ref, Dictionary); + GDVIRTUAL4R(Error, _parse_image_data, Ref, PackedByteArray, String, Ref); + GDVIRTUAL3R(Error, _parse_texture_json, Ref, Dictionary, Ref); GDVIRTUAL3R(Node3D *, _generate_scene_node, Ref, Ref, Node *); GDVIRTUAL1R(Error, _import_post_parse, Ref); GDVIRTUAL4R(Error, _import_node, Ref, Ref, Dictionary, Node *); diff --git a/modules/gltf/extensions/gltf_document_extension_texture_webp.cpp b/modules/gltf/extensions/gltf_document_extension_texture_webp.cpp new file mode 100644 index 000000000000..ded497096850 --- /dev/null +++ b/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 p_state, Vector p_extensions) { + if (!p_extensions.has("EXT_texture_webp")) { + return ERR_SKIP; + } + return OK; +} + +Vector GLTFDocumentExtensionTextureWebP::get_supported_extensions() { + Vector ret; + ret.push_back("EXT_texture_webp"); + return ret; +} + +Error GLTFDocumentExtensionTextureWebP::parse_image_data(Ref p_state, const PackedByteArray &p_image_data, const String &p_mime_type, Ref 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 p_state, const Dictionary &p_texture_json, Ref 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; +} diff --git a/modules/gltf/extensions/gltf_document_extension_texture_webp.h b/modules/gltf/extensions/gltf_document_extension_texture_webp.h new file mode 100644 index 000000000000..9abf09a41f07 --- /dev/null +++ b/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 p_state, Vector p_extensions) override; + Vector get_supported_extensions() override; + Error parse_image_data(Ref p_state, const PackedByteArray &p_image_data, const String &p_mime_type, Ref r_image) override; + Error parse_texture_json(Ref p_state, const Dictionary &p_texture_json, Ref r_gltf_texture) override; +}; + +#endif // GLTF_DOCUMENT_EXTENSION_TEXTURE_WEBP_H diff --git a/modules/gltf/gltf_document.cpp b/modules/gltf/gltf_document.cpp index 251aa6375f77..d77d9c6f6685 100644 --- a/modules/gltf/gltf_document.cpp +++ b/modules/gltf/gltf_document.cpp @@ -678,11 +678,11 @@ void GLTFDocument::_compute_node_heights(Ref p_state) { } } -static Vector _parse_base64_uri(const String &uri) { - int start = uri.find(","); +static Vector _parse_base64_uri(const String &p_uri) { + int start = p_uri.find(","); ERR_FAIL_COND_V(start == -1, Vector()); - 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 _parse_base64_uri(const String &uri) { return buf; } + Error GLTFDocument::_encode_buffer_glb(Ref 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 p_state, const String &p_pa return OK; } +Ref GLTFDocument::_parse_image_bytes_into_image(Ref p_state, const Vector &p_bytes, const String &p_mime_type, int p_index) { + Ref r_image; + r_image.instantiate(); + // Check if any GLTFDocumentExtensions want to import this data as an image. + for (Ref 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 p_state, const String &p_mime_type, int p_index, Ref 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()); + p_state->source_images.push_back(Ref()); + 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()); + p_state->source_images.push_back(Ref()); + } 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()); + p_state->source_images.push_back(Ref()); + } else { + Error err = OK; + bool must_import = true; + Vector 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 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 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 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()); + p_state->source_images.push_back(Ref()); + } + } + return; + } +#endif // TOOLS_ENABLED + if (handling == GLTFState::GLTFHandleBinary::HANDLE_BINARY_EMBED_AS_BASISU) { + Ref 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 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 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 p_state, const String &p_base_p const Array &images = p_state->json["images"]; HashSet 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 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 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 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 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()); // 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 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 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()); // 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()); // Placeholder to keep count. + p_state->source_images.push_back(Ref()); 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 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 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()); + 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()); // Placeholder to keep count. p_state->source_images.push_back(Ref()); continue; } + // Parse the image data from bytes into an Image resource and save if needed. + Ref 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()); - p_state->source_images.push_back(Ref()); -#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()); - p_state->source_images.push_back(Ref()); - } 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()); - p_state->source_images.push_back(Ref()); - } else { - Error err = OK; - bool must_import = true; - Vector 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 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 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 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()); - p_state->source_images.push_back(Ref()); - } - } -#endif - } else if (GLTFState::GLTFHandleBinary(p_state->handle_binary_image) == GLTFState::GLTFHandleBinary::HANDLE_BINARY_EMBED_AS_BASISU) { - Ref 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 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 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 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 texture; + texture.instantiate(); + // Check if any GLTFDocumentExtensions want to handle this texture JSON. + for (Ref 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 p_state, Refget_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 GLTFDocument::_get_texture(Ref 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]; } diff --git a/modules/gltf/gltf_document.h b/modules/gltf/gltf_document.h index ae19f67390f0..718b05b959ed 100644 --- a/modules/gltf/gltf_document.h +++ b/modules/gltf/gltf_document.h @@ -151,6 +151,8 @@ class GLTFDocument : public Resource { Error _serialize_texture_samplers(Ref p_state); Error _serialize_images(Ref p_state, const String &p_path); Error _serialize_lights(Ref p_state); + Ref _parse_image_bytes_into_image(Ref p_state, const Vector &p_bytes, const String &p_mime_type, int p_index); + void _parse_image_save_image(Ref p_state, const String &p_mime_type, int p_index, Ref p_image); Error _parse_images(Ref p_state, const String &p_base_path); Error _parse_textures(Ref p_state); Error _parse_texture_samplers(Ref p_state); diff --git a/modules/gltf/register_types.cpp b/modules/gltf/register_types.cpp index 2f66e5c5a52f..42e3476ff3fe 100644 --- a/modules/gltf/register_types.cpp +++ b/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); diff --git a/modules/gltf/structures/gltf_texture.h b/modules/gltf/structures/gltf_texture.h index 8def62a9a134..a9e19e65eac1 100644 --- a/modules/gltf/structures/gltf_texture.h +++ b/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: diff --git a/modules/webp/image_loader_webp.cpp b/modules/webp/image_loader_webp.cpp index 13a22b5d18b5..b8460fe387b0 100644 --- a/modules/webp/image_loader_webp.cpp +++ b/modules/webp/image_loader_webp.cpp @@ -40,10 +40,10 @@ #include #include -static Ref _webp_mem_loader_func(const uint8_t *p_png, int p_size) { +static Ref _webp_mem_loader_func(const uint8_t *p_webp_data, int p_size) { Ref 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()); return img; }