From e42e5878ae165ce4d6c27b2250cb81e24317fcb9 Mon Sep 17 00:00:00 2001 From: "Silc Lizard (Tokage) Renew" <61938263+TokageItLab@users.noreply.github.com> Date: Sat, 5 Oct 2024 06:00:39 +0900 Subject: [PATCH] Add RetargetModifier3D for realtime retarget to keep original rest --- doc/classes/AnimationLibrary.xml | 6 + doc/classes/RetargetModifier3D.xml | 34 ++ doc/classes/Skeleton3D.xml | 4 + editor/icons/RetargetModifier3D.svg | 1 + .../post_import_plugin_skeleton_renamer.cpp | 2 +- ...post_import_plugin_skeleton_rest_fixer.cpp | 225 ++++++++- ...import_plugin_skeleton_track_organizer.cpp | 44 +- editor/import/3d/resource_importer_scene.cpp | 1 + scene/3d/node_3d.cpp | 4 +- scene/3d/retarget_modifier_3d.cpp | 438 ++++++++++++++++++ scene/3d/retarget_modifier_3d.h | 110 +++++ scene/3d/skeleton_3d.cpp | 8 +- scene/3d/skeleton_3d.h | 1 + scene/register_scene_types.cpp | 2 + scene/resources/animation_library.cpp | 5 + scene/resources/animation_library.h | 1 + scene/resources/skeleton_profile.cpp | 8 + scene/resources/skeleton_profile.h | 1 + 18 files changed, 873 insertions(+), 22 deletions(-) create mode 100644 doc/classes/RetargetModifier3D.xml create mode 100644 editor/icons/RetargetModifier3D.svg create mode 100644 scene/3d/retarget_modifier_3d.cpp create mode 100644 scene/3d/retarget_modifier_3d.h diff --git a/doc/classes/AnimationLibrary.xml b/doc/classes/AnimationLibrary.xml index 7f87ea4616e7..51588a605261 100644 --- a/doc/classes/AnimationLibrary.xml +++ b/doc/classes/AnimationLibrary.xml @@ -31,6 +31,12 @@ Returns the keys for the [Animation]s stored in the library. + + + + Returns the key count for the [Animation]s stored in the library. + + diff --git a/doc/classes/RetargetModifier3D.xml b/doc/classes/RetargetModifier3D.xml new file mode 100644 index 000000000000..af0713c2b121 --- /dev/null +++ b/doc/classes/RetargetModifier3D.xml @@ -0,0 +1,34 @@ + + + + A modifier to transfer parent skeleton poses (or global poses) to child skeletons in model space with different rests. + + + Retrieves the pose (or global pose) relative to the parent Skeleton's rest in model space and transfers it to the child Skeleton. + This modifier rewrites the pose of the child skeleton directly in the parent skeleton's update process. This means that it overwrites the mapped bone pose set in the normal process on the target skeleton. If you want to set the target skeleton bone pose after retargeting, you will need to add a child [SkeletonModifier3D] to the target skeleton and thereby modify the pose. + [b]Note:[/b] When the [member use_global_pose] is enabled, even if it is an unmapped bone, it can cause visual problems because the global pose is applied ignoring the parent bone's pose [b]if it has mapped bone children[/b]. See also [member use_global_pose]. + + + + + + If [code]true[/code], allows to retarget the position. + + + [SkeletonProfile] for retargeting bones with names matching the bone list. + + + If [code]true[/code], allows to retarget the rotation. + + + If [code]true[/code], allows to retarget the scale. + + + If [code]false[/code], in case the target skeleton has fewer bones than the source skeleton, the source bone parent's transform will be ignored. + Instead, it is possible to retarget between models with different body shapes, and position, rotation, and scale can be retargeted separately. + If [code]true[/code], Retargeting is performed taking into account global pose. + In case the target skeleton has fewer bones than the source skeleton, the source bone parent's transform is taken into account. However, bone length between skeletons must match exactly, if not, the bones will be forced to expand or shrink. + This is useful for using dummy bone which length is [b]0[/b] to match postures when retargeting between models with different number of bones. + + + diff --git a/doc/classes/Skeleton3D.xml b/doc/classes/Skeleton3D.xml index f5b808be8e24..ef23c5bfb6c2 100644 --- a/doc/classes/Skeleton3D.xml +++ b/doc/classes/Skeleton3D.xml @@ -393,6 +393,10 @@ [b]Note:[/b] During the update process, this signal is not fired, so modification by [SkeletonModifier3D] is not detected. + + + + Emitted when the value of [member show_rest_only] changes. diff --git a/editor/icons/RetargetModifier3D.svg b/editor/icons/RetargetModifier3D.svg new file mode 100644 index 000000000000..2ca7af6c6e4e --- /dev/null +++ b/editor/icons/RetargetModifier3D.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/editor/import/3d/post_import_plugin_skeleton_renamer.cpp b/editor/import/3d/post_import_plugin_skeleton_renamer.cpp index 3f6bfdcf0582..700b2bc7199e 100644 --- a/editor/import/3d/post_import_plugin_skeleton_renamer.cpp +++ b/editor/import/3d/post_import_plugin_skeleton_renamer.cpp @@ -39,7 +39,7 @@ void PostImportPluginSkeletonRenamer::get_internal_import_options(InternalImportCategory p_category, List *r_options) { if (p_category == INTERNAL_IMPORT_CATEGORY_SKELETON_3D_NODE) { - r_options->push_back(ResourceImporter::ImportOption(PropertyInfo(Variant::BOOL, "retarget/bone_renamer/rename_bones"), true)); + r_options->push_back(ResourceImporter::ImportOption(PropertyInfo(Variant::BOOL, "retarget/bone_renamer/rename_bones", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_UPDATE_ALL_IF_MODIFIED), true)); r_options->push_back(ResourceImporter::ImportOption(PropertyInfo(Variant::BOOL, "retarget/bone_renamer/unique_node/make_unique"), true)); r_options->push_back(ResourceImporter::ImportOption(PropertyInfo(Variant::STRING, "retarget/bone_renamer/unique_node/skeleton_name"), "GeneralSkeleton")); } diff --git a/editor/import/3d/post_import_plugin_skeleton_rest_fixer.cpp b/editor/import/3d/post_import_plugin_skeleton_rest_fixer.cpp index 64bec0532baf..348ac38f1149 100644 --- a/editor/import/3d/post_import_plugin_skeleton_rest_fixer.cpp +++ b/editor/import/3d/post_import_plugin_skeleton_rest_fixer.cpp @@ -33,6 +33,7 @@ #include "editor/import/3d/scene_import_settings.h" #include "scene/3d/bone_attachment_3d.h" #include "scene/3d/importer_mesh_instance_3d.h" +#include "scene/3d/retarget_modifier_3d.h" #include "scene/3d/skeleton_3d.h" #include "scene/animation/animation_player.h" #include "scene/resources/bone_map.h" @@ -42,8 +43,19 @@ void PostImportPluginSkeletonRestFixer::get_internal_import_options(InternalImpo r_options->push_back(ResourceImporter::ImportOption(PropertyInfo(Variant::BOOL, "retarget/rest_fixer/apply_node_transforms"), true)); r_options->push_back(ResourceImporter::ImportOption(PropertyInfo(Variant::BOOL, "retarget/rest_fixer/normalize_position_tracks"), true)); r_options->push_back(ResourceImporter::ImportOption(PropertyInfo(Variant::BOOL, "retarget/rest_fixer/reset_all_bone_poses_after_import"), true)); - r_options->push_back(ResourceImporter::ImportOption(PropertyInfo(Variant::BOOL, "retarget/rest_fixer/overwrite_axis", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_UPDATE_ALL_IF_MODIFIED), true)); + + r_options->push_back(ResourceImporter::ImportOption(PropertyInfo(Variant::INT, "retarget/rest_fixer/retarget_method", PROPERTY_HINT_ENUM, "None,Overwrite Axis,Use Retarget Modifier", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_UPDATE_ALL_IF_MODIFIED), 1)); r_options->push_back(ResourceImporter::ImportOption(PropertyInfo(Variant::BOOL, "retarget/rest_fixer/keep_global_rest_on_leftovers"), true)); + String skeleton_bones_must_be_renamed_warning = String( + "The skeleton modifier option uses SkeletonProfile as a list of bone names and retargets by name matching." + "Without renaming, retargeting by modifier will not work and the track path of the animation will be broken and it will be not playbacked correctly."); // TODO: translate. + r_options->push_back(ResourceImporter::ImportOption( + PropertyInfo( + Variant::STRING, U"retarget/rest_fixer/\u26A0_validation_warning/skeleton_bones_must_be_renamed", + PROPERTY_HINT_MULTILINE_TEXT, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_READ_ONLY), + Variant(skeleton_bones_must_be_renamed_warning))); + r_options->push_back(ResourceImporter::ImportOption(PropertyInfo(Variant::BOOL, "retarget/rest_fixer/use_global_pose"), true)); + r_options->push_back(ResourceImporter::ImportOption(PropertyInfo(Variant::STRING, "retarget/rest_fixer/original_skeleton_name"), "OriginalSkeleton")); r_options->push_back(ResourceImporter::ImportOption(PropertyInfo(Variant::BOOL, "retarget/rest_fixer/fix_silhouette/enable", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_UPDATE_ALL_IF_MODIFIED), false)); // TODO: PostImportPlugin need to be implemented such as validate_option(PropertyInfo &property, const Dictionary &p_options). // get_internal_option_visibility() is not sufficient because it can only retrieve options implemented in the core and can only read option values. @@ -63,7 +75,11 @@ Variant PostImportPluginSkeletonRestFixer::get_internal_option_visibility(Intern } } } else if (p_option == "retarget/rest_fixer/keep_global_rest_on_leftovers") { - return bool(p_options["retarget/rest_fixer/overwrite_axis"]); + return int(p_options["retarget/rest_fixer/retarget_method"]) == 1; + } else if (p_option == "retarget/rest_fixer/original_skeleton_name" || p_option == "retarget/rest_fixer/use_global_pose") { + return int(p_options["retarget/rest_fixer/retarget_method"]) == 2; + } else if (p_option.begins_with("retarget/") && p_option.ends_with("skeleton_bones_must_be_renamed")) { + return int(p_options["retarget/rest_fixer/retarget_method"]) == 2 && bool(p_options["retarget/bone_renamer/rename_bones"]) == false; } } return true; @@ -147,7 +163,7 @@ void PostImportPluginSkeletonRestFixer::internal_process(InternalImportCategory src_skeleton->set_bone_pose_position(src_idx, src_skeleton->get_bone_pose_position(src_idx) * scl); } - // Fix animation. + // Fix animation by changing node transform. bones_to_process = src_skeleton->get_parentless_bones(); { TypedArray nodes = p_base_scene->find_children("*", "AnimationPlayer"); @@ -224,6 +240,10 @@ void PostImportPluginSkeletonRestFixer::internal_process(InternalImportCategory List anims; ap->get_animation_list(&anims); for (const StringName &name : anims) { + if (String(name).contains("/")) { + continue; // Avoid animation library which may be created by importer dynamically. + } + Ref anim = ap->get_animation(name); int track_len = anim->get_track_count(); @@ -454,8 +474,13 @@ void PostImportPluginSkeletonRestFixer::internal_process(InternalImportCategory } } - // Overwrite axis. - if (bool(p_options["retarget/rest_fixer/overwrite_axis"])) { + bool is_using_modifier = int(p_options["retarget/rest_fixer/retarget_method"]) == 2; + bool is_using_global_pose = bool(p_options["retarget/rest_fixer/use_global_pose"]); + Skeleton3D *orig_skeleton = nullptr; + Skeleton3D *profile_skeleton = nullptr; + + // Retarget in some way. + if (int(p_options["retarget/rest_fixer/retarget_method"]) > 0) { LocalVector old_skeleton_rest; LocalVector old_skeleton_global_rest; for (int i = 0; i < src_skeleton->get_bone_count(); i++) { @@ -463,11 +488,151 @@ void PostImportPluginSkeletonRestFixer::internal_process(InternalImportCategory old_skeleton_global_rest.push_back(src_skeleton->get_bone_global_rest(i)); } + // Build structure for modifier. + if (is_using_modifier) { + orig_skeleton = src_skeleton; + + // Duplicate src_skeleton to modify animation tracks, it will memdelele after that animation track modification. + src_skeleton = memnew(Skeleton3D); + for (int i = 0; i < orig_skeleton->get_bone_count(); i++) { + src_skeleton->add_bone(orig_skeleton->get_bone_name(i)); + src_skeleton->set_bone_rest(i, orig_skeleton->get_bone_rest(i)); + src_skeleton->set_bone_pose(i, orig_skeleton->get_bone_pose(i)); + } + for (int i = 0; i < orig_skeleton->get_bone_count(); i++) { + src_skeleton->set_bone_parent(i, orig_skeleton->get_bone_parent(i)); + } + src_skeleton->set_motion_scale(orig_skeleton->get_motion_scale()); + + // Rename orig_skeleton (previous src_skeleton), since it is not animated by animation track with GeneralSkeleton. + String original_skeleton_name = String(p_options["retarget/rest_fixer/original_skeleton_name"]); + String skel_name = orig_skeleton->get_name(); + ERR_FAIL_COND_MSG(original_skeleton_name.is_empty(), "Original skeleton name cannot be empty."); + ERR_FAIL_COND_MSG(original_skeleton_name == skel_name, "Original skeleton name must be different from unique skeleton name."); + + // Rename profile skeleton to be general skeleton. + profile_skeleton = memnew(Skeleton3D); + bool is_unique = orig_skeleton->is_unique_name_in_owner(); + if (is_unique) { + orig_skeleton->set_unique_name_in_owner(false); + } + orig_skeleton->set_name(original_skeleton_name); + profile_skeleton->set_name(skel_name); + if (is_unique) { + profile_skeleton->set_unique_name_in_owner(true); + } + // Build profile skeleton bones. + int len = profile->get_bone_size(); + for (int i = 0; i < len; i++) { + profile_skeleton->add_bone(profile->get_bone_name(i)); + profile_skeleton->set_bone_rest(i, profile->get_reference_pose(i)); + } + for (int i = 0; i < len; i++) { + int target_parent = profile_skeleton->find_bone(profile->get_bone_parent(i)); + if (target_parent >= 0) { + profile_skeleton->set_bone_parent(i, target_parent); + } + } + for (int i = 0; i < len; i++) { + Vector3 origin = Vector3(0, 0, 0); + int found = orig_skeleton->find_bone(profile->get_bone_name(i)); + String parent_name = profile->get_bone_parent(i); + if (found >= 0) { + origin = orig_skeleton->get_bone_global_rest(found).origin; + if (profile->get_bone_name(i) != profile->get_root_bone()) { + int src_parent = -1; + while (src_parent < 0 && !parent_name.is_empty()) { + src_parent = orig_skeleton->find_bone(parent_name); + parent_name = profile->get_bone_parent(profile->find_bone(parent_name)); + } + if (src_parent >= 0) { + Transform3D parent_grest = orig_skeleton->get_bone_global_rest(src_parent); + origin = origin - parent_grest.origin; + } + } + } + int target_parent = profile_skeleton->find_bone(profile->get_bone_parent(i)); + if (target_parent >= 0) { + origin = profile_skeleton->get_bone_global_rest(target_parent).basis.get_rotation_quaternion().xform_inv(origin); + } + profile_skeleton->set_bone_rest(i, Transform3D(profile_skeleton->get_bone_rest(i).basis, origin)); + } + profile_skeleton->set_motion_scale(orig_skeleton->get_motion_scale()); + profile_skeleton->reset_bone_poses(); + // Make structure with modifier. + Node *owner = p_node->get_owner(); + + Node *pr = orig_skeleton->get_parent(); + pr->add_child(profile_skeleton); + profile_skeleton->set_owner(owner); + + RetargetModifier3D *mod = memnew(RetargetModifier3D); + profile_skeleton->add_child(mod); + mod->set_owner(owner); + mod->set_name("RetargetModifier3D"); + + orig_skeleton->set_owner(nullptr); + orig_skeleton->reparent(mod, false); + orig_skeleton->set_owner(owner); + orig_skeleton->set_unique_name_in_owner(true); + + mod->set_use_global_pose(is_using_global_pose); + mod->set_profile(profile); + + // Fix skeleton name in animation. + // Mapped skeleton is animated by %GenerarSkeleton:RenamedBoneName. + // Unmapped skeleton is animated by %OriginalSkeleton:OriginalBoneName. + if (is_using_modifier) { + TypedArray nodes = p_base_scene->find_children("*", "AnimationPlayer"); + String general_skeleton_pathname = UNIQUE_NODE_PREFIX + profile_skeleton->get_name(); + while (nodes.size()) { + AnimationPlayer *ap = Object::cast_to(nodes.pop_back()); + List anims; + ap->get_animation_list(&anims); + for (const StringName &name : anims) { + Ref anim = ap->get_animation(name); + int track_len = anim->get_track_count(); + for (int i = 0; i < track_len; i++) { + if (anim->track_get_path(i).get_name_count() == 0) { + return; + } + if (anim->track_get_path(i).get_name(0) == general_skeleton_pathname) { + bool replace = false; + if (anim->track_get_path(i).get_subname_count() > 0) { + int found = profile_skeleton->find_bone(anim->track_get_path(i).get_concatenated_subnames()); + if (found < 0) { + replace = true; + } + } else { + replace = true; + } + if (replace) { + String path_string = UNIQUE_NODE_PREFIX + original_skeleton_name; + if (anim->track_get_path(i).get_name_count() > 1) { + Vector names = anim->track_get_path(i).get_names(); + names.remove_at(0); + for (int j = 0; j < names.size(); j++) { + path_string += "/" + names[i].operator String(); + } + } + if (anim->track_get_path(i).get_subname_count() > 0) { + path_string = path_string + String(":") + anim->track_get_path(i).get_concatenated_subnames(); + } + anim->track_set_path(i, path_string); + } + } + } + } + } + } + } + bool keep_global_rest_leftovers = bool(p_options["retarget/rest_fixer/keep_global_rest_on_leftovers"]); // Scan hierarchy and populate a whitelist of unmapped bones without mapped descendants. + // When both is_using_modifier and is_using_global_pose are enabled, this array is used for detecting warning. Vector keep_bone_rest; - if (keep_global_rest_leftovers) { + if (is_using_modifier || keep_global_rest_leftovers) { Vector bones_to_process = src_skeleton->get_parentless_bones(); while (bones_to_process.size() > 0) { int src_idx = bones_to_process[0]; @@ -526,12 +691,14 @@ void PostImportPluginSkeletonRestFixer::internal_process(InternalImportCategory if (src_parent_idx >= 0) { src_pg = src_skeleton->get_bone_global_rest(src_parent_idx).basis; } - int prof_idx = profile->find_bone(src_bone_name); if (prof_idx >= 0) { - tgt_rot = src_pg.inverse() * prof_skeleton->get_bone_global_rest(prof_idx).basis; // Mapped bone uses reference pose. + // Mapped bone uses reference pose. + // It is fine to change rest here even though is_using_modifier is enabled, since next process is aborted with unmapped bones. + tgt_rot = src_pg.inverse() * prof_skeleton->get_bone_global_rest(prof_idx).basis; } else if (keep_global_rest_leftovers && keep_bone_rest.has(src_idx)) { - tgt_rot = src_pg.inverse() * old_skeleton_global_rest[src_idx].basis; // Non-Mapped bone without mapped children keeps global rest. + // Non-Mapped bones without mapped children keeps global rest. + tgt_rot = src_pg.inverse() * old_skeleton_global_rest[src_idx].basis; } } @@ -548,7 +715,8 @@ void PostImportPluginSkeletonRestFixer::internal_process(InternalImportCategory src_skeleton->set_bone_rest(src_idx, Transform3D(tgt_rot, diff.xform(src_skeleton->get_bone_rest(src_idx).origin))); } - // Fix animation. + // Fix animation by changing rest. + bool warning_detected = false; { TypedArray nodes = p_base_scene->find_children("*", "AnimationPlayer"); while (nodes.size()) { @@ -573,7 +741,9 @@ void PostImportPluginSkeletonRestFixer::internal_process(InternalImportCategory ERR_CONTINUE(!node); Skeleton3D *track_skeleton = Object::cast_to(node); - if (!track_skeleton || track_skeleton != src_skeleton) { + if (!track_skeleton || + (is_using_modifier && track_skeleton != profile_skeleton && track_skeleton != orig_skeleton) || + (!is_using_modifier && track_skeleton != src_skeleton)) { continue; } @@ -584,6 +754,16 @@ void PostImportPluginSkeletonRestFixer::internal_process(InternalImportCategory int bone_idx = src_skeleton->find_bone(bn); + if (is_using_modifier) { + int prof_idx = profile->find_bone(bn); + if (prof_idx < 0) { + if (keep_bone_rest.has(bone_idx)) { + warning_detected = true; + } + continue; // If is_using_modifier, the original skeleton rest is not changed. + } + } + Transform3D old_rest = old_skeleton_rest[bone_idx]; Transform3D new_rest = src_skeleton->get_bone_rest(bone_idx); Transform3D old_pg; @@ -629,6 +809,10 @@ void PostImportPluginSkeletonRestFixer::internal_process(InternalImportCategory } } } + if (is_using_global_pose && warning_detected) { + WARN_PRINT_ED("Animated extra bone between mapped bones detected, consider disabling Use Global Animation option to prevent that the pose origin be overridden by the RetargetModifier3D."); + } + if (p_options.has("retarget/rest_fixer/reset_all_bone_poses_after_import") && !bool(p_options["retarget/rest_fixer/reset_all_bone_poses_after_import"])) { // If Reset All Bone Poses After Import is disabled, preserve the original bone pose, adjusted for the new bone rolls. for (int bone_idx = 0; bone_idx < src_skeleton->get_bone_count(); bone_idx++) { @@ -654,6 +838,11 @@ void PostImportPluginSkeletonRestFixer::internal_process(InternalImportCategory } } + if (is_using_modifier) { + memdelete(src_skeleton); + src_skeleton = profile_skeleton; + } + is_rest_changed = true; } @@ -681,7 +870,9 @@ void PostImportPluginSkeletonRestFixer::internal_process(InternalImportCategory ERR_CONTINUE(!node); Skeleton3D *track_skeleton = Object::cast_to(node); - if (!track_skeleton || track_skeleton != src_skeleton) { + if (!track_skeleton || + (is_using_modifier && track_skeleton != profile_skeleton && track_skeleton != orig_skeleton) || + (!is_using_modifier && track_skeleton != src_skeleton)) { continue; } @@ -696,7 +887,7 @@ void PostImportPluginSkeletonRestFixer::internal_process(InternalImportCategory } } - if (is_rest_changed) { + if (!is_using_modifier && is_rest_changed) { // Fix skin. { HashSet> mutated_skins; @@ -766,6 +957,14 @@ void PostImportPluginSkeletonRestFixer::internal_process(InternalImportCategory src_skeleton->set_bone_pose_rotation(i, fixed_rest.basis.get_rotation_quaternion()); src_skeleton->set_bone_pose_scale(i, fixed_rest.basis.get_scale()); } + if (orig_skeleton) { + for (int i = 0; i < orig_skeleton->get_bone_count(); i++) { + Transform3D fixed_rest = orig_skeleton->get_bone_rest(i); + orig_skeleton->set_bone_pose_position(i, fixed_rest.origin); + orig_skeleton->set_bone_pose_rotation(i, fixed_rest.basis.get_rotation_quaternion()); + orig_skeleton->set_bone_pose_scale(i, fixed_rest.basis.get_scale()); + } + } } } diff --git a/editor/import/3d/post_import_plugin_skeleton_track_organizer.cpp b/editor/import/3d/post_import_plugin_skeleton_track_organizer.cpp index 53bcc59fcb16..b5f84b6b4e65 100644 --- a/editor/import/3d/post_import_plugin_skeleton_track_organizer.cpp +++ b/editor/import/3d/post_import_plugin_skeleton_track_organizer.cpp @@ -39,7 +39,7 @@ void PostImportPluginSkeletonTrackOrganizer::get_internal_import_options(Interna if (p_category == INTERNAL_IMPORT_CATEGORY_SKELETON_3D_NODE) { r_options->push_back(ResourceImporter::ImportOption(PropertyInfo(Variant::BOOL, "retarget/remove_tracks/except_bone_transform"), false)); r_options->push_back(ResourceImporter::ImportOption(PropertyInfo(Variant::BOOL, "retarget/remove_tracks/unimportant_positions"), true)); - r_options->push_back(ResourceImporter::ImportOption(PropertyInfo(Variant::BOOL, "retarget/remove_tracks/unmapped_bones"), false)); + r_options->push_back(ResourceImporter::ImportOption(PropertyInfo(Variant::INT, "retarget/remove_tracks/unmapped_bones", PROPERTY_HINT_ENUM, "None,Remove,Separate Library"), 0)); } } @@ -61,9 +61,9 @@ void PostImportPluginSkeletonTrackOrganizer::internal_process(InternalImportCate } bool remove_except_bone = bool(p_options["retarget/remove_tracks/except_bone_transform"]); bool remove_positions = bool(p_options["retarget/remove_tracks/unimportant_positions"]); - bool remove_unmapped_bones = bool(p_options["retarget/remove_tracks/unmapped_bones"]); + int separate_unmapped_bones = int(p_options["retarget/remove_tracks/unmapped_bones"]); - if (!remove_positions && !remove_unmapped_bones) { + if (!remove_positions && separate_unmapped_bones == 0) { return; } @@ -72,10 +72,16 @@ void PostImportPluginSkeletonTrackOrganizer::internal_process(InternalImportCate AnimationPlayer *ap = Object::cast_to(nodes.pop_back()); List anims; ap->get_animation_list(&anims); + + Ref unmapped_al; + unmapped_al.instantiate(); + for (const StringName &name : anims) { Ref anim = ap->get_animation(name); int track_len = anim->get_track_count(); Vector remove_indices; + Vector mapped_bone_indices; + Vector unmapped_bone_indices; for (int i = 0; i < track_len; i++) { String track_path = String(anim->track_get_path(i).get_concatenated_names()); Node *node = (ap->get_node(ap->get_root_node()))->get_node(NodePath(track_path)); @@ -96,16 +102,19 @@ void PostImportPluginSkeletonTrackOrganizer::internal_process(InternalImportCate StringName bn = anim->track_get_path(i).get_subname(0); if (bn) { int prof_idx = profile->find_bone(bone_map->find_profile_bone_name(bn)); - if (remove_unmapped_bones && prof_idx < 0) { - remove_indices.push_back(i); + if (prof_idx < 0) { + unmapped_bone_indices.push_back(i); continue; } if (remove_positions && anim->track_get_type(i) == Animation::TYPE_POSITION_3D && prof_idx >= 0) { StringName prof_bn = profile->get_bone_name(prof_idx); if (prof_bn == profile->get_root_bone() || prof_bn == profile->get_scale_base_bone()) { + mapped_bone_indices.push_back(i); continue; } remove_indices.push_back(i); + } else { + mapped_bone_indices.push_back(i); } } } @@ -114,11 +123,36 @@ void PostImportPluginSkeletonTrackOrganizer::internal_process(InternalImportCate } } + if (separate_unmapped_bones == 2 && !unmapped_bone_indices.is_empty()) { + Ref unmapped_anim = anim->duplicate(); + Vector to_delete; + to_delete.append_array(mapped_bone_indices); + to_delete.append_array(remove_indices); + to_delete.sort(); + to_delete.reverse(); + for (int i = 0; i < to_delete.size(); i++) { + unmapped_anim->remove_track(to_delete[i]); + } + unmapped_al->add_animation(name, unmapped_anim); + } + + if (separate_unmapped_bones >= 1) { + remove_indices.append_array(unmapped_bone_indices); + remove_indices.sort(); + } remove_indices.reverse(); for (int i = 0; i < remove_indices.size(); i++) { anim->remove_track(remove_indices[i]); } } + + if (unmapped_al->get_animation_list_size() == 0) { + unmapped_al.unref(); + } else { + if (separate_unmapped_bones == 2) { + ap->add_animation_library("unmapped_bones", unmapped_al); + } + } } } } diff --git a/editor/import/3d/resource_importer_scene.cpp b/editor/import/3d/resource_importer_scene.cpp index cb348f713c12..c0175c61a1ef 100644 --- a/editor/import/3d/resource_importer_scene.cpp +++ b/editor/import/3d/resource_importer_scene.cpp @@ -2314,6 +2314,7 @@ bool ResourceImporterScene::get_internal_option_visibility(InternalImportCategor } } + // TODO: If there are more than 2 or equal get_internal_option_visibility method, visibility state is broken. for (int i = 0; i < post_importer_plugins.size(); i++) { Variant ret = post_importer_plugins.write[i]->get_internal_option_visibility(EditorScenePostImportPlugin::InternalImportCategory(p_category), _scene_import_type, p_option, p_options); if (ret.get_type() == Variant::BOOL) { diff --git a/scene/3d/node_3d.cpp b/scene/3d/node_3d.cpp index 86ce8a881a2f..ddaca87750a3 100644 --- a/scene/3d/node_3d.cpp +++ b/scene/3d/node_3d.cpp @@ -841,10 +841,10 @@ void Node3D::reparent(Node *p_parent, bool p_keep_global_transform) { ERR_THREAD_GUARD; if (p_keep_global_transform) { Transform3D temp = get_global_transform(); - Node::reparent(p_parent); + Node::reparent(p_parent, p_keep_global_transform); set_global_transform(temp); } else { - Node::reparent(p_parent); + Node::reparent(p_parent, p_keep_global_transform); } } diff --git a/scene/3d/retarget_modifier_3d.cpp b/scene/3d/retarget_modifier_3d.cpp new file mode 100644 index 000000000000..d9ddf2ff3727 --- /dev/null +++ b/scene/3d/retarget_modifier_3d.cpp @@ -0,0 +1,438 @@ +/**************************************************************************/ +/* retarget_modifier_3d.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 "retarget_modifier_3d.h" + +PackedStringArray RetargetModifier3D::get_configuration_warnings() const { + PackedStringArray warnings = SkeletonModifier3D::get_configuration_warnings(); + if (child_skeletons.is_empty()) { + warnings.push_back(RTR("There is no child Skeleton3D!")); + } + return warnings; +} + +/// Caching + +void RetargetModifier3D::_profile_changed(Ref p_old, Ref p_new) { + if (p_old.is_valid() && p_old->is_connected(SNAME("profile_updated"), callable_mp(this, &RetargetModifier3D::cache_rests_with_reset))) { + p_old->disconnect(SNAME("profile_updated"), callable_mp(this, &RetargetModifier3D::cache_rests_with_reset)); + } + profile = p_new; + if (p_new.is_valid() && !p_new->is_connected(SNAME("profile_updated"), callable_mp(this, &RetargetModifier3D::cache_rests_with_reset))) { + p_new->connect(SNAME("profile_updated"), callable_mp(this, &RetargetModifier3D::cache_rests_with_reset)); + } + cache_rests_with_reset(); +} + +void RetargetModifier3D::_skeleton_changed(Skeleton3D *p_old, Skeleton3D *p_new) { + if (p_old && p_old->is_connected(SNAME("rest_updated"), callable_mp(this, &RetargetModifier3D::cache_rests))) { + p_old->disconnect(SNAME("rest_updated"), callable_mp(this, &RetargetModifier3D::cache_rests)); + } + if (p_new && !p_new->is_connected(SNAME("rest_updated"), callable_mp(this, &RetargetModifier3D::cache_rests))) { + p_new->connect(SNAME("rest_updated"), callable_mp(this, &RetargetModifier3D::cache_rests)); + } + cache_rests(); +} + +void RetargetModifier3D::cache_rests_with_reset() { + _reset_child_skeleton_poses(); + cache_rests(); +} + +void RetargetModifier3D::cache_rests() { + source_bone_ids.clear(); + + Skeleton3D *source_skeleton = get_skeleton(); + if (profile.is_null() || !source_skeleton) { + return; + } + + PackedStringArray bone_names = profile->get_bone_names(); + for (const String &E : bone_names) { + source_bone_ids.push_back(source_skeleton->find_bone(E)); + } + + for (int i = 0; i < child_skeletons.size(); i++) { + _update_child_skeleton_rests(i); + } +}; + +Vector RetargetModifier3D::cache_bone_global_rests(Skeleton3D *p_skeleton) { + // Retarget global pose in model space: + // tgt_global_pose.basis = src_global_pose.basis * src_rest.basis.inv * src_parent_global_rest.basis.inv * tgt_parent_global_rest.basis * tgt_rest.basis + // tgt_global_pose.origin = src_global_pose.origin + Skeleton3D *source_skeleton = get_skeleton(); + Vector bone_rests; + if (profile.is_null() || !source_skeleton) { + return bone_rests; + } + PackedStringArray bone_names = profile->get_bone_names(); + for (const String &E : bone_names) { + RetargetBoneInfo rbi; + int source_bone_id = source_skeleton->find_bone(E); + if (source_bone_id >= 0) { + Transform3D parent_global_rest = Transform3D(); + int bone_parent = source_skeleton->get_bone_parent(source_bone_id); + if (bone_parent >= 0) { + parent_global_rest = source_skeleton->get_bone_global_rest(bone_parent); + } + rbi.post_basis = source_skeleton->get_bone_rest(source_bone_id).basis.inverse() * parent_global_rest.basis.inverse(); + } + int target_bone_id = p_skeleton->find_bone(E); + rbi.bone_id = target_bone_id; + if (target_bone_id >= 0) { + Transform3D parent_global_rest = Transform3D(); + int bone_parent = p_skeleton->get_bone_parent(target_bone_id); + if (bone_parent >= 0) { + parent_global_rest = p_skeleton->get_bone_global_rest(bone_parent); + } + rbi.post_basis = rbi.post_basis * parent_global_rest.basis * p_skeleton->get_bone_rest(target_bone_id).basis; + } + bone_rests.push_back(rbi); + } + return bone_rests; +} + +Vector RetargetModifier3D::cache_bone_rests(Skeleton3D *p_skeleton) { + // Retarget pose in model space: + // tgt_pose.basis = tgt_parent_global_rest.basis.inv * src_parent_global_rest.basis * src_pose.basis * src_rest.basis.inv * src_parent_global_rest.basis.inv * tgt_parent_global_rest.basis * tgt_rest.basis + // tgt_pose.origin = tgt_parent_global_rest.basis.inv.xform(src_parent_global_rest.basis.xform(src_pose.origin - src_rest.origin)) + tgt_rest.origin + Skeleton3D *source_skeleton = get_skeleton(); + Vector bone_rests; + if (profile.is_null() || !source_skeleton) { + return bone_rests; + } + PackedStringArray bone_names = profile->get_bone_names(); + for (const String &E : bone_names) { + RetargetBoneInfo rbi; + int source_bone_id = source_skeleton->find_bone(E); + if (source_bone_id >= 0) { + Transform3D parent_global_rest = Transform3D(); + int bone_parent = source_skeleton->get_bone_parent(source_bone_id); + if (bone_parent >= 0) { + parent_global_rest = source_skeleton->get_bone_global_rest(bone_parent); + } + rbi.pre_basis = parent_global_rest.basis; + rbi.post_basis = source_skeleton->get_bone_rest(source_bone_id).basis.inverse() * parent_global_rest.basis.inverse(); + } + + int target_bone_id = p_skeleton->find_bone(E); + rbi.bone_id = target_bone_id; + if (target_bone_id >= 0) { + Transform3D parent_global_rest = Transform3D(); + int bone_parent = p_skeleton->get_bone_parent(target_bone_id); + if (bone_parent >= 0) { + parent_global_rest = p_skeleton->get_bone_global_rest(bone_parent); + } + rbi.pre_basis = parent_global_rest.basis.inverse() * rbi.pre_basis; + rbi.post_basis = rbi.post_basis * parent_global_rest.basis * p_skeleton->get_bone_rest(target_bone_id).basis; + } + bone_rests.push_back(rbi); + } + return bone_rests; +} + +void RetargetModifier3D::_update_child_skeleton_rests(int p_child_skeleton_idx) { + ERR_FAIL_INDEX(p_child_skeleton_idx, child_skeletons.size()); + Skeleton3D *c = Object::cast_to(ObjectDB::get_instance(child_skeletons[p_child_skeleton_idx].skeleton_id)); + if (!c) { + return; + } + if (use_global_pose) { + child_skeletons.write[p_child_skeleton_idx].humanoid_bone_rests = cache_bone_global_rests(c); + } else { + child_skeletons.write[p_child_skeleton_idx].humanoid_bone_rests = cache_bone_rests(c); + } +} + +void RetargetModifier3D::_update_child_skeletons() { + _reset_child_skeletons(); + + for (int i = 0; i < get_child_count(); i++) { + RetargetInfo ri; + Skeleton3D *c = Object::cast_to(get_child(i)); + if (c) { + int id = child_skeletons.size(); + ri.skeleton_id = c->get_instance_id(); + child_skeletons.push_back(ri); + c->connect(SNAME("rest_updated"), callable_mp(this, &RetargetModifier3D::_update_child_skeleton_rests).bind(id)); + } + } + + cache_rests(); + update_configuration_warnings(); +} + +void RetargetModifier3D::_reset_child_skeleton_poses() { + for (const RetargetInfo &E : child_skeletons) { + Skeleton3D *c = Object::cast_to(ObjectDB::get_instance(E.skeleton_id)); + if (!c) { + continue; + } + if (c->is_connected(SNAME("rest_updated"), callable_mp(this, &RetargetModifier3D::_update_child_skeleton_rests))) { + c->disconnect(SNAME("rest_updated"), callable_mp(this, &RetargetModifier3D::_update_child_skeleton_rests)); + } + for (const RetargetBoneInfo &F : E.humanoid_bone_rests) { + if (F.bone_id < 0) { + continue; + } + c->reset_bone_pose(F.bone_id); + } + } +} + +void RetargetModifier3D::_reset_child_skeletons() { + _reset_child_skeleton_poses(); + child_skeletons.clear(); +} + +/// General functions + +void RetargetModifier3D::add_child_notify(Node *p_child) { + if (Object::cast_to(p_child)) { + _update_child_skeletons(); + } +} + +void RetargetModifier3D::move_child_notify(Node *p_child) { + if (Object::cast_to(p_child)) { + _update_child_skeletons(); + } +} + +void RetargetModifier3D::remove_child_notify(Node *p_child) { + if (Object::cast_to(p_child)) { + // Reset after process. + callable_mp(this, &RetargetModifier3D::_update_child_skeletons).call_deferred(); + } +} + +void RetargetModifier3D::_validate_property(PropertyInfo &p_property) const { + if (use_global_pose) { + if (p_property.name == "position_enabled" || p_property.name == "rotation_enabled" || p_property.name == "scale_enabled") { + p_property.usage = PROPERTY_USAGE_NONE; + } + } +} + +void RetargetModifier3D::_bind_methods() { + ClassDB::bind_method(D_METHOD("set_profile", "profile"), &RetargetModifier3D::set_profile); + ClassDB::bind_method(D_METHOD("get_profile"), &RetargetModifier3D::get_profile); + ClassDB::bind_method(D_METHOD("set_use_global_pose", "use_global_pose"), &RetargetModifier3D::set_use_global_pose); + ClassDB::bind_method(D_METHOD("is_using_global_pose"), &RetargetModifier3D::is_using_global_pose); + ClassDB::bind_method(D_METHOD("set_position_enabled", "enabled"), &RetargetModifier3D::set_position_enabled); + ClassDB::bind_method(D_METHOD("is_position_enabled"), &RetargetModifier3D::is_position_enabled); + ClassDB::bind_method(D_METHOD("set_rotation_enabled", "enabled"), &RetargetModifier3D::set_rotation_enabled); + ClassDB::bind_method(D_METHOD("is_rotation_enabled"), &RetargetModifier3D::is_rotation_enabled); + ClassDB::bind_method(D_METHOD("set_scale_enabled", "enabled"), &RetargetModifier3D::set_scale_enabled); + ClassDB::bind_method(D_METHOD("is_scale_enabled"), &RetargetModifier3D::is_scale_enabled); + + ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "profile", PROPERTY_HINT_RESOURCE_TYPE, "SkeletonProfile"), "set_profile", "get_profile"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "use_global_pose"), "set_use_global_pose", "is_using_global_pose"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "position_enabled"), "set_position_enabled", "is_position_enabled"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "rotation_enabled"), "set_rotation_enabled", "is_rotation_enabled"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "scale_enabled"), "set_scale_enabled", "is_scale_enabled"); +} + +void RetargetModifier3D::_set_active(bool p_active) { + if (!p_active) { + _reset_child_skeleton_poses(); + } +} + +void RetargetModifier3D::_retarget_global_pose() { + Skeleton3D *source_skeleton = get_skeleton(); + if (profile.is_null() || !source_skeleton) { + return; + } + + LocalVector source_poses; + if (influence < 1.0) { + for (int i = 0; i < source_bone_ids.size(); i++) { + source_poses.push_back(source_bone_ids[i] < 0 ? Transform3D() : source_skeleton->get_bone_global_rest(source_bone_ids[i]).interpolate_with(source_skeleton->get_bone_global_pose(source_bone_ids[i]), influence)); + } + } else { + for (int i = 0; i < source_bone_ids.size(); i++) { + source_poses.push_back(source_bone_ids[i] < 0 ? Transform3D() : source_skeleton->get_bone_global_pose(source_bone_ids[i])); + } + } + + for (const RetargetInfo &E : child_skeletons) { + Skeleton3D *target_skeleton = Object::cast_to(ObjectDB::get_instance(E.skeleton_id)); + if (!target_skeleton) { + continue; + } + for (int i = 0; i < source_bone_ids.size(); i++) { + int target_bone_id = E.humanoid_bone_rests[i].bone_id; + if (target_bone_id < 0) { + continue; + } + Transform3D retarget_pose = source_poses[i]; + retarget_pose.basis = retarget_pose.basis * E.humanoid_bone_rests[i].post_basis; + target_skeleton->set_bone_global_pose(target_bone_id, retarget_pose); + } + } +} + +void RetargetModifier3D::_retarget_pose() { + Skeleton3D *source_skeleton = get_skeleton(); + if (profile.is_null() || !source_skeleton) { + return; + } + + LocalVector source_poses; + if (influence < 1.0) { + for (int i = 0; i < source_bone_ids.size(); i++) { + source_poses.push_back(source_bone_ids[i] < 0 ? Transform3D() : source_skeleton->get_bone_rest(source_bone_ids[i]).interpolate_with(source_skeleton->get_bone_pose(source_bone_ids[i]), influence)); + } + } else { + for (int i = 0; i < source_bone_ids.size(); i++) { + source_poses.push_back(source_bone_ids[i] < 0 ? Transform3D() : source_skeleton->get_bone_pose(source_bone_ids[i])); + } + } + + for (const RetargetInfo &E : child_skeletons) { + Skeleton3D *target_skeleton = Object::cast_to(ObjectDB::get_instance(E.skeleton_id)); + if (!target_skeleton) { + continue; + } + float motion_scale_ratio = target_skeleton->get_motion_scale() / source_skeleton->get_motion_scale(); + for (int i = 0; i < source_bone_ids.size(); i++) { + int target_bone_id = E.humanoid_bone_rests[i].bone_id; + if (target_bone_id < 0) { + continue; + } + int source_bone_id = source_bone_ids[i]; + if (source_bone_id < 0) { + continue; + } + + Transform3D extracted_transform = source_poses[i]; + extracted_transform.basis = E.humanoid_bone_rests[i].pre_basis * extracted_transform.basis * E.humanoid_bone_rests[i].post_basis; + extracted_transform.origin = E.humanoid_bone_rests[i].pre_basis.xform((extracted_transform.origin - source_skeleton->get_bone_rest(source_bone_id).origin) * motion_scale_ratio) + target_skeleton->get_bone_rest(target_bone_id).origin; + + Transform3D retarget_pose = target_skeleton->get_bone_pose(target_bone_id); + if (enable_position) { + retarget_pose.origin = extracted_transform.origin; + } + if (enable_rotation) { + retarget_pose.basis = extracted_transform.basis.get_rotation_quaternion(); + } + if (enable_scale) { + retarget_pose.basis.scale_local(extracted_transform.basis.get_scale()); + } + target_skeleton->set_bone_pose(target_bone_id, retarget_pose); + } + } +} + +void RetargetModifier3D::_process_modification() { + if (use_global_pose) { + _retarget_global_pose(); + } else { + _retarget_pose(); + } +} + +void RetargetModifier3D::set_profile(Ref p_profile) { + if (profile == p_profile) { + return; + } + _profile_changed(profile, p_profile); +} + +Ref RetargetModifier3D::get_profile() const { + return profile; +} + +void RetargetModifier3D::set_use_global_pose(bool p_use_global_pose) { + if (use_global_pose == p_use_global_pose) { + return; + } + + use_global_pose = p_use_global_pose; + cache_rests_with_reset(); + + notify_property_list_changed(); +} + +bool RetargetModifier3D::is_using_global_pose() const { + return use_global_pose; +} + +void RetargetModifier3D::set_position_enabled(bool p_enabled) { + if (enable_position != p_enabled) { + _reset_child_skeleton_poses(); + } + enable_position = p_enabled; +} + +bool RetargetModifier3D::is_position_enabled() const { + return enable_position; +} + +void RetargetModifier3D::set_rotation_enabled(bool p_enabled) { + if (enable_rotation != p_enabled) { + _reset_child_skeleton_poses(); + } + enable_rotation = p_enabled; +} + +bool RetargetModifier3D::is_rotation_enabled() const { + return enable_rotation; +} + +void RetargetModifier3D::set_scale_enabled(bool p_enabled) { + if (enable_scale != p_enabled) { + _reset_child_skeleton_poses(); + } + enable_scale = p_enabled; +} + +bool RetargetModifier3D::is_scale_enabled() const { + return enable_scale; +} + +void RetargetModifier3D::_notification(int p_what) { + switch (p_what) { + case NOTIFICATION_ENTER_TREE: { + _update_child_skeletons(); + } break; + case NOTIFICATION_EXIT_TREE: { + _reset_child_skeletons(); + } break; + } +} + +RetargetModifier3D::RetargetModifier3D() { +} + +RetargetModifier3D::~RetargetModifier3D() { +} diff --git a/scene/3d/retarget_modifier_3d.h b/scene/3d/retarget_modifier_3d.h new file mode 100644 index 000000000000..75112d74bf27 --- /dev/null +++ b/scene/3d/retarget_modifier_3d.h @@ -0,0 +1,110 @@ +/**************************************************************************/ +/* retarget_modifier_3d.h */ +/**************************************************************************/ +/* 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. */ +/**************************************************************************/ + +#ifndef RETARGET_MODIFIER_3D_H +#define RETARGET_MODIFIER_3D_H + +#include "scene/3d/skeleton_modifier_3d.h" +#include "scene/resources/skeleton_profile.h" + +class RetargetModifier3D : public SkeletonModifier3D { + GDCLASS(RetargetModifier3D, SkeletonModifier3D); + + Ref profile; + + bool use_global_pose = false; + bool enable_position = true; + bool enable_rotation = true; + bool enable_scale = true; + + struct RetargetBoneInfo { + int bone_id = -1; + Basis pre_basis; + Basis post_basis; + }; + + struct RetargetInfo { + ObjectID skeleton_id; + Vector humanoid_bone_rests; + }; + + Vector child_skeletons; + Vector source_bone_ids; + + void _update_child_skeleton_rests(int p_child_skeleton_idx); + void _update_child_skeletons(); + void _reset_child_skeleton_poses(); + void _reset_child_skeletons(); + + void cache_rests_with_reset(); + void cache_rests(); + Vector cache_bone_global_rests(Skeleton3D *p_skeleton); + Vector cache_bone_rests(Skeleton3D *p_skeleton); + Vector get_humanoid_bone_rests(Skeleton3D *p_skeleton); + + void _retarget_global_pose(); + void _retarget_pose(); + +protected: + virtual void _skeleton_changed(Skeleton3D *p_old, Skeleton3D *p_new) override; + void _profile_changed(Ref p_old, Ref p_new); + + void _validate_property(PropertyInfo &p_property) const; + + static void _bind_methods(); + virtual void _notification(int p_what); + + virtual void add_child_notify(Node *p_child) override; + virtual void move_child_notify(Node *p_child) override; + virtual void remove_child_notify(Node *p_child) override; + + virtual void _set_active(bool p_active) override; + virtual void _process_modification() override; + +public: + virtual PackedStringArray get_configuration_warnings() const override; + + void set_use_global_pose(bool p_use_global_pose); + bool is_using_global_pose() const; + void set_position_enabled(bool p_enabled); + bool is_position_enabled() const; + void set_rotation_enabled(bool p_enabled); + bool is_rotation_enabled() const; + void set_scale_enabled(bool p_enabled); + bool is_scale_enabled() const; + + void set_profile(Ref p_profile); + Ref get_profile() const; + + RetargetModifier3D(); + virtual ~RetargetModifier3D(); +}; + +#endif // RETARGET_MODIFIER_3D_H diff --git a/scene/3d/skeleton_3d.cpp b/scene/3d/skeleton_3d.cpp index db9c4db30d06..772a7fc691ae 100644 --- a/scene/3d/skeleton_3d.cpp +++ b/scene/3d/skeleton_3d.cpp @@ -925,7 +925,12 @@ void Skeleton3D::force_update_all_bone_transforms() { for (int i = 0; i < parentless_bones.size(); i++) { force_update_bone_children_transforms(parentless_bones[i]); } - rest_dirty = false; + if (rest_dirty) { + rest_dirty = false; + emit_signal(SNAME("rest_updated")); + } else { + rest_dirty = false; + } dirty = false; if (updating) { return; @@ -1141,6 +1146,7 @@ void Skeleton3D::_bind_methods() { ADD_GROUP("Modifier", "modifier_"); ADD_PROPERTY(PropertyInfo(Variant::INT, "modifier_callback_mode_process", PROPERTY_HINT_ENUM, "Physics,Idle"), "set_modifier_callback_mode_process", "get_modifier_callback_mode_process"); + ADD_SIGNAL(MethodInfo("rest_updated")); ADD_SIGNAL(MethodInfo("pose_updated")); ADD_SIGNAL(MethodInfo("skeleton_updated")); ADD_SIGNAL(MethodInfo("bone_enabled_changed", PropertyInfo(Variant::INT, "bone_idx"))); diff --git a/scene/3d/skeleton_3d.h b/scene/3d/skeleton_3d.h index 07bdeccf2f76..9dacad284e42 100644 --- a/scene/3d/skeleton_3d.h +++ b/scene/3d/skeleton_3d.h @@ -210,6 +210,7 @@ class Skeleton3D : public Node3D { // Skeleton creation API uint64_t get_version() const; int add_bone(const String &p_name); + void remove_bone(int p_bone); int find_bone(const String &p_name) const; String get_bone_name(int p_bone) const; void set_bone_name(int p_bone, const String &p_name); diff --git a/scene/register_scene_types.cpp b/scene/register_scene_types.cpp index 6b1ce2b4ca7f..1de0693a2688 100644 --- a/scene/register_scene_types.cpp +++ b/scene/register_scene_types.cpp @@ -276,6 +276,7 @@ #include "scene/3d/physics/vehicle_body_3d.h" #include "scene/3d/reflection_probe.h" #include "scene/3d/remote_transform_3d.h" +#include "scene/3d/retarget_modifier_3d.h" #include "scene/3d/skeleton_3d.h" #include "scene/3d/skeleton_ik_3d.h" #include "scene/3d/skeleton_modifier_3d.h" @@ -590,6 +591,7 @@ void register_scene_types() { GDREGISTER_CLASS(Marker3D); GDREGISTER_CLASS(RootMotionView); GDREGISTER_VIRTUAL_CLASS(SkeletonModifier3D); + GDREGISTER_CLASS(RetargetModifier3D); OS::get_singleton()->yield(); // may take time to init diff --git a/scene/resources/animation_library.cpp b/scene/resources/animation_library.cpp index 22666876aea8..92612fc3d794 100644 --- a/scene/resources/animation_library.cpp +++ b/scene/resources/animation_library.cpp @@ -125,6 +125,10 @@ void AnimationLibrary::get_animation_list(List *p_animations) const } } +int AnimationLibrary::get_animation_list_size() const { + return animations.size(); +} + void AnimationLibrary::_set_data(const Dictionary &p_data) { for (KeyValue> &K : animations) { K.value->disconnect_changed(callable_mp(this, &AnimationLibrary::_animation_changed)); @@ -166,6 +170,7 @@ void AnimationLibrary::_bind_methods() { ClassDB::bind_method(D_METHOD("has_animation", "name"), &AnimationLibrary::has_animation); ClassDB::bind_method(D_METHOD("get_animation", "name"), &AnimationLibrary::get_animation); ClassDB::bind_method(D_METHOD("get_animation_list"), &AnimationLibrary::_get_animation_list); + ClassDB::bind_method(D_METHOD("get_animation_list_size"), &AnimationLibrary::get_animation_list_size); ClassDB::bind_method(D_METHOD("_set_data", "data"), &AnimationLibrary::_set_data); ClassDB::bind_method(D_METHOD("_get_data"), &AnimationLibrary::_get_data); diff --git a/scene/resources/animation_library.h b/scene/resources/animation_library.h index 00baf9d302b3..794f142744d0 100644 --- a/scene/resources/animation_library.h +++ b/scene/resources/animation_library.h @@ -61,6 +61,7 @@ class AnimationLibrary : public Resource { bool has_animation(const StringName &p_name) const; Ref get_animation(const StringName &p_name) const; void get_animation_list(List *p_animations) const; + int get_animation_list_size() const; #ifdef TOOLS_ENABLED virtual void get_argument_options(const StringName &p_function, int p_idx, List *r_options) const override; diff --git a/scene/resources/skeleton_profile.cpp b/scene/resources/skeleton_profile.cpp index c2d77ec7ff57..b76e591c257d 100644 --- a/scene/resources/skeleton_profile.cpp +++ b/scene/resources/skeleton_profile.cpp @@ -269,6 +269,14 @@ int SkeletonProfile::find_bone(const StringName &p_bone_name) const { return -1; } +PackedStringArray SkeletonProfile::get_bone_names() { + PackedStringArray s; + for (int i = 0; i < bones.size(); i++) { + s.push_back(bones[i].bone_name); + } + return s; +} + StringName SkeletonProfile::get_bone_name(int p_bone_idx) const { ERR_FAIL_INDEX_V(p_bone_idx, bones.size(), StringName()); return bones[p_bone_idx].bone_name; diff --git a/scene/resources/skeleton_profile.h b/scene/resources/skeleton_profile.h index b5a3bce940bf..9361c6a9ce39 100644 --- a/scene/resources/skeleton_profile.h +++ b/scene/resources/skeleton_profile.h @@ -97,6 +97,7 @@ class SkeletonProfile : public Resource { int find_bone(const StringName &p_bone_name) const; + PackedStringArray get_bone_names(); StringName get_bone_name(int p_bone_idx) const; void set_bone_name(int p_bone_idx, const StringName &p_bone_name);