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

Batch import Blend files using XML RPC
This commit is contained in:
Rémi Verschelde 2023-02-01 08:21:16 +01:00
commit d29036bcda
No known key found for this signature in database
GPG key ID: C3336907360768E1
4 changed files with 450 additions and 75 deletions

View file

@ -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

View file

@ -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

View file

@ -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.
request_options["path"] = source_global;
request_options["gltf_options"] = parameters_map;
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.
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;

View file

@ -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.