Selaa lähdekoodia

Merge pull request #98909 from demolke/master

GLTF: Don't duplicate textures when importing blend files
Rémi Verschelde 8 kuukautta sitten
vanhempi
commit
ba8a155551

+ 0 - 1
editor/editor_file_system.cpp

@@ -3216,7 +3216,6 @@ void EditorFileSystem::reimport_files(const Vector<String> &p_files) {
 }
 
 Error EditorFileSystem::reimport_append(const String &p_file, const HashMap<StringName, Variant> &p_custom_options, const String &p_custom_importer, Variant p_generator_parameters) {
-	ERR_FAIL_COND_V_MSG(!importing, ERR_INVALID_PARAMETER, "Can only append files to import during a current reimport process.");
 	Vector<String> reloads;
 	reloads.append(p_file);
 

+ 1 - 0
editor/import/editor_import_plugin.cpp

@@ -214,6 +214,7 @@ Error EditorImportPlugin::_append_import_external_resource(const String &p_file,
 }
 
 Error EditorImportPlugin::append_import_external_resource(const String &p_file, const HashMap<StringName, Variant> &p_custom_options, const String &p_custom_importer, Variant p_generator_parameters) {
+	ERR_FAIL_COND_V_MSG(!EditorFileSystem::get_singleton()->is_importing(), ERR_INVALID_PARAMETER, "Can only append files to import during a current reimport process.");
 	return EditorFileSystem::get_singleton()->reimport_append(p_file, p_custom_options, p_custom_importer, p_generator_parameters);
 }
 

+ 3 - 0
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) {

+ 47 - 28
modules/gltf/gltf_document.cpp

@@ -3934,7 +3934,7 @@ Ref<Image> GLTFDocument::_parse_image_bytes_into_image(Ref<GLTFState> p_state, c
 	return r_image;
 }
 
-void GLTFDocument::_parse_image_save_image(Ref<GLTFState> p_state, const Vector<uint8_t> &p_bytes, const String &p_file_extension, int p_index, Ref<Image> p_image) {
+void GLTFDocument::_parse_image_save_image(Ref<GLTFState> p_state, const Vector<uint8_t> &p_bytes, const String &p_resource_uri, const String &p_file_extension, 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>());
@@ -3952,33 +3952,46 @@ void GLTFDocument::_parse_image_save_image(Ref<GLTFState> 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<uint8_t> 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<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 file_path;
+			// If resource_uri is within res:// folder but outside of .godot/imported folder, use it.
+			if (!p_resource_uri.is_empty() && !p_resource_uri.begins_with("res://.godot/imported") && !p_resource_uri.begins_with("res://..")) {
+				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<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_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.
@@ -3991,10 +4004,13 @@ void GLTFDocument::_parse_image_save_image(Ref<GLTFState> 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<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);
 			}
@@ -4004,7 +4020,7 @@ void GLTFDocument::_parse_image_save_image(Ref<GLTFState> 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));
 			}
 		}
 	}
@@ -4072,6 +4088,9 @@ Error GLTFDocument::_parse_images(Ref<GLTFState> 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<uint8_t> data;
@@ -4089,14 +4108,14 @@ Error GLTFDocument::_parse_images(Ref<GLTFState> 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<Texture2D> 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<Texture2D> 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());
@@ -4107,13 +4126,13 @@ Error GLTFDocument::_parse_images(Ref<GLTFState> 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<Texture2D>()); // Placeholder to keep count.
 					p_state->source_images.push_back(Ref<Image>());
 					continue;
@@ -4143,7 +4162,7 @@ Error GLTFDocument::_parse_images(Ref<GLTFState> p_state, const String &p_base_p
 		String file_extension;
 		Ref<Image> 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()));

+ 1 - 1
modules/gltf/gltf_document.h

@@ -190,7 +190,7 @@ private:
 	Error _serialize_images(Ref<GLTFState> p_state);
 	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, String &r_file_extension);
-	void _parse_image_save_image(Ref<GLTFState> p_state, const Vector<uint8_t> &p_bytes, const String &p_file_extension, int p_index, Ref<Image> p_image);
+	void _parse_image_save_image(Ref<GLTFState> p_state, const Vector<uint8_t> &p_bytes, const String &p_resource_uri, const String &p_file_extension, 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);

+ 147 - 0
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="
+		}
+	]
+}

+ 147 - 0
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="
+		}
+	]
+}

BIN
modules/gltf/tests/data/gltf_placed_in_dot_godot_imported/texture.png


+ 147 - 0
modules/gltf/tests/data/gltf_pointing_to_texture_outside_of_res_folder/gltf_pointing_to_texture_outside_of_res_folder.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="
+		}
+	]
+}

BIN
modules/gltf/tests/data/gltf_pointing_to_texture_outside_of_res_folder/texture_source.png


+ 165 - 0
modules/gltf/tests/test_gltf.h

