/**************************************************************************/ /* editor_import_blend_runner.cpp */ /**************************************************************************/ /* This file is part of: */ /* GODOT ENGINE */ /* https://godotengine.org */ /**************************************************************************/ /* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ /* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ /* "Software"), to deal in the Software without restriction, including */ /* without limitation the rights to use, copy, modify, merge, publish, */ /* distribute, sublicense, and/or sell copies of the Software, and to */ /* permit persons to whom the Software is furnished to do so, subject to */ /* the following conditions: */ /* */ /* The above copyright notice and this permission notice shall be */ /* included in all copies or substantial portions of the Software. */ /* */ /* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ /* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ /* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ /* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ /* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ /* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /**************************************************************************/ #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("%d", raw_value ? 1 : 0); break; } case Variant::Type::STRING: case Variant::Type::STRING_NAME: { value = raw_value; value = vformat("%s", 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("%s%s", key, value); } return vformat("%s", 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 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()) { Error err = do_import_rpc(p_options); if (err != OK) { // Retry without using RPC (slow, but better than the import failing completely). if (err == ERR_CONNECTION_ERROR) { // Disable RPC if the connection could not be established. print_error(vformat("Failed to connect to Blender via RPC, switching to direct imports of .blend files. Check your proxy and firewall settings, then RPC can be re-enabled by changing the editor setting `filesystem/import/blender/rpc_port` to %d.", rpc_port)); EditorSettings::get_singleton()->set_manually("filesystem/import/blender/rpc_port", 0); rpc_port = 0; } err = do_import_direct(p_options); } return err; } 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("export_gltf%s", xml_options); // Connect to RPC server. Ref 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(), 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