From 0065730efead82889560887d23328c3923a9aa35 Mon Sep 17 00:00:00 2001 From: demolke Date: Fri, 29 Nov 2024 21:33:00 +0100 Subject: [PATCH] GLTF: Don't duplicate textures when importing blend files Blender imports will always start within `.godot/imported` folder because we first convert the .blend file to .gltf, store it in `.godot/imported` and run the import from there, so on-disk resources linked from .blend files end up with duplicate textures. --- .../editor/editor_scene_importer_gltf.cpp | 3 + modules/gltf/gltf_document.cpp | 75 +++++--- modules/gltf/gltf_document.h | 2 +- .../embedded_texture.gltf | 147 ++++++++++++++++ .../gltf_placed_in_dot_godot_imported.gltf | 147 ++++++++++++++++ .../texture.png | Bin 0 -> 92 bytes modules/gltf/tests/test_gltf.h | 163 ++++++++++++++++++ modules/gltf/tests/test_gltf_extras.h | 83 ++------- modules/gltf/tests/test_gltf_images.h | 147 ++++++++++++++++ 9 files changed, 674 insertions(+), 93 deletions(-) create mode 100644 modules/gltf/tests/data/gltf_embedded_texture/embedded_texture.gltf create mode 100644 modules/gltf/tests/data/gltf_placed_in_dot_godot_imported/gltf_placed_in_dot_godot_imported.gltf create mode 100644 modules/gltf/tests/data/gltf_placed_in_dot_godot_imported/texture.png create mode 100644 modules/gltf/tests/test_gltf.h create mode 100644 modules/gltf/tests/test_gltf_images.h diff --git a/modules/gltf/editor/editor_scene_importer_gltf.cpp b/modules/gltf/editor/editor_scene_importer_gltf.cpp index 41e294cfc6e1..3e75017fe65b 100644 --- a/modules/gltf/editor/editor_scene_importer_gltf.cpp +++ b/modules/gltf/editor/editor_scene_importer_gltf.cpp @@ -62,6 +62,9 @@ Node *EditorSceneFormatImporterGLTF::import_scene(const String &p_path, uint32_t if (p_options.has(SNAME("nodes/import_as_skeleton_bones")) ? (bool)p_options[SNAME("nodes/import_as_skeleton_bones")] : false) { state->set_import_as_skeleton_bones(true); } + if (p_options.has(SNAME("extract_path"))) { + state->set_extract_path(p_options["extract_path"]); + } state->set_bake_fps(p_options["animation/fps"]); Error err = gltf->append_from_file(p_path, state, p_flags); if (err != OK) { diff --git a/modules/gltf/gltf_document.cpp b/modules/gltf/gltf_document.cpp index 2f36c29ec4a5..98ca398748eb 100644 --- a/modules/gltf/gltf_document.cpp +++ b/modules/gltf/gltf_document.cpp @@ -3931,7 +3931,7 @@ Ref GLTFDocument::_parse_image_bytes_into_image(Ref p_state, c return r_image; } -void GLTFDocument::_parse_image_save_image(Ref p_state, const Vector &p_bytes, const String &p_file_extension, int p_index, Ref p_image) { +void GLTFDocument::_parse_image_save_image(Ref p_state, const Vector &p_bytes, const String &p_resource_uri, const String &p_file_extension, 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()); @@ -3949,33 +3949,46 @@ void GLTFDocument::_parse_image_save_image(Ref p_state, const Vector< WARN_PRINT(vformat("glTF: Image index '%d' did not have a name. It will be automatically given a name based on its index.", p_index)); p_image->set_name(itos(p_index)); } - bool must_import = true; + bool must_write = true; // If the resource does not exist on the disk within res:// directory write it. + bool must_import = true; // Trigger import. Vector img_data = p_image->get_data(); Dictionary generator_parameters; - String file_path = p_state->get_extract_path().path_join(p_state->get_extract_prefix() + "_" + p_image->get_name()); - file_path += p_file_extension.is_empty() ? ".png" : p_file_extension; - 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 file_path; + // If resource_uri is outside of .godot/imported folder, use it. + if (!p_resource_uri.is_empty() && !p_resource_uri.begins_with("res://.godot/imported")) { + file_path = p_resource_uri; + must_import = true; + must_write = !FileAccess::exists(file_path); + } else { + // Texture data has to be written to the res:// folder and imported. + file_path = p_state->get_extract_path().path_join(p_state->get_extract_prefix() + "_" + p_image->get_name()); + file_path += p_file_extension.is_empty() ? ".png" : p_file_extension; + 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_write = false; // Didn't come from a gltf document; don't overwrite. + must_import = false; // And don't import. + } } } - if (must_import) { + + if (must_write) { 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_write = false; must_import = false; } } - if (must_import) { + if (must_write) { Error err = OK; if (p_file_extension.is_empty()) { // If a file extension was not specified, save the image data to a PNG file. @@ -3988,10 +4001,13 @@ void GLTFDocument::_parse_image_save_image(Ref p_state, const Vector< file->store_buffer(p_bytes); file->close(); } + } + if (must_import) { // 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); } @@ -4001,7 +4017,7 @@ void GLTFDocument::_parse_image_save_image(Ref p_state, const Vector< p_state->source_images.push_back(saved_image->get_image()); return; } else { - WARN_PRINT(vformat("glTF: Image index '%d' with the name '%s' couldn't be imported. It will be loaded directly instead, uncompressed.", p_index, p_image->get_name())); + WARN_PRINT(vformat("glTF: Image index '%d' with the name '%s' resolved to %s couldn't be imported. It will be loaded directly instead, uncompressed.", p_index, p_image->get_name(), file_path)); } } } @@ -4069,6 +4085,9 @@ Error GLTFDocument::_parse_images(Ref p_state, const String &p_base_p while (used_names.has(image_name)) { image_name += "_" + itos(i); } + + String resource_uri; + used_names.insert(image_name); // Load the image data. If we get a byte array, store here for later. Vector data; @@ -4086,14 +4105,14 @@ Error GLTFDocument::_parse_images(Ref p_state, const String &p_base_p ERR_FAIL_COND_V(p_base_path.is_empty(), ERR_INVALID_PARAMETER); uri = uri.uri_decode(); uri = p_base_path.path_join(uri).replace("\\", "/"); // Fix for Windows. - // If the image is in the .godot/imported directory, we can't use ResourceLoader. - if (!p_base_path.begins_with("res://.godot/imported")) { - // ResourceLoader will rely on the file extension to use the relevant loader. - // 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 only do that only as fallback. - Ref texture = ResourceLoader::load(uri, "Texture2D"); + resource_uri = uri.simplify_path(); + // ResourceLoader will rely on the file extension to use the relevant loader. + // 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 only do that only as fallback. + if (ResourceLoader::exists(resource_uri)) { + Ref texture = ResourceLoader::load(resource_uri, "Texture2D"); if (texture.is_valid()) { p_state->images.push_back(texture); p_state->source_images.push_back(texture->get_image()); @@ -4104,13 +4123,13 @@ Error GLTFDocument::_parse_images(Ref p_state, const String &p_base_p // 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(); + mime_type = "image/" + resource_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); + data = FileAccess::get_file_as_bytes(resource_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)); + 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, resource_uri)); p_state->images.push_back(Ref()); // Placeholder to keep count. p_state->source_images.push_back(Ref()); continue; @@ -4140,7 +4159,7 @@ Error GLTFDocument::_parse_images(Ref p_state, const String &p_base_p String file_extension; Ref img = _parse_image_bytes_into_image(p_state, data, mime_type, i, file_extension); img->set_name(image_name); - _parse_image_save_image(p_state, data, file_extension, i, img); + _parse_image_save_image(p_state, data, resource_uri, file_extension, i, img); } print_verbose("glTF: Total images: " + itos(p_state->images.size())); diff --git a/modules/gltf/gltf_document.h b/modules/gltf/gltf_document.h index a6d6caa3f0a7..f17d1a1d7328 100644 --- a/modules/gltf/gltf_document.h +++ b/modules/gltf/gltf_document.h @@ -190,7 +190,7 @@ class GLTFDocument : public Resource { Error _serialize_images(Ref p_state); 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, String &r_file_extension); - void _parse_image_save_image(Ref p_state, const Vector &p_bytes, const String &p_file_extension, int p_index, Ref p_image); + void _parse_image_save_image(Ref p_state, const Vector &p_bytes, const String &p_resource_uri, const String &p_file_extension, 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/tests/data/gltf_embedded_texture/embedded_texture.gltf b/modules/gltf/tests/data/gltf_embedded_texture/embedded_texture.gltf new file mode 100644 index 000000000000..59b154fb3d67 --- /dev/null +++ b/modules/gltf/tests/data/gltf_embedded_texture/embedded_texture.gltf @@ -0,0 +1,147 @@ +{ + "asset":{ + "generator":"Khronos glTF Blender I/O v4.2.70", + "version":"2.0" + }, + "scene":0, + "scenes":[ + { + "name":"Scene", + "nodes":[ + 1 + ] + } + ], + "nodes":[ + { + "mesh":0, + "name":"mesh_instance_3d" + }, + { + "children":[ + 0 + ], + "name":"_Node3D_6" + } + ], + "materials":[ + { + "name":"material", + "pbrMetallicRoughness":{ + "baseColorFactor":[ + 0.9999998807907104, + 0.9999998807907104, + 0.9999998807907104, + 1 + ], + "baseColorTexture":{ + "index":0 + }, + "metallicFactor":0 + } + } + ], + "meshes":[ + { + "name":"Mesh_0", + "primitives":[ + { + "attributes":{ + "POSITION":0, + "NORMAL":1, + "TEXCOORD_0":2 + }, + "indices":3, + "material":0 + } + ] + } + ], + "textures":[ + { + "sampler":0, + "source":0 + } + ], + "images":[ + { + "mimeType":"image/png", + "name":"material_albedo000", + "uri":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAYAAABytg0kAAAAAXNSR0IArs4c6QAAABZJREFUCJljZGBg+P+fgYGBBUIxMAAAKCAEAplLvcoAAAAASUVORK5CYII=" + } + ], + "accessors":[ + { + "bufferView":0, + "componentType":5126, + "count":4, + "max":[ + 1, + 0, + 1 + ], + "min":[ + -1, + 0, + -1 + ], + "type":"VEC3" + }, + { + "bufferView":1, + "componentType":5126, + "count":4, + "type":"VEC3" + }, + { + "bufferView":2, + "componentType":5126, + "count":4, + "type":"VEC2" + }, + { + "bufferView":3, + "componentType":5123, + "count":6, + "type":"SCALAR" + } + ], + "bufferViews":[ + { + "buffer":0, + "byteLength":48, + "byteOffset":0, + "target":34962 + }, + { + "buffer":0, + "byteLength":48, + "byteOffset":48, + "target":34962 + }, + { + "buffer":0, + "byteLength":32, + "byteOffset":96, + "target":34962 + }, + { + "buffer":0, + "byteLength":12, + "byteOffset":128, + "target":34963 + } + ], + "samplers":[ + { + "magFilter":9729, + "minFilter":9987 + } + ], + "buffers":[ + { + "byteLength":140, + "uri":"data:application/octet-stream;base64,AACAPwAAAAAAAIA/AACAvwAAAAAAAIA/AACAPwAAAAAAAIC/AACAvwAAAAAAAIC/AAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAACAPwAAgD8AAAAAAACAPwAAgD8AAAAAAAAAAAAAAAACAAEAAAACAAMAAQA=" + } + ] +} diff --git a/modules/gltf/tests/data/gltf_placed_in_dot_godot_imported/gltf_placed_in_dot_godot_imported.gltf b/modules/gltf/tests/data/gltf_placed_in_dot_godot_imported/gltf_placed_in_dot_godot_imported.gltf new file mode 100644 index 000000000000..3c03102eea36 --- /dev/null +++ b/modules/gltf/tests/data/gltf_placed_in_dot_godot_imported/gltf_placed_in_dot_godot_imported.gltf @@ -0,0 +1,147 @@ +{ + "asset":{ + "generator":"Khronos glTF Blender I/O v4.2.70", + "version":"2.0" + }, + "scene":0, + "scenes":[ + { + "name":"Scene", + "nodes":[ + 1 + ] + } + ], + "nodes":[ + { + "mesh":0, + "name":"mesh_instance_3d" + }, + { + "children":[ + 0 + ], + "name":"_Node3D_6" + } + ], + "materials":[ + { + "name":"material", + "pbrMetallicRoughness":{ + "baseColorFactor":[ + 0.9999998807907104, + 0.9999998807907104, + 0.9999998807907104, + 1 + ], + "baseColorTexture":{ + "index":0 + }, + "metallicFactor":0 + } + } + ], + "meshes":[ + { + "name":"Mesh_0", + "primitives":[ + { + "attributes":{ + "POSITION":0, + "NORMAL":1, + "TEXCOORD_0":2 + }, + "indices":3, + "material":0 + } + ] + } + ], + "textures":[ + { + "sampler":0, + "source":0 + } + ], + "images":[ + { + "mimeType":"image/png", + "name":"material_albedo000", + "uri":"texture.png", + } + ], + "accessors":[ + { + "bufferView":0, + "componentType":5126, + "count":4, + "max":[ + 1, + 0, + 1 + ], + "min":[ + -1, + 0, + -1 + ], + "type":"VEC3" + }, + { + "bufferView":1, + "componentType":5126, + "count":4, + "type":"VEC3" + }, + { + "bufferView":2, + "componentType":5126, + "count":4, + "type":"VEC2" + }, + { + "bufferView":3, + "componentType":5123, + "count":6, + "type":"SCALAR" + } + ], + "bufferViews":[ + { + "buffer":0, + "byteLength":48, + "byteOffset":0, + "target":34962 + }, + { + "buffer":0, + "byteLength":48, + "byteOffset":48, + "target":34962 + }, + { + "buffer":0, + "byteLength":32, + "byteOffset":96, + "target":34962 + }, + { + "buffer":0, + "byteLength":12, + "byteOffset":128, + "target":34963 + } + ], + "samplers":[ + { + "magFilter":9729, + "minFilter":9987 + } + ], + "buffers":[ + { + "byteLength":140, + "uri":"data:application/octet-stream;base64,AACAPwAAAAAAAIA/AACAvwAAAAAAAIA/AACAPwAAAAAAAIC/AACAvwAAAAAAAIC/AAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAACAPwAAgD8AAAAAAACAPwAAgD8AAAAAAAAAAAAAAAACAAEAAAACAAMAAQA=" + } + ] +} diff --git a/modules/gltf/tests/data/gltf_placed_in_dot_godot_imported/texture.png b/modules/gltf/tests/data/gltf_placed_in_dot_godot_imported/texture.png new file mode 100644 index 0000000000000000000000000000000000000000..cb872e7419b7904df8cc02fa9fb7f84a8885bb0a GIT binary patch literal 92 zcmeAS@N?(olHy`uVBq!ia0vp^Od!m`1|*BN@u~nR#^NA%Cx&(BWL^R}VxBIJAsjQ4 nQxXz>{GZ?0*vRT+Xu!arp}@j4(|hkJpb7?0S3j3^P6 import_scene = memnew(ResourceImporterScene("PackedScene", true)); + ResourceFormatImporter::get_singleton()->add_importer(import_scene); + Ref import_gltf; + import_gltf.instantiate(); + ResourceImporterScene::add_scene_importer(import_gltf); + + // Support processing png files in editor import. + Ref import_texture = memnew(ResourceImporterTexture(true)); + ResourceFormatImporter::get_singleton()->add_importer(import_texture); + + // Once editor import convert pngs to ctex, we will need to load it as ctex resource. + Ref resource_loader_stream_texture; + resource_loader_stream_texture.instantiate(); + ResourceLoader::add_resource_format_loader(resource_loader_stream_texture); + + HashMap options(21); + options["nodes/root_type"] = ""; + options["nodes/root_name"] = ""; + options["nodes/apply_root_scale"] = true; + options["nodes/root_scale"] = 1.0; + options["meshes/ensure_tangents"] = true; + options["meshes/generate_lods"] = false; + options["meshes/create_shadow_meshes"] = true; + options["meshes/light_baking"] = 1; + options["meshes/lightmap_texel_size"] = 0.2; + options["meshes/force_disable_compression"] = false; + options["skins/use_named_skins"] = true; + options["animation/import"] = true; + options["animation/fps"] = 30; + options["animation/trimming"] = false; + options["animation/remove_immutable_tracks"] = true; + options["import_script/path"] = ""; + options["extract_path"] = "res://"; + options["_subresources"] = Dictionary(); + options["gltf/naming_version"] = 1; + + // Process gltf file, note that this generates `.scn` resource from the 2nd argument. + String scene_file = "res://" + file.get_file().get_basename(); + Error err = import_scene->import(0, file, scene_file, options, nullptr, nullptr, nullptr); + CHECK_MESSAGE(err == OK, "GLTF import failed."); + + Ref packed_scene = ResourceLoader::load(scene_file + ".scn", "", ResourceFormatLoader::CACHE_MODE_REPLACE, &err); + CHECK_MESSAGE(err == OK, "Loading scene failed."); + Node *p_scene = packed_scene->instantiate(); + + ResourceImporterScene::remove_scene_importer(import_gltf); + ResourceFormatImporter::get_singleton()->remove_importer(import_texture); + ResourceLoader::remove_resource_format_loader(resource_loader_stream_texture); + return p_scene; +} + +static Node *gltf_export_then_import(Node *p_root, String test_name) { + String tempfile = TestUtils::get_temp_path(test_name); + + Ref doc; + doc.instantiate(); + Ref state; + state.instantiate(); + Error err = doc->append_from_scene(p_root, state, EditorSceneFormatImporter::IMPORT_USE_NAMED_SKIN_BINDS); + CHECK_MESSAGE(err == OK, "GLTF state generation failed."); + + err = doc->write_to_filesystem(state, tempfile + ".gltf"); + CHECK_MESSAGE(err == OK, "Writing GLTF to cache dir failed."); + + return gltf_import(tempfile + ".gltf"); +} + +void init(const String &p_test, String copy_target = String()) { + Error err; + + // Setup project settings since it's needed for the import process + String project_folder = TestUtils::get_temp_path(p_test.get_file().get_basename()); + Ref da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); + da->make_dir_recursive(project_folder.path_join(".godot").path_join("imported")); + // Initialize res:// to `project_folder` + TestProjectSettingsInternalsAccessor::resource_path() = project_folder; + err = ProjectSettings::get_singleton()->setup(project_folder, String(), true); + + if (copy_target.is_empty()) { + return; + } + + // Copy all the necessary test data files to the res:// directory + da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); + String test_data = String("modules/gltf/tests/data/").path_join(p_test); + da = DirAccess::open(test_data); + CHECK_MESSAGE(!da.is_null(), "Unable to open folder"); + da->list_dir_begin(); + for (String item = da->get_next(); !item.is_empty(); item = da->get_next()) { + if (!FileAccess::exists(test_data.path_join(item))) { + continue; + } + Ref output = FileAccess::open(copy_target.path_join(item), FileAccess::WRITE, &err); + CHECK_MESSAGE(err == OK, "Unable to open output file"); + output->store_buffer(FileAccess::get_file_as_bytes(test_data.path_join(item))); + output->close(); + } + da->list_dir_end(); +} + +} //namespace TestGltf + +#endif // TOOLS_ENABLED + +#endif // TEST_GLTF_H diff --git a/modules/gltf/tests/test_gltf_extras.h b/modules/gltf/tests/test_gltf_extras.h index 73ef02e9f1b4..84a4dff76b96 100644 --- a/modules/gltf/tests/test_gltf_extras.h +++ b/modules/gltf/tests/test_gltf_extras.h @@ -31,6 +31,7 @@ #ifndef TEST_GLTF_EXTRAS_H #define TEST_GLTF_EXTRAS_H +#include "test_gltf.h" #include "tests/test_macros.h" #ifdef TOOLS_ENABLED @@ -47,61 +48,10 @@ #include "scene/resources/material.h" #include "scene/resources/packed_scene.h" -namespace TestGltfExtras { - -static Node *_gltf_export_then_import(Node *p_root, String &p_tempfilebase) { - Ref doc; - doc.instantiate(); - Ref state; - state.instantiate(); - Error err = doc->append_from_scene(p_root, state, EditorSceneFormatImporter::IMPORT_USE_NAMED_SKIN_BINDS); - CHECK_MESSAGE(err == OK, "GLTF state generation failed."); - err = doc->write_to_filesystem(state, p_tempfilebase + ".gltf"); - CHECK_MESSAGE(err == OK, "Writing GLTF to cache dir failed."); - - // Setting up importers. - Ref import_scene = memnew(ResourceImporterScene("PackedScene", true)); - ResourceFormatImporter::get_singleton()->add_importer(import_scene); - Ref import_gltf; - import_gltf.instantiate(); - ResourceImporterScene::add_scene_importer(import_gltf); - - // GTLF importer behaves differently outside of editor, it's too late to modify Engine::get_editor_hint - // as the registration of runtime extensions already happened, so remove them. See modules/gltf/register_types.cpp - GLTFDocument::unregister_all_gltf_document_extensions(); - - HashMap options(20); - options["nodes/root_type"] = ""; - options["nodes/root_name"] = ""; - options["nodes/apply_root_scale"] = true; - options["nodes/root_scale"] = 1.0; - options["meshes/ensure_tangents"] = true; - options["meshes/generate_lods"] = false; - options["meshes/create_shadow_meshes"] = true; - options["meshes/light_baking"] = 1; - options["meshes/lightmap_texel_size"] = 0.2; - options["meshes/force_disable_compression"] = false; - options["skins/use_named_skins"] = true; - options["animation/import"] = true; - options["animation/fps"] = 30; - options["animation/trimming"] = false; - options["animation/remove_immutable_tracks"] = true; - options["import_script/path"] = ""; - options["_subresources"] = Dictionary(); - options["gltf/naming_version"] = 1; - - // Process gltf file, note that this generates `.scn` resource from the 2nd argument. - err = import_scene->import(0, p_tempfilebase + ".gltf", p_tempfilebase, options, nullptr, nullptr, nullptr); - CHECK_MESSAGE(err == OK, "GLTF import failed."); - ResourceImporterScene::remove_scene_importer(import_gltf); - - Ref packed_scene = ResourceLoader::load(p_tempfilebase + ".scn", "", ResourceFormatLoader::CACHE_MODE_REPLACE, &err); - CHECK_MESSAGE(err == OK, "Loading scene failed."); - Node *p_scene = packed_scene->instantiate(); - return p_scene; -} +namespace TestGltf { TEST_CASE("[SceneTree][Node] GLTF test mesh and material meta export and import") { + init("gltf_mesh_material_extras"); // Setup scene. Ref original_material = memnew(StandardMaterial3D); original_material->set_albedo(Color(1.0, .0, .0)); @@ -133,9 +83,11 @@ TEST_CASE("[SceneTree][Node] GLTF test mesh and material meta export and import" original->set_meta("extras", node_dict); original->set_meta("meta_not_nested_under_extras", "should not propagate"); + original->set_owner(SceneTree::get_singleton()->get_root()); + original_mesh_instance->set_owner(SceneTree::get_singleton()->get_root()); + // Convert to GLFT and back. - String tempfile = OS::get_singleton()->get_cache_path().path_join("gltf_extras"); - Node *loaded = _gltf_export_then_import(original, tempfile); + Node *loaded = gltf_export_then_import(original, "gltf_extras"); // Compare the results. CHECK(loaded->get_name() == "node3d"); @@ -161,6 +113,7 @@ TEST_CASE("[SceneTree][Node] GLTF test mesh and material meta export and import" } TEST_CASE("[SceneTree][Node] GLTF test skeleton and bone export and import") { + init("gltf_skeleton_extras"); // Setup scene. Skeleton3D *skeleton = memnew(Skeleton3D); skeleton->set_name("skeleton"); @@ -189,18 +142,20 @@ TEST_CASE("[SceneTree][Node] GLTF test skeleton and bone export and import") { mesh->set_mesh(meshdata); mesh->set_name("mesh_instance_3d"); - Node3D *scene = memnew(Node3D); - SceneTree::get_singleton()->get_root()->add_child(scene); - scene->add_child(skeleton); - scene->add_child(mesh); - scene->set_name("node3d"); + Node3D *original = memnew(Node3D); + SceneTree::get_singleton()->get_root()->add_child(original); + original->add_child(skeleton); + original->add_child(mesh); + original->set_name("node3d"); // Now that both skeleton and mesh are part of scene, link them. mesh->set_skeleton_path(mesh->get_path_to(skeleton)); + mesh->set_owner(SceneTree::get_singleton()->get_root()); + original->set_owner(SceneTree::get_singleton()->get_root()); + // Convert to GLFT and back. - String tempfile = OS::get_singleton()->get_cache_path().path_join("gltf_bone_extras"); - Node *loaded = _gltf_export_then_import(scene, tempfile); + Node *loaded = gltf_export_then_import(original, "gltf_bone_extras"); // Compare the results. CHECK(loaded->get_name() == "node3d"); @@ -212,10 +167,10 @@ TEST_CASE("[SceneTree][Node] GLTF test skeleton and bone export and import") { memdelete(skeleton); memdelete(mesh); - memdelete(scene); + memdelete(original); memdelete(loaded); } -} // namespace TestGltfExtras +} //namespace TestGltf #endif // TOOLS_ENABLED diff --git a/modules/gltf/tests/test_gltf_images.h b/modules/gltf/tests/test_gltf_images.h new file mode 100644 index 000000000000..f66d98f2410c --- /dev/null +++ b/modules/gltf/tests/test_gltf_images.h @@ -0,0 +1,147 @@ +/**************************************************************************/ +/* test_gltf_images.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 TEST_GLTF_IMAGES_H +#define TEST_GLTF_IMAGES_H + +#include "test_gltf.h" +#include "tests/test_macros.h" + +#ifdef TOOLS_ENABLED + +#include "core/os/os.h" +#include "editor/editor_file_system.h" +#include "editor/editor_paths.h" +#include "scene/3d/mesh_instance_3d.h" +#include "scene/3d/skeleton_3d.h" +#include "scene/resources/3d/primitive_meshes.h" +#include "scene/resources/image_texture.h" +#include "scene/resources/material.h" + +namespace TestGltf { +Ref _check_texture(Node *node) { + MeshInstance3D *mesh_instance_3d = Object::cast_to(node->find_child("mesh_instance_3d", true, true)); + StandardMaterial3D *material = Object::cast_to(mesh_instance_3d->get_active_material(0).ptr()); + Ref texture = material->get_texture(StandardMaterial3D::TextureParam::TEXTURE_ALBEDO); + + CHECK_MESSAGE(texture->get_size().x == 2, "Texture width not correct."); + CHECK_MESSAGE(texture->get_size().y == 2, "Texture height not correct."); + + // Check if the loaded texture pixels are exactly as we expect. + for (int x = 0; x < 2; ++x) { + for (int y = 0; y < 2; ++y) { + Color c = texture->get_image()->get_pixel(x, y); + CHECK_MESSAGE(c == Color(x, y, y), "Texture content is incorrect"); + } + } + return texture; +} + +TEST_CASE("[SceneTree][Node] Export GLTF with external texture and import") { + init("gltf_images_external_export_import"); + // Setup scene. + Ref original_texture = memnew(ImageTexture); + Ref image = memnew(Image); + image.instantiate(); + image->initialize_data(2, 2, false, Image::FORMAT_RGBA8); + for (int x = 0; x < 2; ++x) { + for (int y = 0; y < 2; ++y) { + image->set_pixel(x, y, Color(x, y, y)); + } + } + + original_texture->set_image(image); + + Ref original_material = memnew(StandardMaterial3D); + original_material->set_texture(StandardMaterial3D::TextureParam::TEXTURE_ALBEDO, original_texture); + original_material->set_name("material"); + + Ref original_meshdata = memnew(PlaneMesh); + original_meshdata->set_name("planemesh"); + original_meshdata->surface_set_material(0, original_material); + + MeshInstance3D *original_mesh_instance = memnew(MeshInstance3D); + original_mesh_instance->set_mesh(original_meshdata); + original_mesh_instance->set_name("mesh_instance_3d"); + + Node3D *original = memnew(Node3D); + SceneTree::get_singleton()->get_root()->add_child(original); + original->add_child(original_mesh_instance); + original->set_owner(SceneTree::get_singleton()->get_root()); + original_mesh_instance->set_owner(SceneTree::get_singleton()->get_root()); + + // Convert to GLFT and back. + Node *loaded = gltf_export_then_import(original, "gltf_images"); + _check_texture(loaded); + + memdelete(original_mesh_instance); + memdelete(original); + memdelete(loaded); +} + +TEST_CASE("[SceneTree][Node][Editor] Import GLTF from .godot/imported folder with external texture") { + init("gltf_placed_in_dot_godot_imported", "res://.godot/imported"); + + EditorFileSystem *efs = memnew(EditorFileSystem); + EditorResourcePreview *erp = memnew(EditorResourcePreview); + + Node *loaded = gltf_import("res://.godot/imported/gltf_placed_in_dot_godot_imported.gltf"); + Ref texture = _check_texture(loaded); + + // In-editor imports of gltf and texture from .godot/imported folder should end up in res:// if extract_path is defined + CHECK_MESSAGE(texture->get_path() == "res://gltf_placed_in_dot_godot_imported_material_albedo000.png", "Texture not parsed as resource."); + + memdelete(loaded); + memdelete(erp); + memdelete(efs); +} + +TEST_CASE("[SceneTree][Node][Editor] Import GLTF with embedded texture, check how it got extracted") { + init("gltf_embedded_texture", "res://"); + + EditorFileSystem *efs = memnew(EditorFileSystem); + EditorResourcePreview *erp = memnew(EditorResourcePreview); + + Node *loaded = gltf_import("res://embedded_texture.gltf"); + Ref texture = _check_texture(loaded); + + // In-editor imports of texture embedded in file should end up with a resource. + CHECK_MESSAGE(texture->get_path() == "res://embedded_texture_material_albedo000.png", "Texture not parsed as resource."); + + memdelete(loaded); + memdelete(erp); + memdelete(efs); +} + +} //namespace TestGltf + +#endif // TOOLS_ENABLED + +#endif // TEST_GLTF_IMAGES_H