From 044a514211c0ba2b97d0b904375d6eb3d47823cf Mon Sep 17 00:00:00 2001 From: nikitalita <69168929+nikitalita@users.noreply.github.com> Date: Sat, 20 Jun 2026 14:45:43 -0700 Subject: [PATCH] add option to export mesh as a GLB/GLTF file in the single resource export dialogue --- exporters/scene_exporter.cpp | 52 ++++++++++++++++++++------ exporters/scene_exporter.h | 4 +- standalone/gdre_recover.gd | 72 +++++++++++++++++++++++++++++++++++- 3 files changed, 113 insertions(+), 15 deletions(-) diff --git a/exporters/scene_exporter.cpp b/exporters/scene_exporter.cpp index a586cdf36..30a07335f 100644 --- a/exporters/scene_exporter.cpp +++ b/exporters/scene_exporter.cpp @@ -2849,7 +2849,7 @@ Node *GLBExporterInstance::_instantiate_scene(Ref scene) { return root; } -Error GLBExporterInstance::_load_scene_and_deps(Ref &r_scene) { +Error GLBExporterInstance::_load_scene_and_deps(Ref &r_scene) { MeshInstance3D::upgrading_skeleton_compat = true; err = _load_deps(); if (err != OK) { @@ -2858,7 +2858,7 @@ Error GLBExporterInstance::_load_scene_and_deps(Ref &r_scene) { return _load_scene(r_scene); } -Error GLBExporterInstance::_load_scene(Ref &r_scene) { +Error GLBExporterInstance::_load_scene(Ref &r_scene) { auto mode_type = ResourceCompatLoader::get_default_load_type(); // loading older scenes will spam warnings about deprecated features #ifndef DEBUG_ENABLED @@ -2866,15 +2866,15 @@ Error GLBExporterInstance::_load_scene(Ref &r_scene) { _silence_errors(true); } #endif - std::optional> result; + std::optional> result; // For some reason, scenes with meshes fail to load without the load done by ResourceLoader::load, possibly due to notification shenanigans. if (ResourceCompatLoader::is_globally_available()) { - result = TaskManager::get_singleton()->dispatch_to_main_thread((std::function()>)[&]() -> Ref { - return ResourceLoader::load(source_path, "PackedScene", ResourceFormatLoader::CACHE_MODE_REUSE, &err); + result = TaskManager::get_singleton()->dispatch_to_main_thread((std::function()>)[&]() -> Ref { + return ResourceLoader::load(source_path, "", ResourceFormatLoader::CACHE_MODE_REUSE, &err); }); } else { - result = TaskManager::get_singleton()->dispatch_to_main_thread((std::function()>)[&]() -> Ref { - return ResourceCompatLoader::custom_load(source_path, "PackedScene", mode_type, &err, using_threaded_load(), ResourceFormatLoader::CACHE_MODE_REUSE); + result = TaskManager::get_singleton()->dispatch_to_main_thread((std::function()>)[&]() -> Ref { + return ResourceCompatLoader::custom_load(source_path, "", mode_type, &err, using_threaded_load(), ResourceFormatLoader::CACHE_MODE_REUSE); }); } if (!result.has_value()) { @@ -3294,6 +3294,18 @@ struct BatchExportToken : public TaskRunnerStruct { return _check_unsupported(ver_major, is_text_output()); } + Error create_packed_scene_from_mesh(const Ref &mesh, Ref &scene) { + ERR_FAIL_COND_V_MSG(mesh.is_null(), ERR_INVALID_PARAMETER, "Mesh is null"); + return TaskManager::get_singleton()->dispatch_to_main_thread((std::function)[&]() -> Error { + MeshInstance3D *root = memnew(MeshInstance3D); + root->set_mesh(mesh); + scene = Ref(memnew(PackedScene)); + scene->pack(root); + return OK; + }) + .value_or(ERR_SKIP); + } + // scene loading and scene instancing has to be done on the main thread to avoid deadlocks and crashes bool batch_preload() { GDRELogger::clear_error_queues(); @@ -3318,13 +3330,31 @@ struct BatchExportToken : public TaskRunnerStruct { err = gdre::ensure_dir(p_dest_path.get_base_dir()); report->set_error(err); - ERR_FAIL_COND_V_MSG(err, false, "Failed to ensure directory " + p_dest_path.get_base_dir()); + if (err) { + after_preload(); + ERR_FAIL_V_MSG(false, "Failed to ensure directory " + p_dest_path.get_base_dir()); + } { - Ref scene; - err = instance._load_scene_and_deps(scene); - if (scene.is_null() && err == OK) { + String resource_type = report->get_import_info()->get_type(); + bool is_mesh = false; + if (resource_type != "PackedScene") { + if (resource_type != "Mesh" && !ClassDB::is_parent_class(resource_type, "Mesh")) { + after_preload(); + ERR_FAIL_V_MSG(false, "Unsupported resource type: " + resource_type); + } + is_mesh = true; + } + Ref resource; + err = instance._load_scene_and_deps(resource); + if (resource.is_null() && err == OK) { err = ERR_CANT_ACQUIRE_RESOURCE; } + + Ref scene = resource; + if (err == OK && scene.is_null() && is_mesh) { + err = create_packed_scene_from_mesh(resource, scene); + } + if (err != OK) { report->set_error(err); after_preload(); diff --git a/exporters/scene_exporter.h b/exporters/scene_exporter.h index 811861c29..51cebb4fc 100644 --- a/exporters/scene_exporter.h +++ b/exporters/scene_exporter.h @@ -174,8 +174,8 @@ class GLBExporterInstance { Dictionary _get_default_subresource_options(); Error _check_model_can_load(const String &p_dest_path); Error _load_deps(); - Error _load_scene_and_deps(Ref &r_scene); - Error _load_scene(Ref &r_scene); + Error _load_scene_and_deps(Ref &r_scene); + Error _load_scene(Ref &r_scene); void recompute_animation_tracks_for_library(AnimationPlayer *p_player, const Ref &p_anim_lib, const LocalVector &p_anim_names); void convert_animation_tracks_to_v4_for_player(AnimationPlayer *p_player); diff --git a/standalone/gdre_recover.gd b/standalone/gdre_recover.gd index 08f6b280c..a45b5afff 100644 --- a/standalone/gdre_recover.gd +++ b/standalone/gdre_recover.gd @@ -82,6 +82,7 @@ func _get_all_files(files: PackedStringArray) -> PackedStringArray: const DIR_STRUCTURE_OPTION_NAME = "Directory Structure" const EXPORT_SCENE_OPTION_NAME = "Export Scenes as" +const EXPORT_MESH_OPTION_NAME = "Export Meshes as" enum DirStructure { FLAT, @@ -96,6 +97,14 @@ enum ExportSceneType { GLTF } +enum ExportMeshType { + AUTO, + TRES, + OBJ, + GLB, + GLTF +} + const DIR_STRUCTURE_NAMES: PackedStringArray = [ "Flat", "Relative Hierarchical", @@ -109,6 +118,14 @@ const EXPORT_SCENE_TYPE_NAMES: PackedStringArray = [ "GLTF", ] +const EXPORT_MESH_TYPE_NAMES: PackedStringArray = [ + "Auto", + "tres", + "OBJ", + "GLB", + "GLTF", +] + func get_output_file_name(src: String, output_folder: String, dir_structure_option: DirStructure, new_ext: String = "", rel_base: String = "") -> String: var new_name = "" if dir_structure_option == DirStructure.FLAT: @@ -150,6 +167,44 @@ func _export_scene(file: String, output_dir: String, dir_structure: DirStructure report.error = OK return report +# TODO: A more generic way to export resources, stop copying all this code around +func _export_mesh(file: String, output_dir: String, dir_structure: DirStructure, rel_base: String, export_type: ExportMeshType) -> ExportReport: + var source_file = file + var iinfo = GDRESettings.get_import_info_by_dest(file) + if iinfo: + source_file = iinfo.source_file + + var ext = source_file.get_extension().to_lower() + + if export_type == ExportMeshType.GLB: + ext = "glb" + elif export_type == ExportMeshType.GLTF: + ext = "gltf" + elif export_type == ExportMeshType.OBJ: + ext = "obj" + elif export_type == ExportMeshType.TRES: + ext = "tres" + else: # AUTO + if not is_instance_valid(iinfo): + ext = "tres" + + var report: ExportReport = ExportReport.new() + var export_dest = get_output_file_name(source_file, output_dir, dir_structure, ext, rel_base) + if ext == "tres": + # just use bin to text + report.error = ResourceCompatLoader.to_text(file, export_dest) + return report + + if export_type == ExportMeshType.OBJ: + report.error = ObjExporter.export_file_with_options(export_dest, file, {}) + else: + report = SceneExporter.export_file_with_options(export_dest, file, { + "Exporter/Scene/GLTF/replace_shader_materials": true, + }) + if (report.error == ERR_BUG or report.error == ERR_PRINTER_ON_FIRE or report.error == ERR_DATABASE_CANT_READ): + report.error = OK + return report + func get_log_error_string(errs: PackedStringArray) -> String: return "\n".join(GDRECommon.filter_error_backtraces(errs)) @@ -167,7 +222,7 @@ func convert_pcfg_to_text(path: String, output_dir: String) -> Array: return [err, text_file] return [loader.save_cfb(output_dir, ver_major, ver_minor), text_file] -func _export_files(files: PackedStringArray, output_dir: String, dir_structure: DirStructure, rel_base: String, export_glb: ExportSceneType) -> PackedStringArray: +func _export_files(files: PackedStringArray, output_dir: String, dir_structure: DirStructure, rel_base: String, export_glb: ExportSceneType, export_mesh: ExportMeshType) -> PackedStringArray: var errs: PackedStringArray = [] files = _get_all_files(files) @@ -201,6 +256,12 @@ func _export_files(files: PackedStringArray, output_dir: String, dir_structure: errs.append("Exporting cancelled: " + file + "\n" + report.message + "\n" + get_log_error_string(report.get_error_messages())) break errs.append("Failed to export resource: " + file + "\n" + report.message + "\n" + get_log_error_string(report.get_error_messages())) + if file_ext == "mesh" or (_ret and _ret.get_compat_type().contains("Mesh")) and export_mesh != ExportMeshType.AUTO: + var report: ExportReport = _export_mesh(file, output_dir, dir_structure, rel_base, export_mesh) + if not report: + errs.append("Failed to export resource: " + file + get_log_error_string(GDRESettings.get_errors())) + elif report.error != OK and report.error != ERR_PRINTER_ON_FIRE: + errs.append("Failed to export resource: " + file + "\n" + report.message + "\n" + get_log_error_string(report.get_error_messages())) elif _ret: var iinfo: ImportInfo = ImportInfo.copy(_ret) iinfo.export_dest = get_output_file_name(iinfo.source_file, "res://", dir_structure, iinfo.source_file.get_extension().to_lower(), rel_base) @@ -263,8 +324,9 @@ func _do_export(output_dir: String, export_preview_visible: bool): var options = %ExportResDirDialog.get_selected_options() var dir_structure = options.get(DIR_STRUCTURE_OPTION_NAME, DirStructure.RELATIVE_HIERARCHICAL) var export_glb: ExportSceneType = options.get(EXPORT_SCENE_OPTION_NAME, int(ExportSceneType.AUTO)) + var export_mesh: ExportMeshType = options.get(EXPORT_MESH_OPTION_NAME, int(ExportMeshType.AUTO)) - errs = _export_files(files, output_dir, dir_structure, rel_base, export_glb) + errs = _export_files(files, output_dir, dir_structure, rel_base, export_glb, export_mesh) if export_preview_visible: %GdreResourcePreview.set_main_view_visible(true) @@ -371,6 +433,12 @@ func _set_file_dialog_options(file_dialog: FileDialog, default_dir_structure: Di if not include_scene: return var include_glb = GDRESettings.get_ver_major() >= SceneExporter.get_minimum_godot_ver_supported() + var mesh_default = options.get(EXPORT_MESH_OPTION_NAME, int(ExportMeshType.AUTO)) + var mesh_opts = EXPORT_MESH_TYPE_NAMES.duplicate() + if not include_glb: + mesh_opts.remove_at(int(ExportMeshType.GLTF)) + mesh_opts.remove_at(int(ExportMeshType.GLB)) + file_dialog.add_option(EXPORT_MESH_OPTION_NAME, mesh_opts, mesh_default) var scene_default = options.get(EXPORT_SCENE_OPTION_NAME, int(ExportSceneType.AUTO)) #file_dialog.set_option_default(0, int(default_dir_structure)) var glb_opts = EXPORT_SCENE_TYPE_NAMES.duplicate()