Skip to content

Commit

Permalink
Option to use Animation as skeleton rest silhouette.
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
lyuma authored and fire committed Mar 24, 2024
1 parent fe01776 commit 9db0860
Show file tree
Hide file tree
Showing 2 changed files with 248 additions and 10 deletions.
188 changes: 183 additions & 5 deletions editor/import/3d/resource_importer_scene.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -1157,6 +1158,74 @@ Node *ResourceImporterScene::_post_fix_node(Node *p_node, Node *p_root, HashMap<
}

if (Object::cast_to<Skeleton3D>(p_node)) {
Ref<Animation> rest_animation;
float rest_animation_timestamp = 0.0;
Skeleton3D *skeleton = Object::cast_to<Skeleton3D>(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<Node> children = p_root->find_children("*", "AnimationPlayer", true, false);
for (int node_i = 0; node_i < children.size(); node_i++) {
AnimationPlayer *anim_player = cast_to<AnimationPlayer>(children[node_i]);
ERR_CONTINUE(anim_player == nullptr);
List<StringName> 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<AnimationLibrary> library(external_object);
if (library.is_valid()) {
List<StringName> 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<Resource>(), node_settings);
Expand Down Expand Up @@ -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: {
Expand Down Expand Up @@ -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<BoneMap>(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<Animation> anim(res);
if (anim.is_valid() && p_option == "rest_pose/selected_animation") {
return false;
}
Ref<AnimationLibrary> library(res);
String selected_animation_name = p_options["rest_pose/selected_animation"];
if (library.is_valid()) {
List<StringName> 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<BoneMap> bone_map;
Ref<SkeletonProfile> 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<StringName> 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: {
Expand Down Expand Up @@ -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 = "";
}
Expand Down
70 changes: 65 additions & 5 deletions editor/import/3d/scene_import_settings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ class SceneImportSettingsData : public Object {
HashMap<StringName, Variant> current;
HashMap<StringName, Variant> defaults;
List<ResourceImporter::ImportOption> options;
Vector<String> animation_list;

bool hide_options = false;
String path;

Expand Down Expand Up @@ -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)) {
Expand All @@ -109,29 +112,81 @@ class SceneImportSettingsData : public Object {
}
return false;
}
void _get_property_list(List<PropertyInfo> *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<Animation> anim(res);
Ref<AnimationLibrary> library(res);
if (anim.is_valid()) {
hint_string = anim->get_name();
}
if (library.is_valid()) {
List<StringName> 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<PropertyInfo> *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);
}
}
}
Expand Down Expand Up @@ -376,10 +431,15 @@ void SceneImportSettingsDialog::_fill_scene(Node *p_node, TreeItem *p_parent_ite

AnimationPlayer *anim_node = Object::cast_to<AnimationPlayer>(p_node);
if (anim_node) {
Vector<String> animation_list;
List<StringName> 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;
}
}

Expand Down

0 comments on commit 9db0860

Please sign in to comment.