Skip to content

Commit

Permalink
GLTF: Don't duplicate textures when importing blend files
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
demolke committed Nov 29, 2024
1 parent e1ae782 commit d8d6cae
Show file tree
Hide file tree
Showing 9 changed files with 674 additions and 93 deletions.
3 changes: 3 additions & 0 deletions modules/gltf/editor/editor_scene_importer_gltf.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
75 changes: 47 additions & 28 deletions modules/gltf/gltf_document.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3931,7 +3931,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>());
Expand All @@ -3949,33 +3949,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 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<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.
Expand All @@ -3988,10 +4001,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);
}
Expand All @@ -4001,7 +4017,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));
}
}
}
Expand Down Expand Up @@ -4069,6 +4085,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;
Expand All @@ -4086,14 +4105,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());
Expand All @@ -4104,13 +4123,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;
Expand Down Expand Up @@ -4140,7 +4159,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()));
Expand Down
2 changes: 1 addition & 1 deletion modules/gltf/gltf_document.h
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ class GLTFDocument : public Resource {
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);
Expand Down
147 changes: 147 additions & 0 deletions modules/gltf/tests/data/gltf_embedded_texture/embedded_texture.gltf
Original file line number Diff line number Diff line change
@@ -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="
}
]
}
Loading

0 comments on commit d8d6cae

Please sign in to comment.