From 9db0860c2ed851855a9b18bc5d3783dbed77a960 Mon Sep 17 00:00:00 2001 From: Lyuma Date: Sun, 25 Feb 2024 08:06:37 -0800 Subject: [PATCH] Option to use Animation as skeleton rest silhouette. Adds `rest_pose/external_animation_library` advanced option to replace bone rest with an exported Animation before retargeting. Together this allows a purely importer based workflow to transfer a known good pose from one FBX to another. --- editor/import/3d/resource_importer_scene.cpp | 188 ++++++++++++++++++- editor/import/3d/scene_import_settings.cpp | 70 ++++++- 2 files changed, 248 insertions(+), 10 deletions(-) diff --git a/editor/import/3d/resource_importer_scene.cpp b/editor/import/3d/resource_importer_scene.cpp index ca128968dea..1bdbddb6293 100644 --- a/editor/import/3d/resource_importer_scene.cpp +++ b/editor/import/3d/resource_importer_scene.cpp @@ -53,6 +53,7 @@ #include "scene/resources/3d/sphere_shape_3d.h" #include "scene/resources/3d/world_boundary_shape_3d.h" #include "scene/resources/animation.h" +#include "scene/resources/bone_map.h" #include "scene/resources/packed_scene.h" #include "scene/resources/resource_format_text.h" #include "scene/resources/surface_tool.h" @@ -1157,6 +1158,74 @@ Node *ResourceImporterScene::_post_fix_node(Node *p_node, Node *p_root, HashMap< } if (Object::cast_to(p_node)) { + Ref rest_animation; + float rest_animation_timestamp = 0.0; + Skeleton3D *skeleton = Object::cast_to(p_node); + if (skeleton != nullptr && int(node_settings.get("rest_pose/load_pose", 0)) != 0) { + String selected_animation_name = node_settings.get("rest_pose/selected_animation", String()); + if (int(node_settings["rest_pose/load_pose"]) == 1) { + TypedArray children = p_root->find_children("*", "AnimationPlayer", true, false); + for (int node_i = 0; node_i < children.size(); node_i++) { + AnimationPlayer *anim_player = cast_to(children[node_i]); + ERR_CONTINUE(anim_player == nullptr); + List anim_list; + anim_player->get_animation_list(&anim_list); + if (anim_list.size() == 1) { + selected_animation_name = anim_list[0]; + } + rest_animation = anim_player->get_animation(selected_animation_name); + if (rest_animation.is_valid()) { + break; + } + } + } else if (int(node_settings["rest_pose/load_pose"]) == 2) { + Object *external_object = node_settings.get("rest_pose/external_animation_library", Variant()); + rest_animation = external_object; + if (rest_animation.is_null()) { + Ref library(external_object); + if (library.is_valid()) { + List anim_list; + library->get_animation_list(&anim_list); + if (anim_list.size() == 1) { + selected_animation_name = String(anim_list[0]); + } + rest_animation = library->get_animation(selected_animation_name); + } + } + } + rest_animation_timestamp = double(node_settings.get("rest_pose/selected_timestamp", 0.0)); + if (rest_animation.is_valid()) { + for (int track_i = 0; track_i < rest_animation->get_track_count(); track_i++) { + NodePath path = rest_animation->track_get_path(track_i); + StringName node_path = path.get_concatenated_names(); + if (String(node_path).begins_with("%")) { + continue; // Unique node names are commonly used with retargeted animations, which we do not want to use. + } + StringName skeleton_bone = path.get_concatenated_subnames(); + if (skeleton_bone == StringName()) { + continue; + } + int bone_idx = skeleton->find_bone(skeleton_bone); + if (bone_idx == -1) { + continue; + } + switch (rest_animation->track_get_type(track_i)) { + case Animation::TYPE_POSITION_3D: { + Vector3 bone_position = rest_animation->position_track_interpolate(track_i, rest_animation_timestamp); + skeleton->set_bone_rest(bone_idx, Transform3D(skeleton->get_bone_rest(bone_idx).basis, bone_position)); + } break; + case Animation::TYPE_ROTATION_3D: { + Quaternion bone_rotation = rest_animation->rotation_track_interpolate(track_i, rest_animation_timestamp); + Transform3D current_rest = skeleton->get_bone_rest(bone_idx); + skeleton->set_bone_rest(bone_idx, Transform3D(Basis(bone_rotation).scaled(current_rest.basis.get_scale()), current_rest.origin)); + } break; + default: + break; + } + } + } + } + ObjectID node_id = p_node->get_instance_id(); for (int i = 0; i < post_importer_plugins.size(); i++) { post_importer_plugins.write[i]->internal_process(EditorScenePostImportPlugin::INTERNAL_IMPORT_CATEGORY_SKELETON_3D_NODE, p_root, p_node, Ref(), node_settings); @@ -1745,6 +1814,34 @@ void ResourceImporterScene::get_internal_import_options(InternalImportCategory p } break; case INTERNAL_IMPORT_CATEGORY_SKELETON_3D_NODE: { r_options->push_back(ImportOption(PropertyInfo(Variant::BOOL, "import/skip_import", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_UPDATE_ALL_IF_MODIFIED), false)); + r_options->push_back(ImportOption(PropertyInfo(Variant::INT, "rest_pose/load_pose", PROPERTY_HINT_ENUM, "Default Pose,Use AnimationPlayer,Load External Animation", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_UPDATE_ALL_IF_MODIFIED), 0)); + r_options->push_back(ImportOption(PropertyInfo(Variant::OBJECT, "rest_pose/external_animation_library", PROPERTY_HINT_RESOURCE_TYPE, "Animation,AnimationLibrary", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_UPDATE_ALL_IF_MODIFIED), Variant())); + r_options->push_back(ImportOption(PropertyInfo(Variant::STRING, "rest_pose/selected_animation", PROPERTY_HINT_ENUM, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_UPDATE_ALL_IF_MODIFIED), "")); + r_options->push_back(ImportOption(PropertyInfo(Variant::FLOAT, "rest_pose/selected_timestamp", PROPERTY_HINT_RANGE, "0,1,0.001,or_greater,suffix:s", PROPERTY_USAGE_DEFAULT), 0.0f)); + String mismatched_or_empty_profile_warning = String( + "The external rest animation is missing some bones. " + "Consider disabling Remove Immutable Tracks on the other file."); // TODO: translate. + r_options->push_back(ImportOption( + PropertyInfo( + Variant::STRING, U"rest_pose/\u26A0_validation_warning/mismatched_or_empty_profile", + PROPERTY_HINT_MULTILINE_TEXT, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_READ_ONLY), + Variant(mismatched_or_empty_profile_warning))); + String profile_must_not_be_retargeted_warning = String( + "This external rest animation appears to have been imported with a BoneMap. " + "Disable the bone map when exporting a rest animation from the reference model."); // TODO: translate. + r_options->push_back(ImportOption( + PropertyInfo( + Variant::STRING, U"rest_pose/\u26A0_validation_warning/profile_must_not_be_retargeted", + PROPERTY_HINT_MULTILINE_TEXT, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_READ_ONLY), + Variant(profile_must_not_be_retargeted_warning))); + String no_animation_warning = String( + "Select an animation: Find a FBX or glTF in a compatible rest pose " + "and export a compatible animation from its import settings."); // TODO: translate. + r_options->push_back(ImportOption( + PropertyInfo( + Variant::STRING, U"rest_pose//no_animation_chosen", + PROPERTY_HINT_MULTILINE_TEXT, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_READ_ONLY), + Variant(no_animation_warning))); r_options->push_back(ImportOption(PropertyInfo(Variant::OBJECT, "retarget/bone_map", PROPERTY_HINT_RESOURCE_TYPE, "BoneMap", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_UPDATE_ALL_IF_MODIFIED), Variant())); } break; default: { @@ -1859,9 +1956,90 @@ bool ResourceImporterScene::get_internal_option_visibility(InternalImportCategor } } break; case INTERNAL_IMPORT_CATEGORY_SKELETON_3D_NODE: { - const bool use_retarget = p_options["retarget/bone_map"].get_validated_object() != nullptr; - if (p_option != "retarget/bone_map" && p_option.begins_with("retarget/")) { - return use_retarget; + const bool use_retarget = Object::cast_to(p_options["retarget/bone_map"].get_validated_object()) != nullptr; + if (!use_retarget && p_option != "retarget/bone_map" && p_option.begins_with("retarget/")) { + return false; + } + int rest_warning = 0; + if (p_option.begins_with("rest_pose/")) { + if (!p_options.has("rest_pose/load_pose") || int(p_options["rest_pose/load_pose"]) == 0) { + if (p_option != "rest_pose/load_pose") { + return false; + } + } else if (int(p_options["rest_pose/load_pose"]) == 1) { + if (p_option == "rest_pose/external_animation_library") { + return false; + } + } else if (int(p_options["rest_pose/load_pose"]) == 2) { + Object *res = p_options["rest_pose/external_animation_library"]; + Ref anim(res); + if (anim.is_valid() && p_option == "rest_pose/selected_animation") { + return false; + } + Ref library(res); + String selected_animation_name = p_options["rest_pose/selected_animation"]; + if (library.is_valid()) { + List anim_list; + library->get_animation_list(&anim_list); + if (anim_list.size() == 1) { + selected_animation_name = String(anim_list[0]); + } + if (library->has_animation(selected_animation_name)) { + anim = library->get_animation(selected_animation_name); + } + } + int found_bone_count = 0; + Ref bone_map; + Ref prof; + if (p_options.has("retarget/bone_map")) { + bone_map = p_options["retarget/bone_map"]; + } + if (bone_map.is_valid()) { + prof = bone_map->get_profile(); + } + if (anim.is_valid()) { + HashSet target_bones; + if (bone_map.is_valid() && prof.is_valid()) { + for (int target_i = 0; target_i < prof->get_bone_size(); target_i++) { + StringName skeleton_bone_name = bone_map->get_skeleton_bone_name(prof->get_bone_name(target_i)); + if (skeleton_bone_name) { + target_bones.insert(skeleton_bone_name); + } + } + } + for (int track_i = 0; track_i < anim->get_track_count(); track_i++) { + if (anim->track_get_type(track_i) != Animation::TYPE_POSITION_3D && anim->track_get_type(track_i) != Animation::TYPE_ROTATION_3D) { + continue; + } + NodePath path = anim->track_get_path(track_i); + StringName node_path = path.get_concatenated_names(); + StringName skeleton_bone = path.get_concatenated_subnames(); + if (skeleton_bone) { + if (String(node_path).begins_with("%")) { + rest_warning = 1; + } + if (target_bones.has(skeleton_bone)) { + target_bones.erase(skeleton_bone); + } + found_bone_count++; + } + } + if ((found_bone_count < 15 || !target_bones.is_empty()) && rest_warning != 1) { + rest_warning = 2; // heuristic: animation targeted too few bones. + } + } else { + rest_warning = 3; + } + } + if (p_option.begins_with("rest_pose/") && p_option.ends_with("profile_must_not_be_retargeted")) { + return rest_warning == 1; + } + if (p_option.begins_with("rest_pose/") && p_option.ends_with("mismatched_or_empty_profile")) { + return rest_warning == 2; + } + if (p_option.begins_with("rest_pose/") && p_option.ends_with("no_animation_chosen")) { + return rest_warning == 3; + } } } break; default: { @@ -2079,8 +2257,8 @@ Node *ResourceImporterScene::_generate_meshes(Node *p_node, const Dictionary &p_ merge_angle = mesh_settings["lods/normal_merge_angle"]; } - if (mesh_settings.has("save_to_file/enabled") && bool(mesh_settings["save_to_file/enabled"]) && mesh_settings.has("save_to_file/path")) { - save_to_file = mesh_settings["save_to_file/path"]; + if (bool(mesh_settings.get("save_to_file/enabled", false))) { + save_to_file = mesh_settings.get("save_to_file/path", String()); if (!save_to_file.is_resource_file()) { save_to_file = ""; } diff --git a/editor/import/3d/scene_import_settings.cpp b/editor/import/3d/scene_import_settings.cpp index 721eccdfddf..2a8c2f5340e 100644 --- a/editor/import/3d/scene_import_settings.cpp +++ b/editor/import/3d/scene_import_settings.cpp @@ -50,6 +50,8 @@ class SceneImportSettingsData : public Object { HashMap current; HashMap defaults; List options; + Vector animation_list; + bool hide_options = false; String path; @@ -96,6 +98,7 @@ class SceneImportSettingsData : public Object { } return false; } + bool _get(const StringName &p_name, Variant &r_ret) const { if (settings) { if (settings->has(p_name)) { @@ -109,29 +112,81 @@ class SceneImportSettingsData : public Object { } return false; } - void _get_property_list(List *p_list) const { + + void handle_special_properties(PropertyInfo &r_option) const { + ERR_FAIL_NULL(settings); + if (r_option.name == "rest_pose/load_pose") { + if (!settings->has("rest_pose/load_pose") || int((*settings)["rest_pose/load_pose"]) != 2) { + (*settings)["rest_pose/external_animation_library"] = Variant(); + } + } + if (r_option.name == "rest_pose/selected_animation") { + if (!settings->has("rest_pose/load_pose")) { + return; + } + String hint_string; + + switch (int((*settings)["rest_pose/load_pose"])) { + case 1: { + hint_string = String(",").join(animation_list); + if (animation_list.size() == 1) { + (*settings)["rest_pose/selected_animation"] = animation_list[0]; + } + } break; + case 2: { + Object *res = (*settings)["rest_pose/external_animation_library"]; + Ref anim(res); + Ref library(res); + if (anim.is_valid()) { + hint_string = anim->get_name(); + } + if (library.is_valid()) { + List anim_names; + library->get_animation_list(&anim_names); + if (anim_names.size() == 1) { + (*settings)["rest_pose/selected_animation"] = String(anim_names[0]); + } + for (StringName anim_name : anim_names) { + hint_string += "," + anim_name; // Include preceding, as a catch-all. + } + } + } break; + default: + break; + } + r_option.hint = PROPERTY_HINT_ENUM; + r_option.hint_string = hint_string; + } + } + + void _get_property_list(List *r_list) const { if (hide_options) { return; } for (const ResourceImporter::ImportOption &E : options) { + PropertyInfo option = E.option; if (SceneImportSettingsDialog::get_singleton()->is_editing_animation()) { if (category == ResourceImporterScene::INTERNAL_IMPORT_CATEGORY_MAX) { if (ResourceImporterScene::get_animation_singleton()->get_option_visibility(path, E.option.name, current)) { - p_list->push_back(E.option); + handle_special_properties(option); + r_list->push_back(option); } } else { if (ResourceImporterScene::get_animation_singleton()->get_internal_option_visibility(category, E.option.name, current)) { - p_list->push_back(E.option); + handle_special_properties(option); + r_list->push_back(option); } } } else { if (category == ResourceImporterScene::INTERNAL_IMPORT_CATEGORY_MAX) { if (ResourceImporterScene::get_scene_singleton()->get_option_visibility(path, E.option.name, current)) { - p_list->push_back(E.option); + handle_special_properties(option); + r_list->push_back(option); } } else { if (ResourceImporterScene::get_scene_singleton()->get_internal_option_visibility(category, E.option.name, current)) { - p_list->push_back(E.option); + handle_special_properties(option); + r_list->push_back(option); } } } @@ -376,10 +431,15 @@ void SceneImportSettingsDialog::_fill_scene(Node *p_node, TreeItem *p_parent_ite AnimationPlayer *anim_node = Object::cast_to(p_node); if (anim_node) { + Vector animation_list; List animations; anim_node->get_animation_list(&animations); for (const StringName &E : animations) { _fill_animation(scene_tree, anim_node->get_animation(E), E, item); + animation_list.append(E); + } + if (scene_import_settings_data != nullptr) { + scene_import_settings_data->animation_list = animation_list; } }