Browse Source

Merge pull request #69319 from RedMser/blender-import-rpc

Batch import Blend files using XML RPC
Rémi Verschelde 2 years ago
parent
commit
d29036bcda

+ 314 - 0
modules/gltf/editor/editor_import_blend_runner.cpp

@@ -0,0 +1,314 @@
+/*************************************************************************/
+/*  editor_import_blend_runner.cpp                                       */
+/*************************************************************************/
+/*                       This file is part of:                           */
+/*                           GODOT ENGINE                                */
+/*                      https://godotengine.org                          */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur.                 */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md).   */
+/*                                                                       */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the       */
+/* "Software"), to deal in the Software without restriction, including   */
+/* without limitation the rights to use, copy, modify, merge, publish,   */
+/* distribute, sublicense, and/or sell copies of the Software, and to    */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions:                                             */
+/*                                                                       */
+/* The above copyright notice and this permission notice shall be        */
+/* included in all copies or substantial portions of the Software.       */
+/*                                                                       */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,       */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF    */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY  */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,  */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE     */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                */
+/*************************************************************************/
+
+#include "editor_import_blend_runner.h"
+
+#ifdef TOOLS_ENABLED
+
+#include "core/io/http_client.h"
+#include "editor/editor_file_system.h"
+#include "editor/editor_node.h"
+#include "editor/editor_settings.h"
+
+static constexpr char PYTHON_SCRIPT_RPC[] = R"(
+import bpy, sys, threading
+from xmlrpc.server import SimpleXMLRPCServer
+req = threading.Condition()
+res = threading.Condition()
+info = None
+def xmlrpc_server():
+  server = SimpleXMLRPCServer(('127.0.0.1', %d))
+  server.register_function(export_gltf)
+  server.serve_forever()
+def export_gltf(opts):
+  with req:
+    global info
+    info = ('export_gltf', opts)
+    req.notify()
+  with res:
+    res.wait()
+if bpy.app.version < (3, 0, 0):
+  print('Blender 3.0 or higher is required.', file=sys.stderr)
+threading.Thread(target=xmlrpc_server).start()
+while True:
+  with req:
+    while info is None:
+      req.wait()
+  method, opts = info
+  if method == 'export_gltf':
+    try:
+      bpy.ops.wm.open_mainfile(filepath=opts['path'])
+      if opts['unpack_all']:
+        bpy.ops.file.unpack_all(method='USE_LOCAL')
+      bpy.ops.export_scene.gltf(**opts['gltf_options'])
+    except:
+      pass
+  info = None
+  with res:
+    res.notify()
+)";
+
+static constexpr char PYTHON_SCRIPT_DIRECT[] = R"(
+import bpy, sys
+opts = %s
+if bpy.app.version < (3, 0, 0):
+  print('Blender 3.0 or higher is required.', file=sys.stderr)
+bpy.ops.wm.open_mainfile(filepath=opts['path'])
+if opts['unpack_all']:
+  bpy.ops.file.unpack_all(method='USE_LOCAL')
+bpy.ops.export_scene.gltf(**opts['gltf_options'])
+)";
+
+String dict_to_python(const Dictionary &p_dict) {
+	String entries;
+	Array dict_keys = p_dict.keys();
+	for (int i = 0; i < dict_keys.size(); i++) {
+		const String key = dict_keys[i];
+		String value;
+		Variant raw_value = p_dict[key];
+
+		switch (raw_value.get_type()) {
+			case Variant::Type::BOOL: {
+				value = raw_value ? "True" : "False";
+				break;
+			}
+			case Variant::Type::STRING:
+			case Variant::Type::STRING_NAME: {
+				value = raw_value;
+				value = vformat("'%s'", value.c_escape());
+				break;
+			}
+			case Variant::Type::DICTIONARY: {
+				value = dict_to_python(raw_value);
+				break;
+			}
+			default: {
+				ERR_FAIL_V_MSG("", vformat("Unhandled Variant type %s for python dictionary", Variant::get_type_name(raw_value.get_type())));
+			}
+		}
+
+		entries += vformat("'%s': %s,", key, value);
+	}
+	return vformat("{%s}", entries);
+}
+
+String dict_to_xmlrpc(const Dictionary &p_dict) {
+	String members;
+	Array dict_keys = p_dict.keys();
+	for (int i = 0; i < dict_keys.size(); i++) {
+		const String key = dict_keys[i];
+		String value;
+		Variant raw_value = p_dict[key];
+
+		switch (raw_value.get_type()) {
+			case Variant::Type::BOOL: {
+				value = vformat("<boolean>%d</boolean>", raw_value ? 1 : 0);
+				break;
+			}
+			case Variant::Type::STRING:
+			case Variant::Type::STRING_NAME: {
+				value = raw_value;
+				value = vformat("<string>%s</string>", value.xml_escape());
+				break;
+			}
+			case Variant::Type::DICTIONARY: {
+				value = dict_to_xmlrpc(raw_value);
+				break;
+			}
+			default: {
+				ERR_FAIL_V_MSG("", vformat("Unhandled Variant type %s for XMLRPC", Variant::get_type_name(raw_value.get_type())));
+			}
+		}
+
+		members += vformat("<member><name>%s</name><value>%s</value></member>", key, value);
+	}
+	return vformat("<struct>%s</struct>", members);
+}
+
+Error EditorImportBlendRunner::start_blender(const String &p_python_script, bool p_blocking) {
+	String blender_path = EDITOR_GET("filesystem/import/blender/blender3_path");
+
+#ifdef WINDOWS_ENABLED
+	blender_path = blender_path.path_join("blender.exe");
+#else
+	blender_path = blender_path.path_join("blender");
+#endif
+
+	List<String> args;
+	args.push_back("--background");
+	args.push_back("--python-expr");
+	args.push_back(p_python_script);
+
+	Error err;
+	if (p_blocking) {
+		int exitcode = 0;
+		err = OS::get_singleton()->execute(blender_path, args, nullptr, &exitcode);
+		if (exitcode != 0) {
+			return FAILED;
+		}
+	} else {
+		err = OS::get_singleton()->create_process(blender_path, args, &blender_pid);
+	}
+	return err;
+}
+
+Error EditorImportBlendRunner::do_import(const Dictionary &p_options) {
+	if (is_using_rpc()) {
+		return do_import_rpc(p_options);
+	} else {
+		return do_import_direct(p_options);
+	}
+}
+
+Error EditorImportBlendRunner::do_import_rpc(const Dictionary &p_options) {
+	kill_timer->stop();
+
+	// Start Blender if not already running.
+	if (!is_running()) {
+		// Start an XML RPC server on the given port.
+		String python = vformat(PYTHON_SCRIPT_RPC, rpc_port);
+		Error err = start_blender(python, false);
+		if (err != OK || blender_pid == 0) {
+			return FAILED;
+		}
+	}
+
+	// Convert options to XML body.
+	String xml_options = dict_to_xmlrpc(p_options);
+	String xml_body = vformat("<?xml version=\"1.0\"?><methodCall><methodName>export_gltf</methodName><params><param><value>%s</value></param></params></methodCall>", xml_options);
+
+	// Connect to RPC server.
+	Ref<HTTPClient> client = HTTPClient::create();
+	client->connect_to_host("127.0.0.1", rpc_port);
+
+	bool done = false;
+	while (!done) {
+		HTTPClient::Status status = client->get_status();
+		switch (status) {
+			case HTTPClient::STATUS_RESOLVING:
+			case HTTPClient::STATUS_CONNECTING: {
+				client->poll();
+				break;
+			}
+			case HTTPClient::STATUS_CONNECTED: {
+				done = true;
+				break;
+			}
+			default: {
+				ERR_FAIL_V_MSG(ERR_CONNECTION_ERROR, vformat("Unexpected status during RPC connection: %d", status));
+			}
+		}
+	}
+
+	// Send XML request.
+	PackedByteArray xml_buffer = xml_body.to_utf8_buffer();
+	Error err = client->request(HTTPClient::METHOD_POST, "/", Vector<String>(), xml_buffer.ptr(), xml_buffer.size());
+	if (err != OK) {
+		ERR_FAIL_V_MSG(err, vformat("Unable to send RPC request: %d", err));
+	}
+
+	// Wait for response.
+	done = false;
+	while (!done) {
+		HTTPClient::Status status = client->get_status();
+		switch (status) {
+			case HTTPClient::STATUS_REQUESTING: {
+				client->poll();
+				break;
+			}
+			case HTTPClient::STATUS_BODY: {
+				client->poll();
+				// Parse response here if needed. For now we can just ignore it.
+				done = true;
+				break;
+			}
+			default: {
+				ERR_FAIL_V_MSG(ERR_CONNECTION_ERROR, vformat("Unexpected status during RPC response: %d", status));
+			}
+		}
+	}
+
+	return OK;
+}
+
+Error EditorImportBlendRunner::do_import_direct(const Dictionary &p_options) {
+	// Export glTF directly.
+	String python = vformat(PYTHON_SCRIPT_DIRECT, dict_to_python(p_options));
+	Error err = start_blender(python, true);
+	if (err != OK) {
+		return err;
+	}
+
+	return OK;
+}
+
+void EditorImportBlendRunner::_resources_reimported(const PackedStringArray &p_files) {
+	if (is_running()) {
+		// After a batch of imports is done, wait a few seconds before trying to kill blender,
+		// in case of having multiple imports trigger in quick succession.
+		kill_timer->start();
+	}
+}
+
+void EditorImportBlendRunner::_kill_blender() {
+	kill_timer->stop();
+	if (is_running()) {
+		OS::get_singleton()->kill(blender_pid);
+	}
+	blender_pid = 0;
+}
+
+void EditorImportBlendRunner::_notification(int p_what) {
+	switch (p_what) {
+		case NOTIFICATION_PREDELETE: {
+			_kill_blender();
+			break;
+		}
+	}
+}
+
+EditorImportBlendRunner *EditorImportBlendRunner::singleton = nullptr;
+
+EditorImportBlendRunner::EditorImportBlendRunner() {
+	ERR_FAIL_COND_MSG(singleton != nullptr, "EditorImportBlendRunner already created.");
+	singleton = this;
+
+	rpc_port = EDITOR_GET("filesystem/import/blender/rpc_port");
+
+	kill_timer = memnew(Timer);
+	add_child(kill_timer);
+	kill_timer->set_one_shot(true);
+	kill_timer->set_wait_time(EDITOR_GET("filesystem/import/blender/rpc_server_uptime"));
+	kill_timer->connect("timeout", callable_mp(this, &EditorImportBlendRunner::_kill_blender));
+
+	EditorFileSystem::get_singleton()->connect("resources_reimported", callable_mp(this, &EditorImportBlendRunner::_resources_reimported));
+}
+
+#endif // TOOLS_ENABLED

