1
0
mirror of https://github.com/godotengine/godot.git synced 2025-11-04 12:00:25 +00:00

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.
This commit is contained in:
demolke
2024-11-29 21:33:00 +01:00
parent 143d8c87bb
commit e649e7e3c5
11 changed files with 845 additions and 93 deletions

View File

@@ -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) { 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); 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"]); state->set_bake_fps(p_options["animation/fps"]);
Error err = gltf->append_from_file(p_path, state, p_flags); Error err = gltf->append_from_file(p_path, state, p_flags);
if (err != OK) { if (err != OK) {

View File

@@ -3932,7 +3932,7 @@ Ref<Image> GLTFDocument::_parse_image_bytes_into_image(Ref<GLTFState> p_state, c
return r_image; 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); GLTFState::GLTFHandleBinary handling = GLTFState::GLTFHandleBinary(p_state->handle_binary_image);
if (p_image->is_empty() || handling == GLTFState::GLTFHandleBinary::HANDLE_BINARY_DISCARD_TEXTURES) { if (p_image->is_empty() || handling == GLTFState::GLTFHandleBinary::HANDLE_BINARY_DISCARD_TEXTURES) {
p_state->images.push_back(Ref<Texture2D>()); p_state->images.push_back(Ref<Texture2D>());
@@ -3950,10 +3950,19 @@ 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)); 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)); 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(); Vector<uint8_t> img_data = p_image->get_data();
Dictionary generator_parameters; Dictionary generator_parameters;
String file_path = p_state->get_extract_path().path_join(p_state->get_extract_prefix() + "_" + p_image->get_name()); 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; file_path += p_file_extension.is_empty() ? ".png" : p_file_extension;
if (FileAccess::exists(file_path + ".import")) { if (FileAccess::exists(file_path + ".import")) {
Ref<ConfigFile> config; Ref<ConfigFile> config;
@@ -3963,20 +3972,24 @@ void GLTFDocument::_parse_image_save_image(Ref<GLTFState> p_state, const Vector<
generator_parameters = (Dictionary)config->get_value("remap", "generator_parameters"); generator_parameters = (Dictionary)config->get_value("remap", "generator_parameters");
} }
if (!generator_parameters.has("md5")) { if (!generator_parameters.has("md5")) {
must_import = false; // Didn't come from a gltf document; don't overwrite. 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"]; String existing_md5 = generator_parameters["md5"];
unsigned char md5_hash[16]; unsigned char md5_hash[16];
CryptoCore::md5(img_data.ptr(), img_data.size(), md5_hash); CryptoCore::md5(img_data.ptr(), img_data.size(), md5_hash);
String new_md5 = String::hex_encode_buffer(md5_hash, 16); String new_md5 = String::hex_encode_buffer(md5_hash, 16);
generator_parameters["md5"] = new_md5; generator_parameters["md5"] = new_md5;
if (new_md5 == existing_md5) { if (new_md5 == existing_md5) {
must_write = false;
must_import = false; must_import = false;
} }
} }
if (must_import) { if (must_write) {
Error err = OK; Error err = OK;
if (p_file_extension.is_empty()) { if (p_file_extension.is_empty()) {
// If a file extension was not specified, save the image data to a PNG file. // If a file extension was not specified, save the image data to a PNG file.
@@ -3989,10 +4002,13 @@ void GLTFDocument::_parse_image_save_image(Ref<GLTFState> p_state, const Vector<
file->store_buffer(p_bytes); file->store_buffer(p_bytes);
file->close(); 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. // 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; HashMap<StringName, Variant> custom_options;
custom_options[SNAME("mipmaps/generate")] = true; custom_options[SNAME("mipmaps/generate")] = true;
// Will only use project settings defaults if custom_importer is empty. // Will only use project settings defaults if custom_importer is empty.
EditorFileSystem::get_singleton()->update_file(file_path); EditorFileSystem::get_singleton()->update_file(file_path);
EditorFileSystem::get_singleton()->reimport_append(file_path, custom_options, String(), generator_parameters); EditorFileSystem::get_singleton()->reimport_append(file_path, custom_options, String(), generator_parameters);
} }
@@ -4002,7 +4018,7 @@ void GLTFDocument::_parse_image_save_image(Ref<GLTFState> p_state, const Vector<
p_state->source_images.push_back(saved_image->get_image()); p_state->source_images.push_back(saved_image->get_image());
return; return;
} else { } 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));
} }
} }
} }
@@ -4070,6 +4086,9 @@ Error GLTFDocument::_parse_images(Ref<GLTFState> p_state, const String &p_base_p
while (used_names.has(image_name)) { while (used_names.has(image_name)) {
image_name += "_" + itos(i); image_name += "_" + itos(i);
} }
String resource_uri;
used_names.insert(image_name); used_names.insert(image_name);
// Load the image data. If we get a byte array, store here for later. // Load the image data. If we get a byte array, store here for later.
Vector<uint8_t> data; Vector<uint8_t> data;
@@ -4087,14 +4106,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); ERR_FAIL_COND_V(p_base_path.is_empty(), ERR_INVALID_PARAMETER);
uri = uri.uri_decode(); uri = uri.uri_decode();
uri = p_base_path.path_join(uri).replace("\\", "/"); // Fix for Windows. 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. resource_uri = uri.simplify_path();
if (!p_base_path.begins_with("res://.godot/imported")) {
// ResourceLoader will rely on the file extension to use the relevant loader. // 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. // 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 // 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 // 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. // the material), so we only do that only as fallback.
Ref<Texture2D> texture = ResourceLoader::load(uri, "Texture2D"); if (ResourceLoader::exists(resource_uri)) {
Ref<Texture2D> texture = ResourceLoader::load(resource_uri, "Texture2D");
if (texture.is_valid()) { if (texture.is_valid()) {
p_state->images.push_back(texture); p_state->images.push_back(texture);
p_state->source_images.push_back(texture->get_image()); p_state->source_images.push_back(texture->get_image());
@@ -4105,13 +4124,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 // If the mimeType does not match with the file extension, either it should be
// specified in the file, or the GLTFDocumentExtension should handle it. // specified in the file, or the GLTFDocumentExtension should handle it.
if (mime_type.is_empty()) { 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 // Fallback to loading as byte array. This enables us to support the
// spec's requirement that we honor mimetype regardless of file URI. // 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) { 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->images.push_back(Ref<Texture2D>()); // Placeholder to keep count.
p_state->source_images.push_back(Ref<Image>()); p_state->source_images.push_back(Ref<Image>());
continue; continue;
@@ -4141,7 +4160,7 @@ Error GLTFDocument::_parse_images(Ref<GLTFState> p_state, const String &p_base_p
String file_extension; String file_extension;
Ref<Image> img = _parse_image_bytes_into_image(p_state, data, mime_type, i, file_extension); Ref<Image> img = _parse_image_bytes_into_image(p_state, data, mime_type, i, file_extension);
img->set_name(image_name); 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())); print_verbose("glTF: Total images: " + itos(p_state->images.size()));

View File

@@ -190,7 +190,7 @@ private:
Error _serialize_images(Ref<GLTFState> p_state); Error _serialize_images(Ref<GLTFState> p_state);
Error _serialize_lights(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); 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_images(Ref<GLTFState> p_state, const String &p_base_path);
Error _parse_textures(Ref<GLTFState> p_state); Error _parse_textures(Ref<GLTFState> p_state);
Error _parse_texture_samplers(Ref<GLTFState> p_state); Error _parse_texture_samplers(Ref<GLTFState> p_state);

View File

@@ -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="
}
]
}

View File

@@ -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="
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 B

View File

@@ -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="
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 B

View File

@@ -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

View File

@@ -31,6 +31,7 @@
#ifndef TEST_GLTF_EXTRAS_H #ifndef TEST_GLTF_EXTRAS_H
#define TEST_GLTF_EXTRAS_H #define TEST_GLTF_EXTRAS_H
#include "test_gltf.h"
#include "tests/test_macros.h" #include "tests/test_macros.h"
#ifdef TOOLS_ENABLED #ifdef TOOLS_ENABLED
@@ -47,61 +48,10 @@
#include "scene/resources/material.h" #include "scene/resources/material.h"
#include "scene/resources/packed_scene.h" #include "scene/resources/packed_scene.h"
namespace TestGltfExtras { namespace TestGltf {
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;
}
TEST_CASE("[SceneTree][Node] GLTF test mesh and material meta export and import") { TEST_CASE("[SceneTree][Node] GLTF test mesh and material meta export and import") {
init("gltf_mesh_material_extras");
// Setup scene. // Setup scene.
Ref<StandardMaterial3D> original_material = memnew(StandardMaterial3D); Ref<StandardMaterial3D> original_material = memnew(StandardMaterial3D);
original_material->set_albedo(Color(1.0, .0, .0)); 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("extras", node_dict);
original->set_meta("meta_not_nested_under_extras", "should not propagate"); 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. // Convert to GLFT and back.
String tempfile = OS::get_singleton()->get_cache_path().path_join("gltf_extras"); Node *loaded = gltf_export_then_import(original, "gltf_extras");
Node *loaded = _gltf_export_then_import(original, tempfile);
// Compare the results. // Compare the results.
CHECK(loaded->get_name() == "node3d"); 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") { TEST_CASE("[SceneTree][Node] GLTF test skeleton and bone export and import") {
init("gltf_skeleton_extras");
// Setup scene. // Setup scene.
Skeleton3D *skeleton = memnew(Skeleton3D); Skeleton3D *skeleton = memnew(Skeleton3D);
skeleton->set_name("skeleton"); 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_mesh(meshdata);
mesh->set_name("mesh_instance_3d"); mesh->set_name("mesh_instance_3d");
Node3D *scene = memnew(Node3D); Node3D *original = memnew(Node3D);
SceneTree::get_singleton()->get_root()->add_child(scene); SceneTree::get_singleton()->get_root()->add_child(original);
scene->add_child(skeleton); original->add_child(skeleton);
scene->add_child(mesh); original->add_child(mesh);
scene->set_name("node3d"); original->set_name("node3d");
// Now that both skeleton and mesh are part of scene, link them. // Now that both skeleton and mesh are part of scene, link them.
mesh->set_skeleton_path(mesh->get_path_to(skeleton)); 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. // Convert to GLFT and back.
String tempfile = OS::get_singleton()->get_cache_path().path_join("gltf_bone_extras"); Node *loaded = gltf_export_then_import(original, "gltf_bone_extras");
Node *loaded = _gltf_export_then_import(scene, tempfile);
// Compare the results. // Compare the results.
CHECK(loaded->get_name() == "node3d"); CHECK(loaded->get_name() == "node3d");
@@ -212,10 +167,10 @@ TEST_CASE("[SceneTree][Node] GLTF test skeleton and bone export and import") {
memdelete(skeleton); memdelete(skeleton);
memdelete(mesh); memdelete(mesh);
memdelete(scene); memdelete(original);
memdelete(loaded); memdelete(loaded);
} }
} // namespace TestGltfExtras } //namespace TestGltf
#endif // TOOLS_ENABLED #endif // TOOLS_ENABLED

View File

@@ -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