From f2b5f5fb6b65a160129582a5ed9072ee1794e7f6 Mon Sep 17 00:00:00 2001 From: Aaron Franke Date: Sun, 21 Jun 2026 07:40:13 -0700 Subject: [PATCH 1/3] GLTF: Don't unnecessarily pass around the path internally --- modules/fbx/fbx_document.cpp | 20 ++++---- modules/fbx/fbx_document.h | 6 +-- modules/gltf/gltf_document.cpp | 91 ++++++++++++++-------------------- modules/gltf/gltf_document.h | 15 +++--- 4 files changed, 56 insertions(+), 76 deletions(-) diff --git a/modules/fbx/fbx_document.cpp b/modules/fbx/fbx_document.cpp index 167085f70e7d..23fd4704e8a5 100644 --- a/modules/fbx/fbx_document.cpp +++ b/modules/fbx/fbx_document.cpp @@ -1046,7 +1046,7 @@ GLTFImageIndex FBXDocument::_parse_image_save_image(Ref p_state, const return p_state->images.size() - 1; } -Error FBXDocument::_parse_images(Ref p_state, const String &p_base_path) { +Error FBXDocument::_parse_images(Ref p_state) { ERR_FAIL_COND_V(p_state.is_null(), ERR_INVALID_PARAMETER); const ufbx_scene *fbx_scene = p_state->scene.get(); @@ -1057,8 +1057,8 @@ Error FBXDocument::_parse_images(Ref p_state, const String &p_base_pat if (path.is_absolute_path()) { path = path.get_file(); } - if (!p_base_path.is_empty()) { - path = p_base_path.path_join(path); + if (!p_state->base_path.is_empty()) { + path = p_state->base_path.path_join(path); } path = path.simplify_path(); Vector data; @@ -2015,7 +2015,7 @@ void FBXDocument::_process_mesh_instances(Ref p_state, Node *p_scene_r } } -Error FBXDocument::_parse(Ref p_state, const String &p_path, Ref p_file) { +Error FBXDocument::_parse(Ref p_state, Ref p_file) { p_state->scene.reset(); Error err = ERR_INVALID_DATA; @@ -2106,7 +2106,7 @@ Error FBXDocument::_parse(Ref p_state, const String &p_path, Refopen_custom(p_bytes.ptr(), p_bytes.size()); state->base_path = p_base_path.get_base_dir(); - err = _parse(state, state->base_path, file_access); + err = _parse(state, file_access); ERR_FAIL_COND_V(err != OK, err); for (Ref ext : document_extensions) { ERR_CONTINUE(ext.is_null()); @@ -2180,7 +2180,7 @@ Error FBXDocument::append_from_buffer(const PackedByteArray &p_bytes, const Stri return OK; } -Error FBXDocument::_parse_fbx_state(Ref p_state, const String &p_search_path) { +Error FBXDocument::_parse_fbx_state(Ref p_state) { Error err; // Abort parsing if the scene is not loaded. @@ -2196,7 +2196,7 @@ Error FBXDocument::_parse_fbx_state(Ref p_state, const String &p_searc if (!p_state->discard_meshes_and_materials) { /* PARSE IMAGES */ - err = _parse_images(p_state, p_search_path); + err = _parse_images(p_state); ERR_FAIL_COND_V(err != OK, ERR_PARSE_ERROR); @@ -2260,7 +2260,7 @@ Error FBXDocument::append_from_file(const String &p_path, Ref p_state if (p_state == Ref()) { p_state.instantiate(); } - state->filename = p_path.get_file().get_basename(); + state->filename = p_path.get_file(); state->use_named_skin_binds = p_flags & GLTFDocument::ImportFlags::IMPORT_FLAG_USE_NAMED_SKIN_BINDS; state->discard_meshes_and_materials = p_flags & GLTFDocument::ImportFlags::IMPORT_FLAG_DISCARD_MESHES_AND_MATERIALS; Error err; @@ -2272,7 +2272,7 @@ Error FBXDocument::append_from_file(const String &p_path, Ref p_state base_path = p_path.get_base_dir(); } state->base_path = base_path; - err = _parse(p_state, base_path, file); + err = _parse(p_state, file); ERR_FAIL_COND_V(err != OK, err); for (Ref ext : document_extensions) { ERR_CONTINUE(ext.is_null()); diff --git a/modules/fbx/fbx_document.h b/modules/fbx/fbx_document.h index c2dcfd1e2f14..d8667dda724b 100644 --- a/modules/fbx/fbx_document.h +++ b/modules/fbx/fbx_document.h @@ -78,7 +78,7 @@ class FBXDocument : public GLTFDocument { Error _parse_meshes(Ref p_state); Ref _parse_image_bytes_into_image(Ref p_state, const Vector &p_bytes, const String &p_filename, int p_index); GLTFImageIndex _parse_image_save_image(Ref p_state, const Vector &p_bytes, const String &p_file_extension, int p_index, Ref p_image); - Error _parse_images(Ref p_state, const String &p_base_path); + Error _parse_images(Ref p_state); Error _parse_materials(Ref p_state); Error _parse_skins(Ref p_state); Error _parse_animations(Ref p_state); @@ -95,11 +95,11 @@ class FBXDocument : public GLTFDocument { Error _parse_lights(Ref p_state); public: - Error _parse_fbx_state(Ref p_state, const String &p_search_path); + Error _parse_fbx_state(Ref p_state); void _process_mesh_instances(Ref p_state, Node *p_scene_root); void _generate_scene_node(Ref p_state, const GLTFNodeIndex p_node_index, Node *p_scene_parent, Node *p_scene_root); void _generate_skeleton_bone_node(Ref p_state, const GLTFNodeIndex p_node_index, Node *p_scene_parent, Node *p_scene_root); void _import_animation(Ref p_state, AnimationPlayer *p_animation_player, const GLTFAnimationIndex p_index, const bool p_trimming, const bool p_remove_immutable_tracks); - Error _parse(Ref p_state, const String &p_path, Ref p_file); + Error _parse(Ref p_state, Ref p_file); }; diff --git a/modules/gltf/gltf_document.cpp b/modules/gltf/gltf_document.cpp index 9d52b01dd392..4432dc9ee0ba 100644 --- a/modules/gltf/gltf_document.cpp +++ b/modules/gltf/gltf_document.cpp @@ -242,29 +242,6 @@ Error GLTFDocument::_serialize_scenes(Ref p_state) { return OK; } -Error GLTFDocument::_parse_json(const String &p_path, Ref p_state) { - Error err; - Ref file = FileAccess::open(p_path, FileAccess::READ, &err); - if (file.is_null()) { - return err; - } - - Vector array; - array.resize(file->get_length()); - file->get_buffer(array.ptrw(), array.size()); - String text = String::utf8((const char *)array.ptr(), array.size()); - - JSON json; - err = json.parse(text); - if (err != OK) { - _err_print_error("", p_path.utf8().get_data(), json.get_error_line(), json.get_error_message().utf8().get_data(), false, ERR_HANDLER_SCRIPT); - return err; - } - p_state->json = json.get_data(); - - return OK; -} - Error GLTFDocument::_parse_glb(Ref p_file, Ref p_state) { ERR_FAIL_COND_V(p_file.is_null(), ERR_INVALID_PARAMETER); ERR_FAIL_COND_V(p_state.is_null(), ERR_INVALID_PARAMETER); @@ -701,7 +678,7 @@ static inline bool _all_buffers_empty(const Vector> &p_buffers, return true; } -Error GLTFDocument::_encode_buffer_glb(Ref p_state, const String &p_path) { +Error GLTFDocument::_encode_buffer_glb(Ref p_state) { print_verbose("glTF: Total buffers: " + itos(p_state->buffers.size())); if (p_state->buffers.is_empty() || _all_buffers_empty(p_state->buffers)) { @@ -726,16 +703,16 @@ Error GLTFDocument::_encode_buffer_glb(Ref p_state, const String &p_p } continue; } - String filename = p_path.get_basename().get_file() + itos(i) + ".bin"; - String path = p_path.get_base_dir() + "/" + filename; + const String bin_filename = p_state->filename.get_basename() + itos(i) + ".bin"; + const String bin_path = p_state->base_path.path_join(bin_filename); Error err; - Ref file = FileAccess::open(path, FileAccess::WRITE, &err); + Ref file = FileAccess::open(bin_path, FileAccess::WRITE, &err); if (file.is_null()) { return err; } file->create(FileAccess::ACCESS_RESOURCES); file->store_buffer(buffer_data.ptr(), buffer_data.size()); - gltf_buffer["uri"] = filename; + gltf_buffer["uri"] = bin_filename; gltf_buffer["byteLength"] = buffer_data.size(); buffers.push_back(gltf_buffer); } @@ -744,7 +721,7 @@ Error GLTFDocument::_encode_buffer_glb(Ref p_state, const String &p_p return OK; } -Error GLTFDocument::_encode_buffer_bins(Ref p_state, const String &p_path) { +Error GLTFDocument::_encode_buffer_bins(Ref p_state) { print_verbose("glTF: Total buffers: " + itos(p_state->buffers.size())); if (p_state->buffers.is_empty() || _all_buffers_empty(p_state->buffers)) { @@ -765,15 +742,15 @@ Error GLTFDocument::_encode_buffer_bins(Ref p_state, const String &p_ } continue; } - String filename = p_path.get_basename().get_file() + itos(i) + ".bin"; - String path = p_path.get_base_dir() + "/" + filename; + const String bin_filename = p_state->filename.get_basename() + itos(i) + ".bin"; + const String bin_path = p_state->base_path.path_join(bin_filename); Error err; - Ref file = FileAccess::open(path, FileAccess::WRITE, &err); + Ref file = FileAccess::open(bin_path, FileAccess::WRITE, &err); if (file.is_null()) { return err; } file->store_buffer(buffer_data.ptr(), buffer_data.size()); - gltf_buffer["uri"] = filename; + gltf_buffer["uri"] = bin_filename; gltf_buffer["byteLength"] = buffer_data.size(); buffers.push_back(gltf_buffer); } @@ -784,7 +761,7 @@ Error GLTFDocument::_encode_buffer_bins(Ref p_state, const String &p_ return OK; } -Error GLTFDocument::_parse_buffers(Ref p_state, const String &p_base_path) { +Error GLTFDocument::_parse_buffers(Ref p_state) { if (!p_state->json.has("buffers")) { return OK; } @@ -804,9 +781,9 @@ Error GLTFDocument::_parse_buffers(Ref p_state, const String &p_base_ } buffer_data = _parse_base64_uri(uri); } else { // Relative path to an external image file. - ERR_FAIL_COND_V(p_base_path.is_empty(), ERR_INVALID_PARAMETER); + ERR_FAIL_COND_V(p_state->base_path.is_empty(), ERR_INVALID_PARAMETER); uri = uri.uri_file_decode(); - uri = p_base_path.path_join(uri).replace_char('\\', '/'); // Fix for Windows. + uri = p_state->base_path.path_join(uri).replace_char('\\', '/'); // Fix for Windows. ERR_FAIL_COND_V_MSG(!FileAccess::exists(uri), ERR_FILE_NOT_FOUND, "glTF: Binary file not found: " + uri); buffer_data = FileAccess::get_file_as_bytes(uri); ERR_FAIL_COND_V_MSG(buffer_data.is_empty(), ERR_PARSE_ERROR, "glTF: Couldn't load binary file as an array: " + uri); @@ -2135,6 +2112,8 @@ Dictionary GLTFDocument::_serialize_image(Ref p_state, Ref p_i } image_dict["uri"] = relative_texture_dir.path_join(image_file_name).uri_encode(); } else { + // Else, such as if this is a `.glb`, a byte array in memory, or an + // embedded `.gltf`, we need to serialize the image into a buffer view. GLTFBufferViewIndex bvi; Ref bv; @@ -2325,7 +2304,7 @@ void GLTFDocument::_parse_image_save_image(Ref p_state, const Vector< p_state->source_images.push_back(p_image); } -Error GLTFDocument::_parse_images(Ref p_state, const String &p_base_path) { +Error GLTFDocument::_parse_images(Ref p_state) { ERR_FAIL_COND_V(p_state.is_null(), ERR_INVALID_PARAMETER); if (!p_state->json.has("images")) { return OK; @@ -2385,9 +2364,9 @@ Error GLTFDocument::_parse_images(Ref p_state, const String &p_base_p mime_type = uri.substr(5, uri.find(";base64") - 5); } } else { // Relative path to an external image file. - ERR_FAIL_COND_V(p_base_path.is_empty(), ERR_INVALID_PARAMETER); + ERR_FAIL_COND_V(p_state->base_path.is_empty(), ERR_INVALID_PARAMETER); uri = uri.uri_file_decode(); - uri = p_base_path.path_join(uri).replace_char('\\', '/'); // Fix for Windows. + uri = p_state->base_path.path_join(uri).replace_char('\\', '/'); // Fix for Windows. 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. @@ -6703,7 +6682,7 @@ void GLTFDocument::_convert_animation(Ref p_state, AnimationPlayer *p } } -Error GLTFDocument::_parse(Ref p_state, const String &p_path, Ref p_file) { +Error GLTFDocument::_parse(Ref p_state, Ref p_file) { Error err; if (p_file.is_null()) { return FAILED; @@ -6743,7 +6722,7 @@ Error GLTFDocument::_parse(Ref p_state, const String &p_path, Ref p_state) { return OK; } -Error GLTFDocument::_serialize_file(Ref p_state, const String p_path) { +Error GLTFDocument::_serialize_file(Ref p_state) { Error err = FAILED; - if (p_path.to_lower().ends_with("glb")) { - err = _encode_buffer_glb(p_state, p_path); + if (p_state->filename.to_lower().ends_with("glb")) { + err = _encode_buffer_glb(p_state); ERR_FAIL_COND_V(err != OK, err); - Ref file = FileAccess::open(p_path, FileAccess::WRITE, &err); + const String gltf_path = p_state->base_path.path_join(p_state->filename); + Ref file = FileAccess::open(gltf_path, FileAccess::WRITE, &err); ERR_FAIL_COND_V(file.is_null(), FAILED); constexpr uint64_t header_size = 12; @@ -6834,7 +6814,7 @@ Error GLTFDocument::_serialize_file(Ref p_state, const String p_path) // Check if the file length with the buffer is greater than glTF's maximum of 4 GiB. // If it is, we can't write the buffer into the file, but can write it separately. if (unlikely(file_length_with_buffer > (uint64_t)UINT32_MAX)) { - err = _encode_buffer_bins(p_state, p_path); + err = _encode_buffer_bins(p_state); ERR_FAIL_COND_V(err != OK, err); // Since the buffer bins were re-encoded, we need to re-convert the JSON to string. json_string = JSON::stringify(p_state->json, "", true, true); @@ -6873,9 +6853,10 @@ Error GLTFDocument::_serialize_file(Ref p_state, const String p_path) } } } else { - err = _encode_buffer_bins(p_state, p_path); + err = _encode_buffer_bins(p_state); ERR_FAIL_COND_V(err != OK, err); - Ref file = FileAccess::open(p_path, FileAccess::WRITE, &err); + const String gltf_path = p_state->base_path.path_join(p_state->filename); + Ref file = FileAccess::open(gltf_path, FileAccess::WRITE, &err); ERR_FAIL_COND_V(file.is_null(), FAILED); String json = JSON::stringify(p_state->json, "", true, true); @@ -7023,7 +7004,7 @@ HashSet GLTFDocument::get_supported_gltf_extensions_hashset() { } PackedByteArray GLTFDocument::_serialize_glb_buffer(Ref p_state, Error *r_err) { - Error err = _encode_buffer_glb(p_state, ""); + Error err = _encode_buffer_glb(p_state); if (r_err) { *r_err = err; } @@ -7138,11 +7119,11 @@ Error GLTFDocument::_parse_asset_header(Ref p_state) { return OK; } -Error GLTFDocument::_parse_gltf_state(Ref p_state, const String &p_search_path) { +Error GLTFDocument::_parse_gltf_state(Ref p_state) { Error err; /* PARSE BUFFERS */ - err = _parse_buffers(p_state, p_search_path); + err = _parse_buffers(p_state); ERR_FAIL_COND_V(err != OK, ERR_PARSE_ERROR); /* PARSE BUFFER VIEWS */ @@ -7167,7 +7148,7 @@ Error GLTFDocument::_parse_gltf_state(Ref p_state, const String &p_se if (!p_state->discard_meshes_and_materials) { /* PARSE IMAGES */ - err = _parse_images(p_state, p_search_path); + err = _parse_images(p_state); ERR_FAIL_COND_V(err != OK, ERR_PARSE_ERROR); @@ -7239,7 +7220,7 @@ Error GLTFDocument::write_to_filesystem(Ref p_state, const String &p_ p_state->set_base_path(p_path.get_base_dir()); p_state->filename = p_path.get_file(); RETURN_IF_ERROR(_serialize(p_state)); - Error err = _serialize_file(p_state, p_path); + Error err = _serialize_file(p_state); if (err != OK) { return Error::FAILED; } @@ -7355,7 +7336,7 @@ Error GLTFDocument::append_from_buffer(const PackedByteArray &p_bytes, const Str file_access.instantiate(); file_access->open_custom(p_bytes.ptr(), p_bytes.size()); p_state->set_base_path(p_base_path.get_base_dir()); - err = _parse(p_state, p_state->base_path, file_access); + err = _parse(p_state, file_access); ERR_FAIL_COND_V(err != OK, err); for (Ref ext : document_extensions) { ERR_CONTINUE(ext.is_null()); @@ -7383,7 +7364,7 @@ Error GLTFDocument::append_from_file(const String &p_path, Ref p_stat base_path = p_path.get_base_dir(); } p_state->set_base_path(base_path); - err = _parse(p_state, base_path, file); + err = _parse(p_state, file); ERR_FAIL_COND_V(err != OK, err); for (Ref ext : document_extensions) { ERR_CONTINUE(ext.is_null()); diff --git a/modules/gltf/gltf_document.h b/modules/gltf/gltf_document.h index bba726f8b263..fb67d78656fc 100644 --- a/modules/gltf/gltf_document.h +++ b/modules/gltf/gltf_document.h @@ -139,10 +139,9 @@ class GLTFDocument : public Resource { StandardMaterial3D::TextureFilter p_filter_mode, bool p_repeats); Ref _get_sampler_for_texture(Ref p_state, const GLTFTextureIndex p_texture); - Error _parse_json(const String &p_path, Ref p_state); Error _parse_glb(Ref p_file, Ref p_state); void _compute_node_heights(Ref p_state); - Error _parse_buffers(Ref p_state, const String &p_base_path); + Error _parse_buffers(Ref p_state); Error _parse_buffer_views(Ref p_state); Error _parse_accessors(Ref p_state); template @@ -164,7 +163,7 @@ class GLTFDocument : public Resource { 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_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_images(Ref p_state); Error _parse_textures(Ref p_state); Error _parse_texture_samplers(Ref p_state); Error _parse_materials(Ref p_state); @@ -206,13 +205,13 @@ class GLTFDocument : public Resource { Error _serialize_nodes(Ref p_state); Error _serialize_scenes(Ref p_state); String interpolation_to_string(const GLTFAnimation::Interpolation p_interp); - Error _encode_buffer_bins(Ref p_state, const String &p_path); - Error _encode_buffer_glb(Ref p_state, const String &p_path); + Error _encode_buffer_bins(Ref p_state); + Error _encode_buffer_glb(Ref p_state); PackedByteArray _serialize_glb_buffer(Ref p_state, Error *r_err); Dictionary _serialize_texture_transform_uv1(const Ref &p_material); Dictionary _serialize_texture_transform_uv2(const Ref &p_material); Error _serialize_asset_header(Ref p_state); - Error _serialize_file(Ref p_state, const String p_path); + Error _serialize_file(Ref p_state); Error _serialize_gltf_extensions(Ref p_state) const; public: @@ -241,7 +240,7 @@ class GLTFDocument : public Resource { virtual Error write_to_filesystem(Ref p_state, const String &p_path); public: - Error _parse_gltf_state(Ref p_state, const String &p_search_path); + Error _parse_gltf_state(Ref p_state); Error _parse_asset_header(Ref p_state); Error _parse_gltf_extensions(Ref p_state); void _process_mesh_instances(Ref p_state, Node *p_scene_root); @@ -302,7 +301,7 @@ class GLTFDocument : public Resource { void _convert_animation(Ref p_state, AnimationPlayer *p_animation_player, const String &p_animation_track_name); Error _serialize(Ref p_state); - Error _parse(Ref p_state, const String &p_path, Ref p_file); + Error _parse(Ref p_state, Ref p_file); }; VARIANT_ENUM_CAST(GLTFDocument::RootNodeMode); From f7912e5b151a7332487e5ae9d2b3db8d98c14c50 Mon Sep 17 00:00:00 2001 From: Aaron Franke Date: Thu, 25 Jun 2026 15:37:43 -0700 Subject: [PATCH 2/3] GLTF: Unify logic for encoding buffers during export --- modules/gltf/gltf_document.cpp | 157 ++++++++++++++------------------- modules/gltf/gltf_document.h | 3 +- modules/gltf/gltf_state.cpp | 4 + modules/gltf/gltf_state.h | 1 + 4 files changed, 71 insertions(+), 94 deletions(-) diff --git a/modules/gltf/gltf_document.cpp b/modules/gltf/gltf_document.cpp index 4432dc9ee0ba..a8c2a5d055af 100644 --- a/modules/gltf/gltf_document.cpp +++ b/modules/gltf/gltf_document.cpp @@ -678,86 +678,66 @@ static inline bool _all_buffers_empty(const Vector> &p_buffers, return true; } -Error GLTFDocument::_encode_buffer_glb(Ref p_state) { - print_verbose("glTF: Total buffers: " + itos(p_state->buffers.size())); - - if (p_state->buffers.is_empty() || _all_buffers_empty(p_state->buffers)) { +Error GLTFDocument::_export_encode_buffers(Ref p_state, bool p_separate_buffers_into_files) { + const int64_t buffer_count = p_state->buffers.size(); + print_verbose("glTF: Total buffers: " + itos(buffer_count)); + if (buffer_count == 0 || _all_buffers_empty(p_state->buffers)) { ERR_FAIL_COND_V_MSG(!p_state->buffer_views.is_empty(), ERR_INVALID_DATA, "glTF: Buffer views are present, but buffers are empty."); return OK; } - Array buffers; - Dictionary first_buffer; - - first_buffer["byteLength"] = p_state->buffers[0].size(); - buffers.push_back(first_buffer); - - for (GLTFBufferIndex i = 1; i < p_state->buffers.size(); i++) { - const Vector &buffer_data = p_state->buffers[i]; - Dictionary gltf_buffer; - if (buffer_data.is_empty()) { - if (i < p_state->buffers.size() - 1 && !_all_buffers_empty(p_state->buffers, i + 1)) { - // Have to push back an empty buffer to avoid changing the buffer index, even though this is against spec. - WARN_PRINT("glTF: Buffer " + itos(i) + " is empty, but there are non-empty subsequent buffers."); - gltf_buffer["byteLength"] = 0; - buffers.push_back(gltf_buffer); - } - continue; - } - const String bin_filename = p_state->filename.get_basename() + itos(i) + ".bin"; - const String bin_path = p_state->base_path.path_join(bin_filename); - Error err; - Ref file = FileAccess::open(bin_path, FileAccess::WRITE, &err); - if (file.is_null()) { - return err; - } - file->create(FileAccess::ACCESS_RESOURCES); - file->store_buffer(buffer_data.ptr(), buffer_data.size()); - gltf_buffer["uri"] = bin_filename; - gltf_buffer["byteLength"] = buffer_data.size(); - buffers.push_back(gltf_buffer); - } - p_state->json["buffers"] = buffers; - - return OK; -} - -Error GLTFDocument::_encode_buffer_bins(Ref p_state) { - print_verbose("glTF: Total buffers: " + itos(p_state->buffers.size())); - - if (p_state->buffers.is_empty() || _all_buffers_empty(p_state->buffers)) { - ERR_FAIL_COND_V_MSG(!p_state->buffer_views.is_empty(), ERR_INVALID_DATA, "glTF: Buffer views are present, but buffers are empty."); - return OK; - } - Array buffers; - - for (GLTFBufferIndex i = 0; i < p_state->buffers.size(); i++) { - const Vector &buffer_data = p_state->buffers[i]; - Dictionary gltf_buffer; - if (buffer_data.is_empty()) { - if (i < p_state->buffers.size() - 1 && !_all_buffers_empty(p_state->buffers, i + 1)) { + Array buffer_dicts; + buffer_dicts.resize(buffer_count); + // Separate buffers into files if the caller requested it and the base path is available. + const bool should_separate_into_files = p_separate_buffers_into_files && !p_state->base_path.is_empty(); + // Check if the buffer at index 0 should be used as a GLB buffer. + GLTFBufferIndex loop_start_index; + if (should_separate_into_files || p_state->is_text_file()) { + loop_start_index = 0; // Encode all buffers with URIs, don't use a GLB buffer. + } else { + loop_start_index = 1; // Encode buffers starting at index 1 with URIs, use buffer 0 as a GLB buffer. + const PackedByteArray buffer_data = p_state->buffers[0]; + const int64_t buffer_byte_length = buffer_data.size(); + if (buffer_byte_length == 0) { + // If this buffer is empty and the others were too, this would've returned at the top of the function. + ERR_PRINT("glTF export: Buffer 0 is empty, but there are non-empty subsequent buffers. Writing it anyway to preserve buffer indices."); + } + Dictionary first_gltf_buffer_dict; + first_gltf_buffer_dict["byteLength"] = buffer_byte_length; + buffer_dicts[0] = first_gltf_buffer_dict; + } + // Encode the rest of the buffers. + for (GLTFBufferIndex buffer_index = loop_start_index; buffer_index < buffer_count; buffer_index++) { + const PackedByteArray &buffer_data = p_state->buffers[buffer_index]; + const int64_t buffer_byte_length = buffer_data.size(); + Dictionary gltf_buffer_dict; + gltf_buffer_dict["byteLength"] = buffer_byte_length; + if (buffer_byte_length == 0) { + if (buffer_index < buffer_count - 1 && !_all_buffers_empty(p_state->buffers, buffer_index + 1)) { // Have to push back an empty buffer to avoid changing the buffer index, even though this is against spec. - WARN_PRINT("glTF: Buffer " + itos(i) + " is empty, but there are non-empty subsequent buffers."); - gltf_buffer["byteLength"] = 0; - buffers.push_back(gltf_buffer); + WARN_PRINT("glTF: Buffer " + itos(buffer_index) + " is empty, but there are non-empty subsequent buffers."); + buffer_dicts[buffer_index] = gltf_buffer_dict; } continue; } - const String bin_filename = p_state->filename.get_basename() + itos(i) + ".bin"; - const String bin_path = p_state->base_path.path_join(bin_filename); - Error err; - Ref file = FileAccess::open(bin_path, FileAccess::WRITE, &err); - if (file.is_null()) { - return err; + if (should_separate_into_files) { + // Encode the buffer as a separate file. + const String bin_filename = p_state->filename.get_basename() + itos(buffer_index) + ".bin"; + const String bin_path = p_state->base_path.path_join(bin_filename); + Error err; + Ref file = FileAccess::open(bin_path, FileAccess::WRITE, &err); + if (file.is_null()) { + return err; + } + file->store_buffer(buffer_data.ptr(), buffer_byte_length); + gltf_buffer_dict["uri"] = bin_filename; + } else { + // Encode the buffer as a base64 data URI. + const String base64_data = CryptoCore::b64_encode_str(buffer_data.ptr(), buffer_byte_length); + gltf_buffer_dict["uri"] = "data:application/octet-stream;base64," + base64_data; } - file->store_buffer(buffer_data.ptr(), buffer_data.size()); - gltf_buffer["uri"] = bin_filename; - gltf_buffer["byteLength"] = buffer_data.size(); - buffers.push_back(gltf_buffer); - } - if (!buffers.is_empty()) { - p_state->json["buffers"] = buffers; + buffer_dicts[buffer_index] = gltf_buffer_dict; } - + p_state->json["buffers"] = buffer_dicts; return OK; } @@ -2087,7 +2067,7 @@ Dictionary GLTFDocument::_serialize_image(Ref p_state, Ref p_i ERR_FAIL_COND_V_MSG(p_image->is_compressed(), image_dict, "glTF: Image was compressed, but could not be decompressed."); } - if (p_state->filename.to_lower().ends_with("gltf")) { + if (p_state->is_text_file()) { String relative_texture_dir = "textures"; String full_texture_dir = p_state->base_path.path_join(relative_texture_dir); Ref da = DirAccess::open(p_state->base_path); @@ -6786,14 +6766,16 @@ Error GLTFDocument::_serialize_asset_header(Ref p_state) { } Error GLTFDocument::_serialize_file(Ref p_state) { - Error err = FAILED; - if (p_state->filename.to_lower().ends_with("glb")) { - err = _encode_buffer_glb(p_state); - ERR_FAIL_COND_V(err != OK, err); - const String gltf_path = p_state->base_path.path_join(p_state->filename); - Ref file = FileAccess::open(gltf_path, FileAccess::WRITE, &err); - ERR_FAIL_COND_V(file.is_null(), FAILED); - + const String gltf_path = p_state->base_path.path_join(p_state->filename); + Error err = _export_encode_buffers(p_state, p_state->is_text_file()); + ERR_FAIL_COND_V_MSG(err != OK, err, "glTF: Failed to encode buffers for file: " + gltf_path); + Ref file = FileAccess::open(gltf_path, FileAccess::WRITE, &err); + ERR_FAIL_COND_V(file.is_null(), FAILED); + if (p_state->is_text_file()) { + String json = JSON::stringify(p_state->json, "", true, true); + file->store_string(json); + } else { + // Serialize binary glTF (GLB) file. constexpr uint64_t header_size = 12; constexpr uint64_t chunk_header_size = 8; constexpr uint32_t magic = 0x46546C67; // The byte sequence "glTF" as little-endian. @@ -6814,7 +6796,7 @@ Error GLTFDocument::_serialize_file(Ref p_state) { // Check if the file length with the buffer is greater than glTF's maximum of 4 GiB. // If it is, we can't write the buffer into the file, but can write it separately. if (unlikely(file_length_with_buffer > (uint64_t)UINT32_MAX)) { - err = _encode_buffer_bins(p_state); + err = _export_encode_buffers(p_state, true); ERR_FAIL_COND_V(err != OK, err); // Since the buffer bins were re-encoded, we need to re-convert the JSON to string. json_string = JSON::stringify(p_state->json, "", true, true); @@ -6852,15 +6834,6 @@ Error GLTFDocument::_serialize_file(Ref p_state) { file->store_8(0); } } - } else { - err = _encode_buffer_bins(p_state); - ERR_FAIL_COND_V(err != OK, err); - const String gltf_path = p_state->base_path.path_join(p_state->filename); - Ref file = FileAccess::open(gltf_path, FileAccess::WRITE, &err); - ERR_FAIL_COND_V(file.is_null(), FAILED); - - String json = JSON::stringify(p_state->json, "", true, true); - file->store_string(json); } return err; } @@ -7004,7 +6977,7 @@ HashSet GLTFDocument::get_supported_gltf_extensions_hashset() { } PackedByteArray GLTFDocument::_serialize_glb_buffer(Ref p_state, Error *r_err) { - Error err = _encode_buffer_glb(p_state); + Error err = _export_encode_buffers(p_state, false); if (r_err) { *r_err = err; } @@ -7029,7 +7002,7 @@ PackedByteArray GLTFDocument::_serialize_glb_buffer(Ref p_state, Erro total_file_length = file_length_with_buffer; } ERR_FAIL_COND_V_MSG(total_file_length > (uint64_t)UINT32_MAX, PackedByteArray(), - "glTF: File size exceeds glTF Binary's maximum of 4 GiB. Cannot serialize as a single GLB in-memory buffer."); + "glTF: File size exceeds glTF Binary's maximum of 4 GiB. Cannot serialize as a single GLB byte array in memory."); const uint32_t binary_chunk_length = binary_data_length; Ref buffer; diff --git a/modules/gltf/gltf_document.h b/modules/gltf/gltf_document.h index fb67d78656fc..125f35b53e3f 100644 --- a/modules/gltf/gltf_document.h +++ b/modules/gltf/gltf_document.h @@ -205,8 +205,7 @@ class GLTFDocument : public Resource { Error _serialize_nodes(Ref p_state); Error _serialize_scenes(Ref p_state); String interpolation_to_string(const GLTFAnimation::Interpolation p_interp); - Error _encode_buffer_bins(Ref p_state); - Error _encode_buffer_glb(Ref p_state); + Error _export_encode_buffers(Ref p_state, bool p_separate_buffers_into_files); PackedByteArray _serialize_glb_buffer(Ref p_state, Error *r_err); Dictionary _serialize_texture_transform_uv1(const Ref &p_material); Dictionary _serialize_texture_transform_uv2(const Ref &p_material); diff --git a/modules/gltf/gltf_state.cpp b/modules/gltf/gltf_state.cpp index de1ea267b014..9350cf3ade22 100644 --- a/modules/gltf/gltf_state.cpp +++ b/modules/gltf/gltf_state.cpp @@ -445,6 +445,10 @@ void GLTFState::set_filename(const String &p_filename) { } } +bool GLTFState::is_text_file() const { + return filename.to_lower().ends_with(".gltf"); +} + Variant GLTFState::get_additional_data(const StringName &p_extension_name) const { return additional_data.get(p_extension_name, Variant()); } diff --git a/modules/gltf/gltf_state.h b/modules/gltf/gltf_state.h index 39664bc88425..71ceeb4aa50b 100644 --- a/modules/gltf/gltf_state.h +++ b/modules/gltf/gltf_state.h @@ -255,6 +255,7 @@ class GLTFState : public Resource { String get_filename() const; void set_filename(const String &p_filename); + bool is_text_file() const; PackedInt32Array get_root_nodes() const; void set_root_nodes(const PackedInt32Array &p_root_nodes); From ba516442812de138a1a86bd0204392c2894abb5c Mon Sep 17 00:00:00 2001 From: Aaron Franke Date: Thu, 19 Feb 2026 12:47:17 -0800 Subject: [PATCH 3/3] GLTF: Implement the glTF 2.1 64-bit binary file format --- modules/gltf/doc_classes/GLTFDocument.xml | 12 + modules/gltf/doc_classes/GLTFState.xml | 3 +- .../editor_scene_exporter_gltf_plugin.cpp | 4 +- .../editor_scene_exporter_gltf_settings.cpp | 32 +- .../editor_scene_exporter_gltf_settings.h | 3 +- modules/gltf/gltf_document.cpp | 552 ++++++++++++++---- modules/gltf/gltf_document.h | 28 +- modules/gltf/gltf_state.cpp | 6 + modules/gltf/gltf_state.h | 4 + 9 files changed, 518 insertions(+), 126 deletions(-) diff --git a/modules/gltf/doc_classes/GLTFDocument.xml b/modules/gltf/doc_classes/GLTFDocument.xml index c1ac55c143f0..e40713a49c26 100644 --- a/modules/gltf/doc_classes/GLTFDocument.xml +++ b/modules/gltf/doc_classes/GLTFDocument.xml @@ -115,6 +115,9 @@ + + When exporting a binary glTF (.glb), this determines the binary format to use: 32-bit, 64-bit, or automatic selection between the two. This option has no effect when exporting a text glTF (.gltf). + The user-friendly name of the fallback image format. This is used when exporting the glTF file, including writing to a file and writing to a byte array. This property may only be one of "None", "PNG", or "JPEG", and is only used when the [member image_format] is not one of "None", "PNG", or "JPEG". If having multiple extension image formats is desired, that can be done using a [GLTFDocumentExtension] class - this property only covers the use case of providing a base glTF fallback image when using a custom image format. @@ -141,6 +144,15 @@ + + Use the binary glTF format with the minimum size for the file being exported. If the file being exported exceeds the limits of the 32-bit format, the 64-bit format will be used. See [constant BINARY_FORMAT_MODE_32_BIT] and [constant BINARY_FORMAT_MODE_64_BIT] for more details. + + + Use the binary glTF format with 32-bit file size and chunk sizes as defined in the glTF 2.0 specification. If the file being exported exceeds the limits of the 32-bit format, either a separate buffer will be written, or the export will fail. + + + Use the binary glTF format with 64-bit file size and chunk sizes as defined in the glTF 2.1 specification. This format is not compatible with older glTF importers, but allows for larger files to be exported. + Treat the Godot scene's root node as the root node of the glTF file, and mark it as the single root node via the [code]GODOT_single_root[/code] glTF extension. This will be parsed the same as [constant ROOT_NODE_MODE_KEEP_ROOT] if the implementation does not support [code]GODOT_single_root[/code]. diff --git a/modules/gltf/doc_classes/GLTFState.xml b/modules/gltf/doc_classes/GLTFState.xml index 236896bb8fd4..4f8381266a99 100644 --- a/modules/gltf/doc_classes/GLTFState.xml +++ b/modules/gltf/doc_classes/GLTFState.xml @@ -295,6 +295,7 @@ The folder path associated with this glTF data. This is used to find other files the glTF file references, like images or binary buffers. This will be set during import when appending from a file, and will be set during export when writing to a file. + The buffers of the glTF file's buffers array. Buffer data may be stored in separate files, in data URIs, or in the chunks of a binary glTF .glb file. The copyright string in the asset header of the glTF file. This is set during import if present and export if non-empty. See the glTF asset header documentation for more information. @@ -304,7 +305,7 @@ The file name associated with this glTF data. If it ends with [code].gltf[/code], this is text-based glTF, otherwise this is binary GLB. This will be set during import when appending from a file, and will be set during export when writing to a file. If writing to a buffer, this will be an empty string. - + The binary buffer attached to a .glb file. diff --git a/modules/gltf/editor/editor_scene_exporter_gltf_plugin.cpp b/modules/gltf/editor/editor_scene_exporter_gltf_plugin.cpp index dec4db491319..13e36c1b9e2d 100644 --- a/modules/gltf/editor/editor_scene_exporter_gltf_plugin.cpp +++ b/modules/gltf/editor/editor_scene_exporter_gltf_plugin.cpp @@ -66,7 +66,7 @@ SceneExporterGLTFPlugin::SceneExporterGLTFPlugin() { _config_dialog->connect(SceneStringName(confirmed), callable_mp(this, &SceneExporterGLTFPlugin::_export_scene_as_gltf)); _export_settings.instantiate(); - _export_settings->generate_property_list(_gltf_document); + _export_settings->generate_property_list(_gltf_document, ""); _settings_inspector = memnew(EditorInspector); _settings_inspector->set_custom_minimum_size(Size2(350, 300) * EDSCALE); _config_dialog->add_child(_settings_inspector); @@ -83,7 +83,7 @@ void SceneExporterGLTFPlugin::_popup_gltf_settings_dialog(const String &p_select Node *root = EditorNode::get_singleton()->get_tree()->get_edited_scene_root(); ERR_FAIL_NULL(root); // Generate and refresh the export settings. - _export_settings->generate_property_list(_gltf_document, root); + _export_settings->generate_property_list(_gltf_document, p_selected_path, root); _settings_inspector->edit(nullptr); _settings_inspector->edit(_export_settings.ptr()); // Show the config dialog. diff --git a/modules/gltf/editor/editor_scene_exporter_gltf_settings.cpp b/modules/gltf/editor/editor_scene_exporter_gltf_settings.cpp index dc3c6709a5f0..67644ded5fa8 100644 --- a/modules/gltf/editor/editor_scene_exporter_gltf_settings.cpp +++ b/modules/gltf/editor/editor_scene_exporter_gltf_settings.cpp @@ -59,6 +59,15 @@ bool EditorSceneExporterGLTFSettings::_set(const StringName &p_name, const Varia _document->set_fallback_image_quality(p_value); return true; } + if (p_name == StringName("binary_format_mode")) { + _document->set_binary_format_mode((GLTFDocument::BinaryFormatMode)(int64_t)p_value); + emit_signal(CoreStringName(property_list_changed)); + return true; + } + if (p_name == StringName("encoding_format")) { + _document->set_encoding_format((GLTFDocument::EncodingFormat)(int64_t)p_value); + return true; + } if (p_name == StringName("root_node_mode")) { _document->set_root_node_mode((GLTFDocument::RootNodeMode)(int64_t)p_value); return true; @@ -91,6 +100,14 @@ bool EditorSceneExporterGLTFSettings::_get(const StringName &p_name, Variant &r_ r_ret = _document->get_fallback_image_quality(); return true; } + if (p_name == StringName("binary_format_mode")) { + r_ret = _document->get_binary_format_mode(); + return true; + } + if (p_name == StringName("encoding_format")) { + r_ret = _document->get_encoding_format(); + return true; + } if (p_name == StringName("root_node_mode")) { r_ret = _document->get_root_node_mode(); return true; @@ -120,12 +137,16 @@ void EditorSceneExporterGLTFSettings::_get_property_list(List *p_l const String fallback_format = get("fallback_image_format"); prop.usage = (is_image_format_extension && fallback_format != "None") ? PROPERTY_USAGE_DEFAULT : PROPERTY_USAGE_STORAGE; } + // Only offer encoding choices if we are dealing with binary glTF, and either a non-default is selected or the user selected 64-bit mode. + if (prop.name == "encoding_format") { + prop.usage = (_document->get_encoding_format() != 0 || _document->get_binary_format_mode() == GLTFDocument::BinaryFormatMode::BINARY_FORMAT_MODE_64_BIT) ? PROPERTY_USAGE_DEFAULT : PROPERTY_USAGE_STORAGE; + } p_list->push_back(prop); } } void EditorSceneExporterGLTFSettings::_on_extension_property_list_changed() { - generate_property_list(_document); + generate_property_list(_document, _file_path); emit_signal(CoreStringName(property_list_changed)); } @@ -191,9 +212,10 @@ bool is_any_node_invisible(Node *p_node) { } // Run this before popping up the export settings, because the extensions may have changed. -void EditorSceneExporterGLTFSettings::generate_property_list(Ref p_document, Node *p_root) { +void EditorSceneExporterGLTFSettings::generate_property_list(Ref p_document, const String &p_file_path, Node *p_root) { _property_list.clear(); _document = p_document; + _file_path = p_file_path; String image_format_hint_string = "None,PNG,JPEG"; const Vector> all_extensions = GLTFDocument::get_all_gltf_document_extensions(); // If an extension allows saving images in different formats, add to the enum. @@ -212,6 +234,12 @@ void EditorSceneExporterGLTFSettings::generate_property_list(Ref p _property_list.push_back(fallback_image_format_prop); PropertyInfo fallback_image_quality_prop = PropertyInfo(Variant::FLOAT, "fallback_image_quality", PROPERTY_HINT_RANGE, "0,1,0.01"); _property_list.push_back(fallback_image_quality_prop); + if (!_file_path.ends_with(".gltf")) { + PropertyInfo binary_format_mode_prop = PropertyInfo(Variant::INT, "binary_format_mode", PROPERTY_HINT_ENUM, "Auto,32-bit,64-bit"); + _property_list.push_back(binary_format_mode_prop); + PropertyInfo encoding_format_prop = PropertyInfo(Variant::INT, "encoding_format", PROPERTY_HINT_ENUM, "Plain:0,Zstd:1685353306"); + _property_list.push_back(encoding_format_prop); + } PropertyInfo root_node_mode_prop = PropertyInfo(Variant::INT, "root_node_mode", PROPERTY_HINT_ENUM, "Single Root,Keep Root,Multi Root"); _property_list.push_back(root_node_mode_prop); // If the scene contains any non-visible nodes, show the visibility mode setting. diff --git a/modules/gltf/editor/editor_scene_exporter_gltf_settings.h b/modules/gltf/editor/editor_scene_exporter_gltf_settings.h index ed25643b2be2..13d8df73fa73 100644 --- a/modules/gltf/editor/editor_scene_exporter_gltf_settings.h +++ b/modules/gltf/editor/editor_scene_exporter_gltf_settings.h @@ -39,6 +39,7 @@ class EditorSceneExporterGLTFSettings : public RefCounted { HashMap> _config_name_to_extension_map; String _copyright; + String _file_path; double _bake_fps = 30.0; protected: @@ -52,7 +53,7 @@ class EditorSceneExporterGLTFSettings : public RefCounted { bool _get_extension_setting(const String &p_name_str, Variant &r_ret) const; public: - void generate_property_list(Ref p_document, Node *p_root = nullptr); + void generate_property_list(Ref p_document, const String &p_file_path, Node *p_root = nullptr); String get_copyright() const; void set_copyright(const String &p_copyright); diff --git a/modules/gltf/gltf_document.cpp b/modules/gltf/gltf_document.cpp index a8c2a5d055af..5906c2244012 100644 --- a/modules/gltf/gltf_document.cpp +++ b/modules/gltf/gltf_document.cpp @@ -61,6 +61,8 @@ #include "scene/resources/portable_compressed_texture.h" #include "scene/resources/surface_tool.h" +#include + #ifdef TOOLS_ENABLED #include "editor/file_system/editor_file_system.h" #endif @@ -242,47 +244,176 @@ Error GLTFDocument::_serialize_scenes(Ref p_state) { return OK; } -Error GLTFDocument::_parse_glb(Ref p_file, Ref p_state) { +PackedByteArray GLTFDocument::_import_decode_chunk_data(const PackedByteArray &p_raw_encoded_data, const EncodingFormat p_encoding_format) const { + const int64_t chunk_raw_size = p_raw_encoded_data.size(); + switch (p_encoding_format) { + case ENCODING_FORMAT_PLAIN: + return p_raw_encoded_data; + case ENCODING_FORMAT_ZSTD: { + PackedByteArray decompressed; + if (chunk_raw_size > 0) { + const uint64_t decompressed_size = (uint64_t)ZSTD_getFrameContentSize(p_raw_encoded_data.ptr(), chunk_raw_size); + ERR_FAIL_COND_V(decompressed_size == ZSTD_CONTENTSIZE_ERROR || decompressed_size == ZSTD_CONTENTSIZE_UNKNOWN, PackedByteArray()); + ERR_FAIL_COND_V(decompressed_size > INT64_MAX, PackedByteArray()); + decompressed.resize((int64_t)decompressed_size); + constexpr Compression::Mode mode = Compression::Mode::MODE_ZSTD; + const int64_t result = Compression::decompress(decompressed.ptrw(), (int64_t)decompressed_size, p_raw_encoded_data.ptr(), chunk_raw_size, mode); + ERR_FAIL_COND_V(result < 0 || result != (int64_t)decompressed_size, PackedByteArray()); + } + return decompressed; + } + } + const String friendly = _uint32_to_ascii_string(p_encoding_format); + const String number = String::num_uint64(p_encoding_format, 16, true); + ERR_FAIL_V_MSG(PackedByteArray(), "glTF import: Support for reading \"" + friendly + "\" (0x" + number + ") encoded data is not implemented."); +} + +Error GLTFDocument::_import_parse_glb(Ref p_file, Ref p_state) { ERR_FAIL_COND_V(p_file.is_null(), ERR_INVALID_PARAMETER); ERR_FAIL_COND_V(p_state.is_null(), ERR_INVALID_PARAMETER); ERR_FAIL_COND_V(p_file->get_position() != 0, ERR_FILE_CANT_READ); - uint32_t magic = p_file->get_32(); - ERR_FAIL_COND_V(magic != 0x46546C67, ERR_FILE_UNRECOGNIZED); //glTF - p_file->get_32(); // version - p_file->get_32(); // length - uint32_t chunk_length = p_file->get_32(); - uint32_t chunk_type = p_file->get_32(); - - ERR_FAIL_COND_V(chunk_type != 0x4E4F534A, ERR_PARSE_ERROR); //JSON - Vector json_data; - json_data.resize(chunk_length); - uint32_t len = p_file->get_buffer(json_data.ptrw(), chunk_length); - ERR_FAIL_COND_V(len != chunk_length, ERR_FILE_CORRUPT); - - String text = String::utf8((const char *)json_data.ptr(), json_data.size()); - - JSON json; - Error err = json.parse(text); - ERR_FAIL_COND_V_MSG(err != OK, err, "glTF Binary: Error parsing .glb file's JSON data: " + json.get_error_message() + " at line: " + itos(json.get_error_line())); - - p_state->json = json.get_data(); - - //data? - - chunk_length = p_file->get_32(); - chunk_type = p_file->get_32(); + const uint64_t file_total_length = p_file->get_length(); + ERR_FAIL_COND_V_MSG(file_total_length < (uint64_t)32, ERR_INVALID_DATA, "glTF import: File is too small to be a valid glTF file."); + const uint32_t magic = p_file->get_32(); + ERR_FAIL_COND_V(magic != 0x46546C67, ERR_FILE_UNRECOGNIZED); // ASCII string "glTF" as little-endian. + const uint32_t version = p_file->get_32(); // Binary file format version + uint64_t chunk_alignment_bitmask; + uint64_t max_file_size; + if (version == 2) { + // Binary format version 2 uses 32-bit file size. + const uint64_t declared_file_size = p_file->get_32(); + ERR_FAIL_COND_V_MSG(declared_file_size > file_total_length, ERR_INVALID_DATA, "glTF import: Declared file size exceeds available file size. File is corrupted."); + chunk_alignment_bitmask = 3; + max_file_size = (uint64_t)(UINT32_MAX - 3); // 2^32 - 4 (glb version 2 alignment requires the ends of chunks be 4-byte aligned) + _binary_format_mode = BinaryFormatMode::BINARY_FORMAT_MODE_32_BIT; + } else if (version == 3) { + // Binary format version 3 uses 64-bit file size. + const uint64_t declared_file_size = p_file->get_64(); + ERR_FAIL_COND_V_MSG(declared_file_size > file_total_length, ERR_INVALID_DATA, "glTF import: Declared file size exceeds available file size. File is corrupted."); + chunk_alignment_bitmask = 7; + max_file_size = (uint64_t)INT64_MAX; // 2^63 - 1 (one bit always zero) + _binary_format_mode = BinaryFormatMode::BINARY_FORMAT_MODE_64_BIT; + } else { + ERR_FAIL_V_MSG(ERR_FILE_UNRECOGNIZED, "Unsupported .glb binary format version: " + itos(version)); + } + ERR_FAIL_COND_V_MSG(file_total_length > max_file_size, ERR_INVALID_DATA, "glTF import: File size exceeds maximum possible glTF file size. File is corrupted."); + // Read the chunk headers to find where each chunk starts. + // The first chunk is guaranteed to occur right after the file header. + PackedInt64Array chunk_encodings; + PackedInt64Array chunk_data_lengths; + PackedInt64Array chunk_data_starts; + int64_t gltf_json_chunk_index = -1; + uint64_t chunk_starts_read_offset = p_file->get_position(); + while (chunk_starts_read_offset + 15 < file_total_length) { // Only read when there is enough data for another chunk. + p_file->seek(chunk_starts_read_offset); + uint32_t chunk_type; + uint32_t chunk_encoding; + uint64_t chunk_data_length; + if (version == 2) { + // Binary format version 2 uses 32-bit chunk sizes, and the chunk length comes first. + chunk_data_length = p_file->get_32(); + chunk_type = p_file->get_32(); + chunk_encoding = ENCODING_FORMAT_PLAIN; + } else if (version == 3) { + // Binary format version 3 uses 64-bit chunk sizes, and an additional field for chunk encoding. + chunk_type = p_file->get_32(); + chunk_encoding = p_file->get_32(); + chunk_data_length = p_file->get_64(); + } else { + ERR_FAIL_V_MSG(ERR_FILE_UNRECOGNIZED, "Unsupported .glb binary format version: " + itos(version)); + } + // The first JSON chunk in the .glb file is the glTF data. + if (gltf_json_chunk_index == -1 && chunk_type == (uint32_t)0x4E4F534A) { // ASCII string "JSON" in little-endian. + gltf_json_chunk_index = chunk_data_starts.size(); + } + chunk_starts_read_offset += version == 2 ? (uint64_t)8 : (uint64_t)16; // Move forward by the chunk header size. + ERR_FAIL_COND_V_MSG(chunk_data_length > file_total_length - chunk_starts_read_offset, ERR_INVALID_DATA, "glTF import: Declared chunk length would exceed the file size. File is corrupted."); + const uint64_t padded_chunk_length = (chunk_data_length + chunk_alignment_bitmask) & ~chunk_alignment_bitmask; + chunk_encodings.append((int64_t)chunk_encoding); + chunk_data_lengths.append((int64_t)chunk_data_length); + chunk_data_starts.append((int64_t)chunk_starts_read_offset); + chunk_starts_read_offset += padded_chunk_length; + } + ERR_FAIL_COND_V(gltf_json_chunk_index == -1, ERR_PARSE_ERROR); // Missing glTF data JSON chunk. + // Read the glTF JSON data from the identified chunk. + Dictionary gltf_json; + PackedInt64Array buffer_chunk_indices; + PackedInt64Array buffer_chunk_declared_decoded_byte_lengths; + { + p_file->seek(chunk_data_starts[gltf_json_chunk_index]); + const PackedByteArray json_chunk_encoded = p_file->get_buffer(chunk_data_lengths[gltf_json_chunk_index]); + const EncodingFormat json_chunk_encoding_format = (EncodingFormat)(uint32_t)chunk_encodings[gltf_json_chunk_index]; + if (!_is_encoding_format_supported((EncodingFormat)json_chunk_encoding_format)) { + return ERR_UNAVAILABLE; + } + const PackedByteArray json_chunk = _import_decode_chunk_data(json_chunk_encoded, json_chunk_encoding_format); + const String json_string = String::utf8(reinterpret_cast(json_chunk.ptr()), json_chunk.size()); + // Parse the glTF JSON data as a Godot Dictionary via the JSON class. + JSON json; + Error err = json.parse(json_string); + ERR_FAIL_COND_V_MSG(err != OK, err, "glTF Binary: Error parsing .glb file's JSON data: " + json.get_error_message() + " at line: " + itos(json.get_error_line())); + gltf_json = json.get_data(); + ERR_FAIL_COND_V_MSG(gltf_json.is_empty(), ERR_INVALID_DATA, "glTF import: Failed to parse JSON chunk in glTF file. File is corrupted."); + p_state->set_json(gltf_json); + // Parse the JSON structure of only the buffers, so we can know which chunks to read and decode. + err = _import_parse_buffers(p_state, &buffer_chunk_indices, &buffer_chunk_declared_decoded_byte_lengths); + ERR_FAIL_COND_V_MSG(err != OK, err, "glTF import: Failed to parse glTF buffers when reading from binary glTF file."); + } + // Read and decode any chunk data needed by a buffer. + Vector buffers = p_state->get_buffers(); + for (int64_t buffer_index = 0; buffer_index < buffer_chunk_indices.size(); buffer_index++) { + const int64_t chunk_index = buffer_chunk_indices[buffer_index]; + if (chunk_index == -1) { + continue; + } + ERR_FAIL_INDEX_V_MSG(chunk_index, chunk_data_starts.size(), ERR_INVALID_DATA, "glTF import: Buffer chunk index " + itos(chunk_index) + " is out of range for binary file with " + itos(chunk_data_starts.size()) + " chunks. File is corrupted."); + p_file->seek(chunk_data_starts[chunk_index]); + const PackedByteArray chunk_data_encoded = p_file->get_buffer(chunk_data_lengths[chunk_index]); + const EncodingFormat chunk_encoding_format = (EncodingFormat)(uint32_t)chunk_encodings[chunk_index]; + if (!_is_encoding_format_supported((EncodingFormat)chunk_encoding_format)) { + return ERR_UNAVAILABLE; + } + // Decode the chunk data (this will be a no-op if the encoding is ENCODING_FORMAT_PLAIN). + PackedByteArray chunk_data_decoded = _import_decode_chunk_data(chunk_data_encoded, chunk_encoding_format); + // The data has been read in now, so check that the size is at least the declared size, and truncate if larger. + const int64_t declared_decoded_byte_length = buffer_chunk_declared_decoded_byte_lengths[buffer_index]; + if (chunk_data_decoded.size() < declared_decoded_byte_length) { + ERR_FAIL_COND_V_MSG(chunk_data_decoded.is_empty(), ERR_INVALID_DATA, "glTF import: Failed to read buffer data. Aborting file import."); + ERR_FAIL_V_MSG(ERR_INVALID_DATA, "glTF import: Buffer size is not at least the declared size. Aborting file import."); + } + chunk_data_decoded.resize(declared_decoded_byte_length); + buffers.set(buffer_index, chunk_data_decoded); +#ifndef DISABLE_DEPRECATED + if (buffer_index == 0) { + p_state->glb_data = chunk_data_decoded; + } +#endif // DISABLE_DEPRECATED + } + p_state->set_buffers(buffers); + return OK; +} - if (p_file->eof_reached()) { - return OK; //all good +bool GLTFDocument::_is_encoding_format_supported(const EncodingFormat p_encoding_format) { + if (p_encoding_format == ENCODING_FORMAT_PLAIN || p_encoding_format == ENCODING_FORMAT_ZSTD) { + return true; } + const String friendly = _uint32_to_ascii_string(p_encoding_format); + const String number_hex = String::num_uint64(p_encoding_format, 16, true); + ERR_FAIL_V_MSG(false, "glTF: Support for \"" + friendly + "\" (0x" + number_hex + ") encoded data is not implemented."); +} - ERR_FAIL_COND_V(chunk_type != 0x004E4942, ERR_PARSE_ERROR); //BIN - - p_state->glb_data.resize(chunk_length); - len = p_file->get_buffer(p_state->glb_data.ptrw(), chunk_length); - ERR_FAIL_COND_V(len != chunk_length, ERR_FILE_CORRUPT); - - return OK; +String GLTFDocument::_uint32_to_ascii_string(uint32_t p_value) { + String str = ""; + for (int i = 0; i < 4; i++) { + const uint8_t low_byte = (uint8_t)p_value; + if (low_byte > 0x1F && low_byte < 0x7F) { + str += (char32_t)low_byte; + } else { + str += (char32_t)'?'; + } + p_value >>= 8; + } + return str; } static Array _vec3_to_arr(const Vector3 &p_vec3) { @@ -741,18 +872,31 @@ Error GLTFDocument::_export_encode_buffers(Ref p_state, bool p_separa return OK; } -Error GLTFDocument::_parse_buffers(Ref p_state) { +Error GLTFDocument::_import_parse_buffers(Ref p_state, PackedInt64Array *r_chunk_indices, PackedInt64Array *r_decoded_byte_lengths) { if (!p_state->json.has("buffers")) { return OK; } - const Array &buffers = p_state->json["buffers"]; - for (GLTFBufferIndex i = 0; i < buffers.size(); i++) { - const Dictionary &buffer = buffers[i]; + const Array &json_buffers = p_state->json["buffers"]; + const int64_t buffer_count = json_buffers.size(); + if (r_chunk_indices != nullptr) { + r_chunk_indices->resize_uninitialized(buffer_count); + r_decoded_byte_lengths->resize_uninitialized(buffer_count); + for (int64_t i = 0; i < buffer_count; i++) { + r_chunk_indices->set(i, -1); + r_decoded_byte_lengths->set(i, -1); + } + } + for (GLTFBufferIndex i = 0; i < json_buffers.size(); i++) { + const Dictionary &json_buffer = json_buffers[i]; + ERR_FAIL_COND_V(!json_buffer.has("byteLength"), ERR_PARSE_ERROR); + const int64_t declared_decoded_byte_length = json_buffer["byteLength"]; + if (r_decoded_byte_lengths != nullptr) { + r_decoded_byte_lengths->set(i, declared_decoded_byte_length); + } Vector buffer_data; - if (buffer.has("uri")) { - String uri = buffer["uri"]; - + if (json_buffer.has("uri")) { + String uri = json_buffer["uri"]; if (uri.begins_with("data:")) { // Embedded data using base64. // Validate data MIME types and throw an error if it's one we don't know/support. if (!uri.begins_with("data:application/octet-stream;base64") && @@ -768,12 +912,13 @@ Error GLTFDocument::_parse_buffers(Ref p_state) { buffer_data = FileAccess::get_file_as_bytes(uri); ERR_FAIL_COND_V_MSG(buffer_data.is_empty(), ERR_PARSE_ERROR, "glTF: Couldn't load binary file as an array: " + uri); } - - ERR_FAIL_COND_V(!buffer.has("byteLength"), ERR_PARSE_ERROR); - int64_t byteLength = buffer["byteLength"]; - ERR_FAIL_COND_V(byteLength < buffer_data.size(), ERR_PARSE_ERROR); - } else if (i == 0 && p_state->glb_data.size()) { - buffer_data = p_state->glb_data; + ERR_FAIL_COND_V(declared_decoded_byte_length < buffer_data.size(), ERR_PARSE_ERROR); + } else if (json_buffer.has("chunk")) { + const int64_t chunk_index = json_buffer["chunk"]; + r_chunk_indices->set(i, chunk_index); + } else if (i == 0) { + // glTF 2.0 fallback behavior: buffer 0 uses GLB chunk 1 if no explicit "uri" or "chunk" is specified. + r_chunk_indices->set(i, 1); } else { ERR_PRINT("glTF: Buffer " + itos(i) + " has no data and cannot be loaded."); } @@ -6667,12 +6812,15 @@ Error GLTFDocument::_parse(Ref p_state, Ref p_file) { if (p_file.is_null()) { return FAILED; } + // The minimum possible valid glTF JSON is 27 characters: `{"asset":{"version":"2.0"}}`. + // The minimum possible valid binary glTF (GLB) is bigger than that: 20 or 32 bytes of headers, plus some data in the JSON chunk. + ERR_FAIL_COND_V_MSG(p_file->get_length() < 27, ERR_FILE_CORRUPT, "glTF: File is too small to be a valid glTF file: " + p_file->get_path()); p_file->seek(0); - uint32_t magic = p_file->get_32(); - if (magic == 0x46546C67) { + const uint32_t magic = p_file->get_32(); + if (magic == 0x46546C67) { // ASCII string "glTF" in little-endian. // Binary file. p_file->seek(0); - err = _parse_glb(p_file, p_state); + err = _import_parse_glb(p_file, p_state); if (err != OK) { return err; } @@ -6684,6 +6832,11 @@ Error GLTFDocument::_parse(Ref p_state, Ref p_file) { err = json.parse(text); ERR_FAIL_COND_V_MSG(err != OK, err, "glTF: Error parsing .gltf JSON data: " + json.get_error_message() + " at line: " + itos(json.get_error_line())); p_state->json = json.get_data(); + // Parsing buffers needs to be done at the same time as glb chunk parsing right after the JSON chunk is parsed, + // so in the non-glb text case, it also needs to happen right after JSON parsing for consistency. + // We pass nullptr here because we don't need that glb-specific chunk data for text glTF files. + err = _import_parse_buffers(p_state, nullptr, nullptr); + ERR_FAIL_COND_V(err != OK, ERR_PARSE_ERROR); } err = _parse_asset_header(p_state); @@ -6767,6 +6920,9 @@ Error GLTFDocument::_serialize_asset_header(Ref p_state) { Error GLTFDocument::_serialize_file(Ref p_state) { const String gltf_path = p_state->base_path.path_join(p_state->filename); + if (!_is_encoding_format_supported(_encoding_format)) { + return ERR_UNAVAILABLE; + } Error err = _export_encode_buffers(p_state, p_state->is_text_file()); ERR_FAIL_COND_V_MSG(err != OK, err, "glTF: Failed to encode buffers for file: " + gltf_path); Ref file = FileAccess::open(gltf_path, FileAccess::WRITE, &err); @@ -6776,69 +6932,154 @@ Error GLTFDocument::_serialize_file(Ref p_state) { file->store_string(json); } else { // Serialize binary glTF (GLB) file. - constexpr uint64_t header_size = 12; - constexpr uint64_t chunk_header_size = 8; + uint64_t file_header_size = 12; + uint64_t chunk_header_size = 8; + uint64_t chunk_alignment_bitmask = 3; + uint64_t max_file_length = (uint64_t)(UINT32_MAX - 3); + if (_binary_format_mode == BINARY_FORMAT_MODE_64_BIT) { + file_header_size = 16; + chunk_header_size = 16; + chunk_alignment_bitmask = 7; + max_file_length = (uint64_t)INT64_MAX; // Not UINT64_MAX. + } + const EncodingFormat chunk_encoding = chunk_header_size == 8 ? ENCODING_FORMAT_PLAIN : get_encoding_format(); constexpr uint32_t magic = 0x46546C67; // The byte sequence "glTF" as little-endian. constexpr uint32_t text_chunk_type = 0x4E4F534A; // The byte sequence "JSON" as little-endian. constexpr uint32_t binary_chunk_type = 0x004E4942; // The byte sequence "BIN\0" as little-endian. - String json_string = JSON::stringify(p_state->json, "", true, true); - CharString cs = json_string.utf8(); - uint64_t text_data_length = cs.length(); - uint64_t text_chunk_length = ((text_data_length + 3) & (~3)); - uint64_t total_file_length = header_size + chunk_header_size + text_chunk_length; + PackedByteArray json_bytes = json_string.to_utf8_buffer(); + // The _export_encode_chunk_data function will pad the chunk data to the required alignment. + PackedByteArray json_encoded = _export_encode_chunk_data(json_bytes, chunk_alignment_bitmask, true); + if (json_encoded.is_empty()) { + ERR_FAIL_V_MSG(FAILED, "glTF export: Failed to encode JSON chunk data. Aborting."); + } + PackedByteArray binary_encoded; + uint64_t text_data_length = json_encoded.size(); + uint64_t text_data_length_aligned = (text_data_length + chunk_alignment_bitmask) & (~chunk_alignment_bitmask); + uint64_t total_file_length = file_header_size + chunk_header_size + text_data_length; uint64_t binary_data_length = 0; - uint64_t binary_chunk_length = 0; - if (p_state->buffers.size() > 0) { - binary_data_length = p_state->buffers[0].size(); - binary_chunk_length = ((binary_data_length + 3) & (~3)); - const uint64_t file_length_with_buffer = total_file_length + chunk_header_size + binary_chunk_length; - // Check if the file length with the buffer is greater than glTF's maximum of 4 GiB. - // If it is, we can't write the buffer into the file, but can write it separately. - if (unlikely(file_length_with_buffer > (uint64_t)UINT32_MAX)) { + if (p_state->buffers.size() > 0 && p_state->buffers[0].size() > 0) { + binary_encoded = _export_encode_chunk_data(p_state->buffers[0], chunk_alignment_bitmask, false); + if (binary_encoded.is_empty()) { + ERR_FAIL_V_MSG(FAILED, "glTF export: Failed to encode binary blob chunk data. Aborting."); + } + binary_data_length = binary_encoded.size(); + // Recalculate the total file length with the headers, the aligned JSON chunk length, and the binary data length. + total_file_length = file_header_size + chunk_header_size + text_data_length_aligned + chunk_header_size + binary_data_length; + } + // Check if the file length with the buffer is greater than glTF's maximum of 4 GiB. + if (unlikely(total_file_length > max_file_length)) { + if (_binary_format_mode == BINARY_FORMAT_MODE_AUTO) { + // If the binary format mode is auto, try using the 64-bit format to support larger files. + file_header_size = 16; + chunk_header_size = 16; + chunk_alignment_bitmask = 7; + max_file_length = (uint64_t)INT64_MAX; // Not UINT64_MAX. + json_encoded = _export_encode_chunk_data(json_bytes, chunk_alignment_bitmask, true); + text_data_length = json_encoded.size(); + text_data_length_aligned = (text_data_length + chunk_alignment_bitmask) & (~chunk_alignment_bitmask); + binary_encoded = _export_encode_chunk_data(p_state->buffers[0], chunk_alignment_bitmask, false); + if (binary_encoded.is_empty()) { + ERR_FAIL_V_MSG(FAILED, "glTF export: Failed to encode binary blob chunk data. Aborting."); + } + binary_data_length = binary_encoded.size(); + if (binary_data_length > 0) { + total_file_length = file_header_size + chunk_header_size + text_data_length_aligned + chunk_header_size + binary_data_length; + } else { + total_file_length = file_header_size + chunk_header_size + text_data_length; + } + } + if (unlikely(total_file_length > max_file_length)) { + // Still too big? We can't write the buffer into the file, but can write it separately. err = _export_encode_buffers(p_state, true); ERR_FAIL_COND_V(err != OK, err); // Since the buffer bins were re-encoded, we need to re-convert the JSON to string. json_string = JSON::stringify(p_state->json, "", true, true); - cs = json_string.utf8(); - text_data_length = cs.length(); - text_chunk_length = ((text_data_length + 3) & (~3)); - total_file_length = header_size + chunk_header_size + text_chunk_length; + json_bytes = json_string.to_utf8_buffer(); + json_encoded = _export_encode_chunk_data(json_bytes, chunk_alignment_bitmask, true); + text_data_length = json_encoded.size(); + text_data_length_aligned = (text_data_length + chunk_alignment_bitmask) & (~chunk_alignment_bitmask); binary_data_length = 0; - binary_chunk_length = 0; - } else { - total_file_length = file_length_with_buffer; + total_file_length = file_header_size + chunk_header_size + text_data_length; } } - ERR_FAIL_COND_V_MSG(total_file_length > (uint64_t)UINT32_MAX, ERR_CANT_CREATE, - "glTF: File size exceeds glTF Binary's maximum of 4 GiB. Cannot serialize as a GLB file."); + ERR_FAIL_COND_V_MSG(total_file_length > max_file_length, ERR_CANT_CREATE, + "glTF: File size exceeds glTF Binary's maximum file size. Cannot serialize as a GLB file."); file->store_32(magic); - file->store_32(p_state->major_version); // version - file->store_32(total_file_length); + if (file_header_size == 12) { + file->store_32(2); // Binary format version. + file->store_32(total_file_length); + } else { // 16 + file->store_32(3); // Binary format version. + file->store_64(total_file_length); + } // Write the JSON text chunk. - file->store_32(text_chunk_length); - file->store_32(text_chunk_type); - file->store_buffer((uint8_t *)&cs[0], text_data_length); - for (uint64_t pad_i = text_data_length; pad_i < text_chunk_length; pad_i++) { - file->store_8(' '); + if (chunk_header_size == 8) { + file->store_32((uint32_t)text_data_length); + file->store_32(text_chunk_type); + } else { // 16 + file->store_32(text_chunk_type); + file->store_32(chunk_encoding); + file->store_64(text_data_length); } + file->store_buffer(json_encoded.ptr(), json_encoded.size()); // Write a single binary chunk. - if (binary_chunk_length) { - file->store_32((uint32_t)binary_chunk_length); - file->store_32(binary_chunk_type); - file->store_buffer(p_state->buffers[0].ptr(), binary_data_length); - for (uint32_t pad_i = binary_data_length; pad_i < binary_chunk_length; pad_i++) { + if (binary_data_length) { + // Pad the JSON chunk to the required alignment. Plainly encoded JSON chunk data is already padded with + // spaces, so this only handles the case of padding non-plainly encoded JSON chunk data with null bytes. + for (uint64_t pad_i = text_data_length; pad_i < text_data_length_aligned; pad_i++) { file->store_8(0); } + if (chunk_header_size == 8) { + file->store_32((uint32_t)binary_data_length); + file->store_32(binary_chunk_type); + } else { // 16 + file->store_32(binary_chunk_type); + file->store_32(chunk_encoding); + file->store_64(binary_data_length); + } + file->store_buffer(binary_encoded.ptr(), binary_encoded.size()); } } return err; } +PackedByteArray GLTFDocument::_export_encode_chunk_data(const PackedByteArray &p_chunk_data, uint64_t p_chunk_alignment_bitmask, bool is_text_chunk) const { + const int64_t input_chunk_size = p_chunk_data.size(); + PackedByteArray encoded; + if (!_is_encoding_format_supported(_encoding_format)) { + return encoded; + } + switch (_encoding_format) { + case GLTFDocument::ENCODING_FORMAT_PLAIN: { + encoded = p_chunk_data; + // Pad plain chunk data to the required alignment with spaces for text chunks and null bytes for binary chunks. + const int64_t padded_text_chunk_size = (input_chunk_size + p_chunk_alignment_bitmask) & (~p_chunk_alignment_bitmask); + for (int64_t pad_i = input_chunk_size; pad_i < padded_text_chunk_size; pad_i++) { + encoded.push_back(is_text_chunk ? ' ' : 0); + } + } break; + case GLTFDocument::ENCODING_FORMAT_ZSTD: { + if (input_chunk_size > 0) { + constexpr Compression::Mode mode = Compression::Mode::MODE_ZSTD; + encoded.resize(Compression::get_max_compressed_buffer_size(input_chunk_size, mode)); + int result = Compression::compress(encoded.ptrw(), p_chunk_data.ptr(), input_chunk_size, mode); + ERR_FAIL_COND_V(result < 0, PackedByteArray()); + encoded.resize(result); + } + } break; + } + return encoded; +} + void GLTFDocument::_bind_methods() { + BIND_ENUM_CONSTANT(BINARY_FORMAT_MODE_AUTO); + BIND_ENUM_CONSTANT(BINARY_FORMAT_MODE_32_BIT); + BIND_ENUM_CONSTANT(BINARY_FORMAT_MODE_64_BIT); + BIND_ENUM_CONSTANT(ROOT_NODE_MODE_SINGLE_ROOT); BIND_ENUM_CONSTANT(ROOT_NODE_MODE_KEEP_ROOT); BIND_ENUM_CONSTANT(ROOT_NODE_MODE_MULTI_ROOT); @@ -6863,6 +7104,8 @@ void GLTFDocument::_bind_methods() { ClassDB::bind_method(D_METHOD("get_fallback_image_format"), &GLTFDocument::get_fallback_image_format); ClassDB::bind_method(D_METHOD("set_fallback_image_quality", "fallback_image_quality"), &GLTFDocument::set_fallback_image_quality); ClassDB::bind_method(D_METHOD("get_fallback_image_quality"), &GLTFDocument::get_fallback_image_quality); + ClassDB::bind_method(D_METHOD("set_binary_format_mode", "binary_format_mode"), &GLTFDocument::set_binary_format_mode); + ClassDB::bind_method(D_METHOD("get_binary_format_mode"), &GLTFDocument::get_binary_format_mode); ClassDB::bind_method(D_METHOD("set_root_node_mode", "root_node_mode"), &GLTFDocument::set_root_node_mode); ClassDB::bind_method(D_METHOD("get_root_node_mode"), &GLTFDocument::get_root_node_mode); ClassDB::bind_method(D_METHOD("set_texture_map_mode", "texture_map_mode"), &GLTFDocument::set_texture_map_mode); @@ -6886,6 +7129,7 @@ void GLTFDocument::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "lossy_quality"), "set_lossy_quality", "get_lossy_quality"); ADD_PROPERTY(PropertyInfo(Variant::STRING, "fallback_image_format"), "set_fallback_image_format", "get_fallback_image_format"); ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "fallback_image_quality"), "set_fallback_image_quality", "get_fallback_image_quality"); + ADD_PROPERTY(PropertyInfo(Variant::INT, "binary_format_mode"), "set_binary_format_mode", "get_binary_format_mode"); ADD_PROPERTY(PropertyInfo(Variant::INT, "root_node_mode"), "set_root_node_mode", "get_root_node_mode"); ADD_PROPERTY(PropertyInfo(Variant::INT, "texture_map_mode"), "set_texture_map_mode", "get_texture_map_mode"); ADD_PROPERTY(PropertyInfo(Variant::INT, "visibility_mode"), "set_visibility_mode", "get_visibility_mode"); @@ -6977,6 +7221,12 @@ HashSet GLTFDocument::get_supported_gltf_extensions_hashset() { } PackedByteArray GLTFDocument::_serialize_glb_buffer(Ref p_state, Error *r_err) { + if (!_is_encoding_format_supported(_encoding_format)) { + if (r_err != nullptr) { + *r_err = ERR_UNAVAILABLE; + } + ERR_FAIL_V(PackedByteArray()); + } Error err = _export_encode_buffers(p_state, false); if (r_err) { *r_err = err; @@ -6984,42 +7234,106 @@ PackedByteArray GLTFDocument::_serialize_glb_buffer(Ref p_state, Erro ERR_FAIL_COND_V(err != OK, PackedByteArray()); String json_string = JSON::stringify(p_state->json, "", true, true); - constexpr uint64_t header_size = 12; - constexpr uint64_t chunk_header_size = 8; + uint64_t file_header_size = 12; + uint64_t chunk_header_size = 8; + uint64_t chunk_alignment_bitmask = 3; + uint64_t max_file_length = (uint64_t)(UINT32_MAX - 3); + if (_binary_format_mode == BINARY_FORMAT_MODE_64_BIT) { + file_header_size = 16; + chunk_header_size = 16; + chunk_alignment_bitmask = 7; + max_file_length = (uint64_t)INT64_MAX; // Not UINT64_MAX. + } + const EncodingFormat chunk_encoding = chunk_header_size == 8 ? ENCODING_FORMAT_PLAIN : get_encoding_format(); constexpr uint32_t magic = 0x46546C67; // The byte sequence "glTF" as little-endian. constexpr uint32_t text_chunk_type = 0x4E4F534A; // The byte sequence "JSON" as little-endian. constexpr uint32_t binary_chunk_type = 0x004E4942; // The byte sequence "BIN\0" as little-endian. - const CharString cs = json_string.utf8(); - const uint64_t text_data_length = cs.length(); - const uint64_t text_chunk_length = ((text_data_length + 3) & (~3)); - uint64_t total_file_length = header_size + chunk_header_size + text_chunk_length; - ERR_FAIL_COND_V(total_file_length > (uint64_t)UINT32_MAX, PackedByteArray()); + const PackedByteArray json_bytes = json_string.to_utf8_buffer(); + // The _export_encode_chunk_data function will pad the chunk data to the required alignment. + PackedByteArray text_encoded = _export_encode_chunk_data(json_bytes, chunk_alignment_bitmask, true); + if (text_encoded.is_empty()) { + ERR_FAIL_V_MSG(PackedByteArray(), "glTF export: Failed to encode JSON chunk data. Aborting."); + } + + uint64_t text_data_length = text_encoded.size(); + uint64_t text_data_length_aligned = (text_data_length + chunk_alignment_bitmask) & (~chunk_alignment_bitmask); + + uint64_t total_file_length = file_header_size + chunk_header_size + text_data_length; uint64_t binary_data_length = 0; - if (p_state->buffers.size() > 0) { - binary_data_length = p_state->buffers[0].size(); - const uint64_t file_length_with_buffer = total_file_length + chunk_header_size + binary_data_length; - total_file_length = file_length_with_buffer; + PackedByteArray binary_encoded; + if (p_state->buffers.size() > 0 && p_state->buffers[0].size() > 0) { + binary_encoded = _export_encode_chunk_data(p_state->buffers[0], chunk_alignment_bitmask, false); + if (binary_encoded.is_empty()) { + ERR_FAIL_V_MSG(PackedByteArray(), "glTF export: Failed to encode binary chunk data. Aborting."); + } + binary_data_length = binary_encoded.size(); + if (binary_data_length > 0) { + // Recalculate the total file length with the headers, the aligned JSON chunk length, and the binary data length. + total_file_length = file_header_size + chunk_header_size + text_data_length_aligned + chunk_header_size + binary_data_length; + } + } + if (unlikely(total_file_length > max_file_length)) { + if (_binary_format_mode == BINARY_FORMAT_MODE_AUTO) { + // If the binary format mode is auto, try using the 64-bit format to support larger files. + file_header_size = 16; + chunk_header_size = 16; + chunk_alignment_bitmask = 7; + max_file_length = (uint64_t)INT64_MAX; // Not UINT64_MAX. + text_encoded = _export_encode_chunk_data(json_bytes, chunk_alignment_bitmask, true); + text_data_length = text_encoded.size(); + text_data_length_aligned = (text_data_length + chunk_alignment_bitmask) & (~chunk_alignment_bitmask); + if (p_state->buffers.size() > 0 && p_state->buffers[0].size() > 0) { + binary_encoded = _export_encode_chunk_data(p_state->buffers[0], chunk_alignment_bitmask, false); + } + binary_data_length = binary_encoded.size(); + if (binary_data_length > 0) { + total_file_length = file_header_size + chunk_header_size + text_data_length_aligned + chunk_header_size + binary_data_length; + } else { + total_file_length = file_header_size + chunk_header_size + text_data_length; + } + } } - ERR_FAIL_COND_V_MSG(total_file_length > (uint64_t)UINT32_MAX, PackedByteArray(), - "glTF: File size exceeds glTF Binary's maximum of 4 GiB. Cannot serialize as a single GLB byte array in memory."); - const uint32_t binary_chunk_length = binary_data_length; + ERR_FAIL_COND_V_MSG(total_file_length > max_file_length, PackedByteArray(), + "glTF: File size exceeds glTF Binary's maximum file length. Cannot serialize as a single GLB in-memory buffer."); Ref buffer; buffer.instantiate(); + // Write the file header. buffer->put_32(magic); - buffer->put_32(p_state->major_version); // version - buffer->put_32((uint32_t)total_file_length); // length - buffer->put_32((uint32_t)text_chunk_length); - buffer->put_32(text_chunk_type); - buffer->put_data((uint8_t *)&cs[0], text_data_length); - for (uint64_t pad_i = text_data_length; pad_i < text_chunk_length; pad_i++) { - buffer->put_8(' '); - } - if (binary_chunk_length) { - buffer->put_32(binary_chunk_length); - buffer->put_32(binary_chunk_type); - buffer->put_data(p_state->buffers[0].ptr(), binary_data_length); + if (file_header_size == 12) { + buffer->put_32(2); // Binary format version. + buffer->put_32((uint32_t)total_file_length); + } else { // 16 + buffer->put_32(3); // Binary format version. + buffer->put_64(total_file_length); + } + // Write the JSON text chunk. + if (chunk_header_size == 8) { + buffer->put_32((uint32_t)text_data_length); + buffer->put_32(text_chunk_type); + } else { // 16 + buffer->put_32(text_chunk_type); + buffer->put_32(chunk_encoding); + buffer->put_64(text_data_length); + } + buffer->put_data(text_encoded.ptr(), text_encoded.size()); + // Write a single binary chunk if there is any buffer data. + if (binary_data_length > 0) { + // Pad the JSON chunk to the required alignment. Plainly encoded JSON chunk data is already padded with + // spaces, so this only handles the case of padding non-plainly encoded JSON chunk data with null bytes. + for (uint64_t pad_i = text_data_length; pad_i < text_data_length_aligned; pad_i++) { + buffer->put_8((uint8_t)0); + } + if (chunk_header_size == 8) { + buffer->put_32((uint32_t)binary_data_length); + buffer->put_32(binary_chunk_type); + } else { // 16 + buffer->put_32(binary_chunk_type); + buffer->put_32(chunk_encoding); + buffer->put_64(binary_data_length); + } + buffer->put_data(binary_encoded.ptr(), binary_encoded.size()); } return buffer->get_data_array(); } @@ -7095,10 +7409,6 @@ Error GLTFDocument::_parse_asset_header(Ref p_state) { Error GLTFDocument::_parse_gltf_state(Ref p_state) { Error err; - /* PARSE BUFFERS */ - err = _parse_buffers(p_state); - ERR_FAIL_COND_V(err != OK, ERR_PARSE_ERROR); - /* PARSE BUFFER VIEWS */ err = _parse_buffer_views(p_state); ERR_FAIL_COND_V(err != OK, ERR_PARSE_ERROR); @@ -7368,6 +7678,14 @@ Error GLTFDocument::_parse_gltf_extensions(Ref p_state) { return ret; } +void GLTFDocument::set_binary_format_mode(BinaryFormatMode p_binary_format_mode) { + _binary_format_mode = p_binary_format_mode; +} + +void GLTFDocument::set_encoding_format(EncodingFormat p_encoding_format) { + _encoding_format = p_encoding_format; +} + void GLTFDocument::set_root_node_mode(GLTFDocument::RootNodeMode p_root_node_mode) { _root_node_mode = p_root_node_mode; } diff --git a/modules/gltf/gltf_document.h b/modules/gltf/gltf_document.h index 125f35b53e3f..ae822cfa23c5 100644 --- a/modules/gltf/gltf_document.h +++ b/modules/gltf/gltf_document.h @@ -51,6 +51,15 @@ class GLTFDocument : public Resource { TEXTURE_TYPE_GENERIC = 0, TEXTURE_TYPE_NORMAL = 1, }; + enum BinaryFormatMode { + BINARY_FORMAT_MODE_AUTO, + BINARY_FORMAT_MODE_32_BIT, // GLB version 2 + BINARY_FORMAT_MODE_64_BIT, // GLB version 3 + }; + enum EncodingFormat : uint32_t { + ENCODING_FORMAT_PLAIN = 0u, + ENCODING_FORMAT_ZSTD = 0x6474735Au, // ASCII string "Zstd" in little-endian. Note that this does not need to match the magic number in Zstd itself (0xFD2FB528). + }; enum RootNodeMode { ROOT_NODE_MODE_SINGLE_ROOT, ROOT_NODE_MODE_KEEP_ROOT, @@ -79,6 +88,8 @@ class GLTFDocument : public Resource { String _fallback_image_format = "None"; float _fallback_image_quality = 0.25f; Ref _image_save_extension; + BinaryFormatMode _binary_format_mode = BinaryFormatMode::BINARY_FORMAT_MODE_AUTO; + EncodingFormat _encoding_format = EncodingFormat::ENCODING_FORMAT_PLAIN; RootNodeMode _root_node_mode = RootNodeMode::ROOT_NODE_MODE_SINGLE_ROOT; TextureMapMode _texture_map_mode = TextureMapMode::TEXTURE_MAP_MODE_REMAP_TO_STANDARD_MATERIAL; VisibilityMode _visibility_mode = VisibilityMode::VISIBILITY_MODE_INCLUDE_REQUIRED; @@ -113,6 +124,10 @@ class GLTFDocument : public Resource { void set_fallback_image_quality(float p_fallback_image_quality); float get_fallback_image_quality() const; void set_root_node_mode(RootNodeMode p_root_node_mode); + BinaryFormatMode get_binary_format_mode() const { return _binary_format_mode; } + void set_binary_format_mode(BinaryFormatMode p_binary_format_mode); + EncodingFormat get_encoding_format() const { return _encoding_format; } + void set_encoding_format(EncodingFormat p_encoding_format); RootNodeMode get_root_node_mode() const; void set_texture_map_mode(TextureMapMode p_texture_map_mode); TextureMapMode get_texture_map_mode() const { return _texture_map_mode; } @@ -139,9 +154,11 @@ class GLTFDocument : public Resource { StandardMaterial3D::TextureFilter p_filter_mode, bool p_repeats); Ref _get_sampler_for_texture(Ref p_state, const GLTFTextureIndex p_texture); - Error _parse_glb(Ref p_file, Ref p_state); + PackedByteArray _import_decode_chunk_data(const PackedByteArray &p_raw_encoded_data, const EncodingFormat p_encoding_format) const; + Error _import_parse_glb_chunk(Ref p_file, Ref p_state); + Error _import_parse_glb(Ref p_file, Ref p_state); + Error _import_parse_buffers(Ref p_state, PackedInt64Array *r_chunk_indices, PackedInt64Array *r_decoded_byte_lengths); void _compute_node_heights(Ref p_state); - Error _parse_buffers(Ref p_state); Error _parse_buffer_views(Ref p_state); Error _parse_accessors(Ref p_state); template @@ -205,6 +222,7 @@ class GLTFDocument : public Resource { Error _serialize_nodes(Ref p_state); Error _serialize_scenes(Ref p_state); String interpolation_to_string(const GLTFAnimation::Interpolation p_interp); + PackedByteArray _export_encode_chunk_data(const PackedByteArray &p_buffer_data, uint64_t p_chunk_alignment_bitmask, bool is_text_chunk) const; Error _export_encode_buffers(Ref p_state, bool p_separate_buffers_into_files); PackedByteArray _serialize_glb_buffer(Ref p_state, Error *r_err); Dictionary _serialize_texture_transform_uv1(const Ref &p_material); @@ -238,7 +256,9 @@ class GLTFDocument : public Resource { virtual PackedByteArray generate_buffer(Ref p_state); virtual Error write_to_filesystem(Ref p_state, const String &p_path); -public: +private: + static bool _is_encoding_format_supported(const EncodingFormat p_encoding_format); + static String _uint32_to_ascii_string(uint32_t p_value); Error _parse_gltf_state(Ref p_state); Error _parse_asset_header(Ref p_state); Error _parse_gltf_extensions(Ref p_state); @@ -303,6 +323,8 @@ class GLTFDocument : public Resource { Error _parse(Ref p_state, Ref p_file); }; +VARIANT_ENUM_CAST(GLTFDocument::BinaryFormatMode); +VARIANT_ENUM_CAST(GLTFDocument::EncodingFormat); VARIANT_ENUM_CAST(GLTFDocument::RootNodeMode); VARIANT_ENUM_CAST(GLTFDocument::TextureMapMode); VARIANT_ENUM_CAST(GLTFDocument::VisibilityMode); diff --git a/modules/gltf/gltf_state.cpp b/modules/gltf/gltf_state.cpp index 9350cf3ade22..6e309be01f69 100644 --- a/modules/gltf/gltf_state.cpp +++ b/modules/gltf/gltf_state.cpp @@ -48,8 +48,10 @@ void GLTFState::_bind_methods() { ClassDB::bind_method(D_METHOD("set_minor_version", "minor_version"), &GLTFState::set_minor_version); ClassDB::bind_method(D_METHOD("get_copyright"), &GLTFState::get_copyright); ClassDB::bind_method(D_METHOD("set_copyright", "copyright"), &GLTFState::set_copyright); +#ifndef DISABLE_DEPRECATED ClassDB::bind_method(D_METHOD("get_glb_data"), &GLTFState::get_glb_data); ClassDB::bind_method(D_METHOD("set_glb_data", "glb_data"), &GLTFState::set_glb_data); +#endif // DISABLE_DEPRECATED ClassDB::bind_method(D_METHOD("get_use_named_skin_binds"), &GLTFState::get_use_named_skin_binds); ClassDB::bind_method(D_METHOD("set_use_named_skin_binds", "use_named_skin_binds"), &GLTFState::set_use_named_skin_binds); ClassDB::bind_method(D_METHOD("get_nodes"), &GLTFState::get_nodes_bind); @@ -111,7 +113,9 @@ void GLTFState::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::INT, "major_version"), "set_major_version", "get_major_version"); // int ADD_PROPERTY(PropertyInfo(Variant::INT, "minor_version"), "set_minor_version", "get_minor_version"); // int ADD_PROPERTY(PropertyInfo(Variant::STRING, "copyright"), "set_copyright", "get_copyright"); // String +#ifndef DISABLE_DEPRECATED ADD_PROPERTY(PropertyInfo(Variant::PACKED_BYTE_ARRAY, "glb_data"), "set_glb_data", "get_glb_data"); // Vector +#endif // DISABLE_DEPRECATED ADD_PROPERTY(PropertyInfo(Variant::BOOL, "use_named_skin_binds"), "set_use_named_skin_binds", "get_use_named_skin_binds"); // bool ADD_PROPERTY(PropertyInfo(Variant::ARRAY, "nodes", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_STORAGE | PROPERTY_USAGE_INTERNAL | PROPERTY_USAGE_EDITOR), "set_nodes", "get_nodes"); // Vector> ADD_PROPERTY(PropertyInfo(Variant::ARRAY, "buffers"), "set_buffers", "get_buffers"); // Vector @@ -198,6 +202,7 @@ void GLTFState::set_copyright(const String &p_copyright) { copyright = p_copyright; } +#ifndef DISABLE_DEPRECATED Vector GLTFState::get_glb_data() const { return Vector(glb_data); } @@ -205,6 +210,7 @@ Vector GLTFState::get_glb_data() const { void GLTFState::set_glb_data(const Vector &p_glb_data) { glb_data = Vector(p_glb_data); } +#endif // DISABLE_DEPRECATED bool GLTFState::get_use_named_skin_binds() const { return use_named_skin_binds; diff --git a/modules/gltf/gltf_state.h b/modules/gltf/gltf_state.h index 71ceeb4aa50b..dd3ff023edbe 100644 --- a/modules/gltf/gltf_state.h +++ b/modules/gltf/gltf_state.h @@ -68,7 +68,9 @@ class GLTFState : public Resource { int major_version = 0; int minor_version = 0; String copyright; +#ifndef DISABLE_DEPRECATED Vector glb_data; +#endif // DISABLE_DEPRECATED double bake_fps = 30.0; bool use_named_skin_binds = false; @@ -202,8 +204,10 @@ class GLTFState : public Resource { String get_copyright() const; void set_copyright(const String &p_copyright); +#ifndef DISABLE_DEPRECATED Vector get_glb_data() const; void set_glb_data(const Vector &p_glb_data); +#endif // DISABLE_DEPRECATED bool get_use_named_skin_binds() const; void set_use_named_skin_binds(bool p_use_named_skin_binds);