+ 69 - 0
modules/gltf/editor/editor_import_blend_runner.h

@@ -0,0 +1,69 @@
+/*************************************************************************/
+/*  editor_import_blend_runner.h                                         */
+/*************************************************************************/
+/*                       This file is part of:                           */
+/*                           GODOT ENGINE                                */
+/*                      https://godotengine.org                          */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur.                 */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md).   */
+/*                                                                       */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the       */
+/* "Software"), to deal in the Software without restriction, including   */
+/* without limitation the rights to use, copy, modify, merge, publish,   */
+/* distribute, sublicense, and/or sell copies of the Software, and to    */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions:                                             */
+/*                                                                       */
+/* The above copyright notice and this permission notice shall be        */
+/* included in all copies or substantial portions of the Software.       */
+/*                                                                       */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,       */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF    */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY  */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,  */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE     */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                */
+/*************************************************************************/
+
+#ifndef EDITOR_IMPORT_BLEND_RUNNER_H
+#define EDITOR_IMPORT_BLEND_RUNNER_H
+
+#ifdef TOOLS_ENABLED
+
+#include "core/os/os.h"
+#include "scene/main/node.h"
+#include "scene/main/timer.h"
+
+class EditorImportBlendRunner : public Node {
+	GDCLASS(EditorImportBlendRunner, Node);
+
+	static EditorImportBlendRunner *singleton;
+
+	Timer *kill_timer;
+	void _resources_reimported(const PackedStringArray &p_files);
+	void _kill_blender();
+	void _notification(int p_what);
+
+protected:
+	int rpc_port = 0;
+	OS::ProcessID blender_pid = 0;
+	Error start_blender(const String &p_python_script, bool p_blocking);
+	Error do_import_direct(const Dictionary &p_options);
+	Error do_import_rpc(const Dictionary &p_options);
+
+public:
+	static EditorImportBlendRunner *get_singleton() { return singleton; }
+
+	bool is_running() { return blender_pid != 0 && OS::get_singleton()->is_process_running(blender_pid); }
+	bool is_using_rpc() { return rpc_port != 0; }
+	Error do_import(const Dictionary &p_options);
+
+	EditorImportBlendRunner();
+};
+
+#endif // TOOLS_ENABLED
+
+#endif // EDITOR_IMPORT_BLEND_RUNNER_H