@@ -0,0 +1,165 @@
+/**************************************************************************/
+/*  test_gltf.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_H
+#define TEST_GLTF_H
+
+#include "tests/test_macros.h"
+
+#ifdef TOOLS_ENABLED
+
+#include "core/os/os.h"
+#include "drivers/png/image_loader_png.h"
+#include "editor/editor_resource_preview.h"
+#include "editor/import/3d/resource_importer_scene.h"
+#include "editor/import/resource_importer_texture.h"
+#include "modules/gltf/editor/editor_scene_importer_gltf.h"
+#include "modules/gltf/gltf_document.h"
+#include "modules/gltf/gltf_state.h"
+#include "scene/3d/mesh_instance_3d.h"
+#include "scene/3d/skeleton_3d.h"
+#include "scene/main/window.h"
+#include "scene/resources/3d/primitive_meshes.h"
+#include "scene/resources/compressed_texture.h"
+#include "scene/resources/material.h"
+#include "scene/resources/packed_scene.h"
+#include "tests/core/config/test_project_settings.h"
+
+namespace TestGltf {
+
+static Node *gltf_import(const String &p_file) {
+	// Setting up importers.
+	Ref<ResourceImporterScene> import_scene;
+	import_scene.instantiate("PackedScene", true);
+	ResourceFormatImporter::get_singleton()->add_importer(import_scene);
+	Ref<EditorSceneFormatImporterGLTF> import_gltf;
+	import_gltf.instantiate();
+	ResourceImporterScene::add_scene_importer(import_gltf);
+
+	// Support processing png files in editor import.
+	Ref<ResourceImporterTexture> import_texture;
+	import_texture.instantiate(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<ResourceFormatLoaderCompressedTexture2D> resource_loader_stream_texture;
+	resource_loader_stream_texture.instantiate();
+	ResourceLoader::add_resource_format_loader(resource_loader_stream_texture);
+
+	HashMap<StringName, Variant> 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://" + p_file.get_file().get_basename();
+	Error err = import_scene->import(0, p_file, scene_file, options, nullptr, nullptr, nullptr);
+	CHECK_MESSAGE(err == OK, "GLTF import failed.");
+
+	Ref<PackedScene> 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, const String &p_test_name) {
+	String tempfile = TestUtils::get_temp_path(p_test_name);
+
+	Ref<GLTFDocument> doc;
+	doc.instantiate();
+	Ref<GLTFState> 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, const String &p_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<DirAccess> 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 (p_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_valid(), "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<FileAccess> output = FileAccess::open(p_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

+ 19 - 64
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<GLTFDocument> doc;
-	doc.instantiate();
-	Ref<GLTFState> 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<ResourceImporterScene> import_scene = memnew(ResourceImporterScene("PackedScene", true));
-	ResourceFormatImporter::get_singleton()->add_importer(import_scene);
-	Ref<EditorSceneFormatImporterGLTF> 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<StringName, Variant> 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<PackedScene> 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<StandardMaterial3D> 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
 

+ 169 - 0
modules/gltf/tests/test_gltf_images.h

@@ -0,0 +1,169 @@
+/**************************************************************************/
+/*  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"
+
+#ifdef TOOLS_ENABLED
+
+#include "editor/editor_file_system.h"
+#include "editor/editor_paths.h"
+#include "scene/resources/image_texture.h"
+
+namespace TestGltf {
+Ref<Texture2D> _check_texture(Node *p_node) {
+	MeshInstance3D *mesh_instance_3d = Object::cast_to<MeshInstance3D>(p_node->find_child("mesh_instance_3d", true, true));
+	Ref<StandardMaterial3D> material = mesh_instance_3d->get_active_material(0);
+	Ref<Texture2D> 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<ImageTexture> original_texture;
+	original_texture.instantiate();
+	Ref<Image> 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<StandardMaterial3D> original_material;
+	original_material.instantiate();
+	original_material->set_texture(StandardMaterial3D::TextureParam::TEXTURE_ALBEDO, original_texture);
+	original_material->set_name("material");
+
+	Ref<PlaneMesh> original_meshdata;
+	original_meshdata.instantiate();
+	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<Texture2D> 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 texture outside of res:// directory") {
+	init("gltf_pointing_to_texture_outside_of_res_folder", "res://");
+
+	EditorFileSystem *efs = memnew(EditorFileSystem);
+	EditorResourcePreview *erp = memnew(EditorResourcePreview);
+
+	// Copy texture to the parent folder of res:// - i.e. to res://.. where we can't import from.
+	String oneup = TestUtils::get_temp_path("texture.png");
+	Error err;
+	Ref<FileAccess> output = FileAccess::open(oneup, FileAccess::WRITE, &err);
+	CHECK_MESSAGE(err == OK, "Unable to open texture file.");
+	output->store_buffer(FileAccess::get_file_as_bytes("res://texture_source.png"));
+	output->close();
+
+	Node *loaded = gltf_import("res://gltf_pointing_to_texture_outside_of_res_folder.gltf");
+	Ref<Texture2D> texture = _check_texture(loaded);
+
+	// Imports of gltf with texture from outside of res:// folder should end up being copied to res://
+	CHECK_MESSAGE(texture->get_path() == "res://gltf_pointing_to_texture_outside_of_res_folder_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<Texture2D> 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