+ 56 - 75
modules/gltf/editor/editor_scene_importer_blend.cpp

@@ -34,6 +34,7 @@
 
 #include "../gltf_defines.h"
 #include "../gltf_document.h"
+#include "editor_import_blend_runner.h"
 
 #include "core/config/project_settings.h"
 #include "editor/editor_file_dialog.h"
@@ -68,149 +69,129 @@ Node *EditorSceneFormatImporterBlend::import_scene(const String &p_path, uint32_
 
 	// Handle configuration options.
 
-	String parameters_arg;
+	Dictionary request_options;
+	Dictionary parameters_map;
+
+	parameters_map["filepath"] = sink_global;
+	parameters_map["export_keep_originals"] = true;
+	parameters_map["export_format"] = "GLTF_SEPARATE";
+	parameters_map["export_yup"] = true;
 
 	if (p_options.has(SNAME("blender/nodes/custom_properties")) && p_options[SNAME("blender/nodes/custom_properties")]) {
-		parameters_arg += "export_extras=True,";
+		parameters_map["export_extras"] = true;
 	} else {
-		parameters_arg += "export_extras=False,";
+		parameters_map["export_extras"] = false;
 	}
 	if (p_options.has(SNAME("blender/meshes/skins"))) {
 		int32_t skins = p_options["blender/meshes/skins"];
 		if (skins == BLEND_BONE_INFLUENCES_NONE) {
-			parameters_arg += "export_skins=False,";
+			parameters_map["export_skins"] = false;
 		} else if (skins == BLEND_BONE_INFLUENCES_COMPATIBLE) {
-			parameters_arg += "export_all_influences=False,export_skins=True,";
+			parameters_map["export_skins"] = true;
+			parameters_map["export_all_influences"] = false;
 		} else if (skins == BLEND_BONE_INFLUENCES_ALL) {
-			parameters_arg += "export_all_influences=True,export_skins=True,";
+			parameters_map["export_skins"] = true;
+			parameters_map["export_all_influences"] = true;
 		}
 	} else {
-		parameters_arg += "export_skins=False,";
+		parameters_map["export_skins"] = false;
 	}
 	if (p_options.has(SNAME("blender/materials/export_materials"))) {
 		int32_t exports = p_options["blender/materials/export_materials"];
 		if (exports == BLEND_MATERIAL_EXPORT_PLACEHOLDER) {
-			parameters_arg += "export_materials='PLACEHOLDER',";
+			parameters_map["export_materials"] = "PLACEHOLDER";
 		} else if (exports == BLEND_MATERIAL_EXPORT_EXPORT) {
-			parameters_arg += "export_materials='EXPORT',";
+			parameters_map["export_materials"] = "EXPORT";
 		}
 	} else {
-		parameters_arg += "export_materials='PLACEHOLDER',";
+		parameters_map["export_materials"] = "PLACEHOLDER";
 	}
 	if (p_options.has(SNAME("blender/nodes/cameras")) && p_options[SNAME("blender/nodes/cameras")]) {
-		parameters_arg += "export_cameras=True,";
+		parameters_map["export_cameras"] = true;
 	} else {
-		parameters_arg += "export_cameras=False,";
+		parameters_map["export_cameras"] = false;
 	}
 	if (p_options.has(SNAME("blender/nodes/punctual_lights")) && p_options[SNAME("blender/nodes/punctual_lights")]) {
-		parameters_arg += "export_lights=True,";
+		parameters_map["export_lights"] = true;
 	} else {
-		parameters_arg += "export_lights=False,";
+		parameters_map["export_lights"] = false;
 	}
 	if (p_options.has(SNAME("blender/meshes/colors")) && p_options[SNAME("blender/meshes/colors")]) {
-		parameters_arg += "export_colors=True,";
+		parameters_map["export_colors"] = true;
 	} else {
-		parameters_arg += "export_colors=False,";
+		parameters_map["export_colors"] = false;
 	}
 	if (p_options.has(SNAME("blender/nodes/visible"))) {
 		int32_t visible = p_options["blender/nodes/visible"];
 		if (visible == BLEND_VISIBLE_VISIBLE_ONLY) {
-			parameters_arg += "use_visible=True,";
+			parameters_map["use_visible"] = true;
 		} else if (visible == BLEND_VISIBLE_RENDERABLE) {
-			parameters_arg += "use_renderable=True,";
+			parameters_map["use_renderable"] = true;
 		} else if (visible == BLEND_VISIBLE_ALL) {
-			parameters_arg += "use_visible=False,use_renderable=False,";
+			parameters_map["use_renderable"] = false;
+			parameters_map["use_visible"] = false;
 		}
 	} else {
-		parameters_arg += "use_visible=False,use_renderable=False,";
+		parameters_map["use_renderable"] = false;
+		parameters_map["use_visible"] = false;
 	}
 
 	if (p_options.has(SNAME("blender/meshes/uvs")) && p_options[SNAME("blender/meshes/uvs")]) {
-		parameters_arg += "export_texcoords=True,";
+		parameters_map["export_texcoords"] = true;
 	} else {
-		parameters_arg += "export_texcoords=False,";
+		parameters_map["export_texcoords"] = false;
 	}
 	if (p_options.has(SNAME("blender/meshes/normals")) && p_options[SNAME("blender/meshes/normals")]) {
-		parameters_arg += "export_normals=True,";
+		parameters_map["export_normals"] = true;
 	} else {
-		parameters_arg += "export_normals=False,";
+		parameters_map["export_normals"] = false;
 	}
 	if (p_options.has(SNAME("blender/meshes/tangents")) && p_options[SNAME("blender/meshes/tangents")]) {
-		parameters_arg += "export_tangents=True,";
+		parameters_map["export_tangents"] = true;
 	} else {
-		parameters_arg += "export_tangents=False,";
+		parameters_map["export_tangents"] = false;
 	}
 	if (p_options.has(SNAME("blender/animation/group_tracks")) && p_options[SNAME("blender/animation/group_tracks")]) {
-		parameters_arg += "export_nla_strips=True,";
+		parameters_map["export_nla_strips"] = true;
 	} else {
-		parameters_arg += "export_nla_strips=False,";
+		parameters_map["export_nla_strips"] = false;
 	}
 	if (p_options.has(SNAME("blender/animation/limit_playback")) && p_options[SNAME("blender/animation/limit_playback")]) {
-		parameters_arg += "export_frame_range=True,";
+		parameters_map["export_frame_range"] = true;
 	} else {
-		parameters_arg += "export_frame_range=False,";
+		parameters_map["export_frame_range"] = false;
 	}
 	if (p_options.has(SNAME("blender/animation/always_sample")) && p_options[SNAME("blender/animation/always_sample")]) {
-		parameters_arg += "export_force_sampling=True,";
+		parameters_map["export_force_sampling"] = true;
 	} else {
-		parameters_arg += "export_force_sampling=False,";
+		parameters_map["export_force_sampling"] = false;
 	}
 	if (p_options.has(SNAME("blender/meshes/export_bones_deforming_mesh_only")) && p_options[SNAME("blender/meshes/export_bones_deforming_mesh_only")]) {
-		parameters_arg += "export_def_bones=True,";
+		parameters_map["export_def_bones"] = true;
 	} else {
-		parameters_arg += "export_def_bones=False,";
+		parameters_map["export_def_bones"] = false;
 	}
 	if (p_options.has(SNAME("blender/nodes/modifiers")) && p_options[SNAME("blender/nodes/modifiers")]) {
-		parameters_arg += "export_apply=True";
+		parameters_map["export_apply"] = true;
 	} else {
-		parameters_arg += "export_apply=False";
+		parameters_map["export_apply"] = false;
 	}
 
-	String unpack_all;
 	if (p_options.has(SNAME("blender/materials/unpack_enabled")) && p_options[SNAME("blender/materials/unpack_enabled")]) {
-		unpack_all = "bpy.ops.file.unpack_all(method='USE_LOCAL');";
+		request_options["unpack_all"] = true;
+	} else {
+		request_options["unpack_all"] = false;
 	}
 
-	// Prepare Blender export script.
-
-	String common_args = vformat("filepath='%s',", sink_global) +
-			"export_format='GLTF_SEPARATE',"
-			"export_yup=True," +
-			parameters_arg;
-	String export_script =
-			String("import bpy, sys;") +
-			"print('Blender 3.0 or higher is required.', file=sys.stderr) if bpy.app.version < (3, 0, 0) else None;" +
-			vformat("bpy.ops.wm.open_mainfile(filepath='%s');", source_global) +
-			unpack_all +
-			vformat("bpy.ops.export_scene.gltf(export_keep_originals=True,%s);", common_args);
-	print_verbose(export_script);
-
-	// Run script with configured Blender binary.
+	request_options["path"] = source_global;
+	request_options["gltf_options"] = parameters_map;
 
-	String blender_path = EDITOR_GET("filesystem/import/blender/blender3_path");
-
-#ifdef WINDOWS_ENABLED
-	blender_path = blender_path.path_join("blender.exe");
-#else
-	blender_path = blender_path.path_join("blender");
-#endif
-
-	List<String> args;
-	args.push_back("--background");
-	args.push_back("--python-expr");
-	args.push_back(export_script);
-
-	String standard_out;
-	int ret;
-	OS::get_singleton()->execute(blender_path, args, &standard_out, &ret, true);
-	print_verbose(blender_path);
-	print_verbose(standard_out);
-
-	if (ret != 0) {
+	// Run Blender and export glTF.
+	Error err = EditorImportBlendRunner::get_singleton()->do_import(request_options);
+	if (err != OK) {
 		if (r_err) {
 			*r_err = ERR_SCRIPT_FAILED;
 		}
-		ERR_PRINT(vformat("Blend export to glTF failed with error: %d.", ret));
 		return nullptr;
 	}
 
@@ -226,7 +207,7 @@ Node *EditorSceneFormatImporterBlend::import_scene(const String &p_path, uint32_
 	if (p_options.has(SNAME("blender/materials/unpack_enabled")) && p_options[SNAME("blender/materials/unpack_enabled")]) {
 		base_dir = sink.get_base_dir();
 	}
-	Error err = gltf->append_from_file(sink.get_basename() + ".gltf", state, p_flags, base_dir);
+	err = gltf->append_from_file(sink.get_basename() + ".gltf", state, p_flags, base_dir);
 	if (err != OK) {
 		if (r_err) {
 			*r_err = FAILED;

+ 11 - 0
modules/gltf/register_types.cpp

@@ -36,6 +36,7 @@
 
 #ifdef TOOLS_ENABLED
 #include "core/config/project_settings.h"
+#include "editor/editor_import_blend_runner.h"
 #include "editor/editor_node.h"
 #include "editor/editor_scene_exporter_gltf_plugin.h"
 #include "editor/editor_scene_importer_blend.h"
@@ -52,6 +53,14 @@ static void _editor_init() {
 
 	bool blend_enabled = GLOBAL_GET("filesystem/import/blender/enabled");
 	// Defined here because EditorSettings doesn't exist in `register_gltf_types` yet.
+	EDITOR_DEF_RST("filesystem/import/blender/rpc_port", 6011);
+	EditorSettings::get_singleton()->add_property_hint(PropertyInfo(Variant::INT,
+			"filesystem/import/blender/rpc_port", PROPERTY_HINT_RANGE, "0,65535,1"));
+
+	EDITOR_DEF_RST("filesystem/import/blender/rpc_server_uptime", 5);
+	EditorSettings::get_singleton()->add_property_hint(PropertyInfo(Variant::FLOAT,
+			"filesystem/import/blender/rpc_server_uptime", PROPERTY_HINT_RANGE, "0,300,1,or_greater,suffix:s"));
+
 	String blender3_path = EDITOR_DEF_RST("filesystem/import/blender/blender3_path", "");
 	EditorSettings::get_singleton()->add_property_hint(PropertyInfo(Variant::STRING,
 			"filesystem/import/blender/blender3_path", PROPERTY_HINT_GLOBAL_DIR));
@@ -71,6 +80,8 @@ static void _editor_init() {
 			EditorFileSystem::get_singleton()->add_import_format_support_query(blend_import_query);
 		}
 	}
+	memnew(EditorImportBlendRunner);
+	EditorNode::get_singleton()->add_child(EditorImportBlendRunner::get_singleton());
 
 	// FBX to glTF importer.