Procházet zdrojové kódy

Merge pull request #61610 from TokageItLab/importer-retarget-registered-gdhumanoid

Rémi Verschelde před 3 roky
rodič
revize
a2459c7d35
33 změnil soubory, kde provedl 2005 přidání a 14 odebrání
  1. 15 0
      core/string/node_path.cpp
  2. 2 0
      core/string/node_path.h
  3. 1 0
      core/variant/variant_call.cpp
  4. 56 0
      doc/classes/BoneMap.xml
  5. 3 1
      doc/classes/EditorScenePostImportPlugin.xml
  6. 6 0
      doc/classes/NodePath.xml
  7. 106 0
      doc/classes/SkeletonProfile.xml
  8. 14 0
      doc/classes/SkeletonProfileHumanoid.xml
  9. 2 0
      editor/editor_node.cpp
  10. 0 0
      editor/icons/BoneMapHumanBody.svg
  11. 1 0
      editor/icons/BoneMapHumanFace.svg
  12. 1 0
      editor/icons/BoneMapHumanLeftHand.svg
  13. 1 0
      editor/icons/BoneMapHumanRightHand.svg
  14. 1 0
      editor/icons/BoneMapperHandle.svg
  15. 1 0
      editor/icons/BoneMapperHandleCircle.svg
  16. 1 0
      editor/icons/BoneMapperHandleSelected.svg
  17. 144 0
      editor/import/post_import_plugin_skeleton_renamer.cpp
  18. 46 0
      editor/import/post_import_plugin_skeleton_renamer.h
  19. 64 13
      editor/import/resource_importer_scene.cpp
  20. 2 0
      editor/import/resource_importer_scene.h
  21. 11 0
      editor/import/scene_import_settings.cpp
  22. 1 0
      editor/import/scene_import_settings.h
  23. 439 0
      editor/plugins/bone_map_editor_plugin.cpp
  24. 176 0
      editor/plugins/bone_map_editor_plugin.h
  25. 18 0
      modules/mono/glue/GodotSharp/GodotSharp/Core/NodePath.cs
  26. 5 0
      modules/mono/glue/nodepath_glue.cpp
  27. 21 0
      scene/3d/bone_attachment_3d.cpp
  28. 6 0
      scene/3d/bone_attachment_3d.h
  29. 6 0
      scene/register_scene_types.cpp
  30. 172 0
      scene/resources/bone_map.cpp
  31. 69 0
      scene/resources/bone_map.h
  32. 514 0
      scene/resources/skeleton_profile.cpp
  33. 100 0
      scene/resources/skeleton_profile.h

+ 15 - 0
core/string/node_path.cpp

@@ -199,6 +199,21 @@ Vector<StringName> NodePath::get_subnames() const {
 	return Vector<StringName>();
 }
 
+StringName NodePath::get_concatenated_names() const {
+	ERR_FAIL_COND_V(!data, StringName());
+
+	if (!data->concatenated_path) {
+		int pc = data->path.size();
+		String concatenated;
+		const StringName *sn = data->path.ptr();
+		for (int i = 0; i < pc; i++) {
+			concatenated += i == 0 ? sn[i].operator String() : "/" + sn[i];
+		}
+		data->concatenated_path = concatenated;
+	}
+	return data->concatenated_path;
+}
+
 StringName NodePath::get_concatenated_subnames() const {
 	ERR_FAIL_COND_V(!data, StringName());
 

+ 2 - 0
core/string/node_path.h

@@ -39,6 +39,7 @@ class NodePath {
 		SafeRefCount refcount;
 		Vector<StringName> path;
 		Vector<StringName> subpath;
+		StringName concatenated_path;
 		StringName concatenated_subpath;
 		bool absolute;
 		bool has_slashes;
@@ -59,6 +60,7 @@ public:
 	StringName get_subname(int p_idx) const;
 	Vector<StringName> get_names() const;
 	Vector<StringName> get_subnames() const;
+	StringName get_concatenated_names() const;
 	StringName get_concatenated_subnames() const;
 
 	NodePath rel_path_to(const NodePath &p_np) const;

+ 1 - 0
core/variant/variant_call.cpp

@@ -1806,6 +1806,7 @@ static void _register_variant_builtin_methods() {
 	bind_method(NodePath, get_subname_count, sarray(), varray());
 	bind_method(NodePath, hash, sarray(), varray());
 	bind_method(NodePath, get_subname, sarray("idx"), varray());
+	bind_method(NodePath, get_concatenated_names, sarray(), varray());
 	bind_method(NodePath, get_concatenated_subnames, sarray(), varray());
 	bind_method(NodePath, get_as_property_path, sarray(), varray());
 	bind_method(NodePath, is_empty, sarray(), varray());

+ 56 - 0
doc/classes/BoneMap.xml

@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<class name="BoneMap" inherits="Resource" version="4.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../class.xsd">
+	<brief_description>
+		Bone map for retargeting.
+	</brief_description>
+	<description>
+		This class contains a hashmap that uses a list of bone names in [SkeletonProfile] as key names.
+		By assigning the actual [Skeleton3D] bone name as the key value, it maps the [Skeleton3D] to the [SkeletonProfile].
+	</description>
+	<tutorials>
+	</tutorials>
+	<methods>
+		<method name="find_profile_bone_name" qualifiers="const">
+			<return type="StringName" />
+			<argument index="0" name="skeleton_bone_name" type="StringName" />
+			<description>
+				Returns a profile bone name having [code]skeleton_bone_name[/code]. If not found, an empty [StringName] will be returned.
+				In the retargeting process, the returned bone name is the bone name of the target skeleton.
+			</description>
+		</method>
+		<method name="get_skeleton_bone_name" qualifiers="const">
+			<return type="StringName" />
+			<argument index="0" name="profile_bone_name" type="StringName" />
+			<description>
+				Returns a skeleton bone name is mapped to [code]profile_bone_name[/code].
+				In the retargeting process, the returned bone name is the bone name of the source skeleton.
+			</description>
+		</method>
+		<method name="set_skeleton_bone_name">
+			<return type="void" />
+			<argument index="0" name="profile_bone_name" type="StringName" />
+			<argument index="1" name="skeleton_bone_name" type="StringName" />
+			<description>
+				Maps a skeleton bone name to [code]profile_bone_name[/code].
+				In the retargeting process, the setting bone name is the bone name of the source skeleton.
+			</description>
+		</method>
+	</methods>
+	<members>
+		<member name="profile" type="SkeletonProfile" setter="set_profile" getter="get_profile">
+			A [SkeletonProfile] of the mapping target. Key names in the [BoneMap] are synchronized with it.
+		</member>
+	</members>
+	<signals>
+		<signal name="bone_map_updated">
+			<description>
+				This signal is emitted when change the key value in the [BoneMap]. This is used to validate mapping and to update [BoneMap] editor.
+			</description>
+		</signal>
+		<signal name="profile_updated">
+			<description>
+				This signal is emitted when change the value in profile or change the reference of profile. This is used to update key names in the [BoneMap] and to redraw the [BoneMap] editor.
+			</description>
+		</signal>
+	</signals>
+</class>

+ 3 - 1
doc/classes/EditorScenePostImportPlugin.xml

@@ -114,7 +114,9 @@
 		</constant>
 		<constant name="INTERNAL_IMPORT_CATEGORY_ANIMATION_NODE" value="5" enum="InternalImportCategory">
 		</constant>
-		<constant name="INTERNAL_IMPORT_CATEGORY_MAX" value="6" enum="InternalImportCategory">
+		<constant name="INTERNAL_IMPORT_CATEGORY_SKELETON_3D_NODE" value="6" enum="InternalImportCategory">
+		</constant>
+		<constant name="INTERNAL_IMPORT_CATEGORY_MAX" value="7" enum="InternalImportCategory">
 		</constant>
 	</constants>
 </class>

+ 6 - 0
doc/classes/NodePath.xml

@@ -87,6 +87,12 @@
 				[/codeblocks]
 			</description>
 		</method>
+		<method name="get_concatenated_names" qualifiers="const">
+			<return type="StringName" />
+			<description>
+				Returns all paths concatenated with a slash character ([code]/[/code]) as separator without subnames.
+			</description>
+		</method>
 		<method name="get_concatenated_subnames" qualifiers="const">
 			<return type="StringName" />
 			<description>

+ 106 - 0
doc/classes/SkeletonProfile.xml

@@ -0,0 +1,106 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<class name="SkeletonProfile" inherits="Resource" version="4.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../class.xsd">
+	<brief_description>
+		Profile of a virtual skeleton used as a target for retargeting.
+	</brief_description>
+	<description>
+		This resource is used in [EditorScenePostImport]. Some parameters are referring to bones in [Skeleton3D], [Skin], [Animation], and some other nodes are rewritten based on the parameters of [SkeletonProfile].
+	</description>
+	<tutorials>
+	</tutorials>
+	<methods>
+		<method name="get_bone_name" qualifiers="const">
+			<return type="StringName" />
+			<argument index="0" name="bone_idx" type="int" />
+			<description>
+				Returns the name of the bone at [code]bone_idx[/code] that will be the key name in the [BoneMap].
+				In the retargeting process, the returned bone name is the bone name of the target skeleton.
+			</description>
+		</method>
+		<method name="get_group" qualifiers="const">
+			<return type="StringName" />
+			<argument index="0" name="bone_idx" type="int" />
+			<description>
+				Returns the group of the bone at [code]bone_idx[/code].
+			</description>
+		</method>
+		<method name="get_group_name" qualifiers="const">
+			<return type="StringName" />
+			<argument index="0" name="group_idx" type="int" />
+			<description>
+				Returns the name of the group at [code]group_idx[/code] that will be the drawing group in the [BoneMap] editor.
+			</description>
+		</method>
+		<method name="get_handle_offset" qualifiers="const">
+			<return type="Vector2" />
+			<argument index="0" name="bone_idx" type="int" />
+			<description>
+				Returns the offset of the bone at [code]bone_idx[/code] that will be the button position in the [BoneMap] editor.
+				This is the offset with origin at the top left corner of the square.
+			</description>
+		</method>
+		<method name="get_texture" qualifiers="const">
+			<return type="Texture2D" />
+			<argument index="0" name="group_idx" type="int" />
+			<description>
+				Returns the texture of the group at [code]group_idx[/code] that will be the drawing group background image in the [BoneMap] editor.
+			</description>
+		</method>
+		<method name="set_bone_name">
+			<return type="void" />
+			<argument index="0" name="bone_idx" type="int" />
+			<argument index="1" name="bone_name" type="StringName" />
+			<description>
+				Sets the name of the bone at [code]bone_idx[/code] that will be the key name in the [BoneMap].
+				In the retargeting process, the setting bone name is the bone name of the target skeleton.
+			</description>
+		</method>
+		<method name="set_group">
+			<return type="void" />
+			<argument index="0" name="bone_idx" type="int" />
+			<argument index="1" name="group" type="StringName" />
+			<description>
+				Sets the group of the bone at [code]bone_idx[/code].
+			</description>
+		</method>
+		<method name="set_group_name">
+			<return type="void" />
+			<argument index="0" name="group_idx" type="int" />
+			<argument index="1" name="group_name" type="StringName" />
+			<description>
+				Sets the name of the group at [code]group_idx[/code] that will be the drawing group in the [BoneMap] editor.
+			</description>
+		</method>
+		<method name="set_handle_offset">
+			<return type="void" />
+			<argument index="0" name="bone_idx" type="int" />
+			<argument index="1" name="handle_offset" type="Vector2" />
+			<description>
+				Sets the offset of the bone at [code]bone_idx[/code] that will be the button position in the [BoneMap] editor.
+				This is the offset with origin at the top left corner of the square.
+			</description>
+		</method>
+		<method name="set_texture">
+			<return type="void" />
+			<argument index="0" name="group_idx" type="int" />
+			<argument index="1" name="texture" type="Texture2D" />
+			<description>
+				Sets the texture of the group at [code]group_idx[/code] that will be the drawing group background image in the [BoneMap] editor.
+			</description>
+		</method>
+	</methods>
+	<members>
+		<member name="bone_size" type="int" setter="set_bone_size" getter="get_bone_size" default="0">
+		</member>
+		<member name="group_size" type="int" setter="set_group_size" getter="get_group_size" default="0">
+		</member>
+	</members>
+	<signals>
+		<signal name="profile_updated">
+			<description>
+				This signal is emitted when change the value in profile. This is used to update key name in the [BoneMap] and to redraw the [BoneMap] editor.
+				[b]Note[/b]: This signal is not connected directly to editor to simplify the reference, instead it is passed on to editor through the [BoneMap].
+			</description>
+		</signal>
+	</signals>
+</class>

+ 14 - 0
doc/classes/SkeletonProfileHumanoid.xml

@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<class name="SkeletonProfileHumanoid" inherits="SkeletonProfile" version="4.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../class.xsd">
+	<brief_description>
+	</brief_description>
+	<description>
+		A [SkeletonProfile] as a preset that is optimized for the human form. This exists for standardization, so all parameters are read-only.
+	</description>
+	<tutorials>
+	</tutorials>
+	<members>
+		<member name="bone_size" type="int" setter="set_bone_size" getter="get_bone_size" overrides="SkeletonProfile" default="56" />
+		<member name="group_size" type="int" setter="set_group_size" getter="get_group_size" overrides="SkeletonProfile" default="4" />
+	</members>
+</class>

+ 2 - 0
editor/editor_node.cpp

@@ -134,6 +134,7 @@
 #include "editor/plugins/audio_stream_editor_plugin.h"
 #include "editor/plugins/audio_stream_randomizer_editor_plugin.h"
 #include "editor/plugins/bit_map_editor_plugin.h"
+#include "editor/plugins/bone_map_editor_plugin.h"
 #include "editor/plugins/camera_3d_editor_plugin.h"
 #include "editor/plugins/canvas_item_editor_plugin.h"
 #include "editor/plugins/collision_polygon_2d_editor_plugin.h"
@@ -7144,6 +7145,7 @@ EditorNode::EditorNode() {
 	add_editor_plugin(memnew(GradientTexture2DEditorPlugin));
 	add_editor_plugin(memnew(BitMapEditorPlugin));
 	add_editor_plugin(memnew(RayCast2DEditorPlugin));
+	add_editor_plugin(memnew(BoneMapEditorPlugin));
 
 	for (int i = 0; i < EditorPlugins::get_plugin_count(); i++) {
 		add_editor_plugin(EditorPlugins::create(i));

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 0 - 0
editor/icons/BoneMapHumanBody.svg


+ 1 - 0
editor/icons/BoneMapHumanFace.svg

@@ -0,0 +1 @@
+<svg enable-background="new 0 0 1024 1024" height="1024" viewBox="0 0 1024 1024" width="1024" xmlns="http://www.w3.org/2000/svg"><path d="m0 0h1024v1024h-1024z" fill="#3f3f3f"/><path d="m788.105 552.967c17.995-57.892 31.896-124.566 30.875-198.071-3.758-270.403-249.846-251.479-295.568-244.947-359.868 51.409-219.047 452.358-220.453 496.426-4.899 153.499 83.686 170.991 161.665 215.554 2.646 1.512 7.259 1.786 13.313 1.111 7.223 25.179 11.762 59.035 9.548 75.638-3.266 24.495 209.021 24.495 209.021 0 0-62.883 12.233-124.363 33.827-188.89 7.143-2.284 16.054-7.601 25.963-16.95 13.681-12.908 34.839-21.774 45.726-63.145 15.615-59.338 3.869-76.074-13.917-76.726z" fill="#b2b2b2"/></svg>

+ 1 - 0
editor/icons/BoneMapHumanLeftHand.svg

@@ -0,0 +1 @@
+<svg enable-background="new 0 0 1024 1024" height="1024" viewBox="0 0 1024 1024" width="1024" xmlns="http://www.w3.org/2000/svg"><path d="m0 0h1024v1024h-1024z" fill="#3f3f3f"/><path d="m703.906 786.098c7.046-66.929 28.135-153.363 18.529-260.192-1.143-12.71-4.5-48.282-4.46-82.732.025-21.174-2.111-48.505-1.975-64.174.167-19.333-.428-41.584-.625-55.755-1.052-75.44-13.225-85.827-30.813-85.827-17.246 0-26.77 14.266-27.062 84.582-.061 14.42.5 51 .5 58.5 0 17.508-.333 34.167 0 53.5.447 25.955-4.279 68-9 68-3.902 0-8.099-39.299-9.575-76.999-.756-19.326-3.219-58.336-2.6-70.102 1.759-33.413.474-58.914 1.537-90.165 3.183-93.607-13.016-111.729-34.695-111.729-21.973 0-35.979 57.688-34.849 114.224.128 6.394-1.165 50.739.188 89.859.754 21.811-1.07 49.627-1.683 69.67-1.095 35.768-5.755 63.896-8.869 63.896-2.641 0-4.135-32.584-5.456-65.706-.859-21.557-4.468-58.477-3.664-83.616 1.886-59.012-1.139-110.226-1.063-121.501.635-94.955-14.66-123.101-36.052-123.101-21.476 0-37.188 30.192-36.6 123.343.067 10.53-2.62 99.926-1.759 121.816.865 21.992-2.773 65.062-3.517 84.818-1.299 34.521-6.49 63.947-9.124 63.947-3.281 0-10.794-25.638-11.724-60.965-.587-22.275 1.231-50.99.624-70.688-1.257-40.707-3.175-64.631-3.877-99.708-1.945-97.182-16.352-106.289-38.142-106.289-17.957 0-32.453 28.673-32.657 115.03-.065 27.702-2.429 62.626-.315 94.329.805 12.081-.622 42.512-1.875 73.894-.799 20.007-1.102 47.501-1.137 63.775-.17 78.595-26.712 133.424-36.555 131.308-30.333-6.521-51.648-43.918-71.219-117.307-10.551-39.566-36.667-71.149-69.9-77.813-25.9-5.193-19.783 46.161-1.319 125.293 8.65 37.068 27.909 86.227 39.566 122.655 31.653 98.917 125.574 188.563 160.903 228.546 17.146 19.403 236.894 19.403 264.59 0 11.525-8.07 43.087-101.557 45.724-126.616z" fill="#b2b2b2"/></svg>

+ 1 - 0
editor/icons/BoneMapHumanRightHand.svg

@@ -0,0 +1 @@
+<svg enable-background="new 0 0 1024 1024" height="1024" viewBox="0 0 1024 1024" width="1024" xmlns="http://www.w3.org/2000/svg"><path d="m0 0h1024v1024h-1024z" fill="#3f3f3f"/><path d="m320.094 786.098c-7.046-66.929-28.135-153.363-18.529-260.192 1.143-12.71 4.5-48.282 4.46-82.732-.025-21.174 2.111-48.505 1.975-64.174-.167-19.333.428-41.584.625-55.755 1.052-75.44 13.225-85.827 30.813-85.827 17.246 0 26.77 14.266 27.062 84.582.061 14.42-.5 51-.5 58.5 0 17.508.333 34.167 0 53.5-.447 25.955 4.279 68 9 68 3.902 0 8.099-39.299 9.575-76.999.756-19.326 3.219-58.336 2.6-70.102-1.759-33.413-.474-58.914-1.537-90.165-3.183-93.607 13.016-111.729 34.695-111.729 21.973 0 35.979 57.688 34.849 114.224-.128 6.394 1.165 50.739-.188 89.859-.754 21.811 1.07 49.627 1.683 69.67 1.095 35.768 5.755 63.896 8.869 63.896 2.641 0 4.135-32.584 5.456-65.706.859-21.557 4.468-58.477 3.664-83.616-1.886-59.012 1.139-110.226 1.063-121.501-.635-94.955 14.66-123.101 36.052-123.101 21.476 0 37.188 30.192 36.6 123.343-.067 10.53 2.62 99.926 1.759 121.816-.865 21.992 2.773 65.062 3.517 84.818 1.299 34.521 6.49 63.947 9.124 63.947 3.281 0 10.794-25.638 11.724-60.965.587-22.275-1.231-50.99-.624-70.688 1.257-40.707 3.176-64.631 3.877-99.708 1.945-97.182 16.352-106.289 38.142-106.289 17.957 0 32.453 28.673 32.657 115.03.065 27.702 2.429 62.626.314 94.329-.805 12.081.622 42.512 1.875 73.894.799 20.007 1.102 47.501 1.137 63.775.171 78.595 26.713 133.424 36.556 131.308 30.333-6.521 51.648-43.918 71.219-117.307 10.551-39.566 36.667-71.149 69.9-77.813 25.9-5.193 19.783 46.161 1.318 125.293-8.649 37.068-27.909 86.227-39.566 122.655-31.652 98.917-125.573 188.563-160.902 228.546-17.146 19.403-236.894 19.403-264.59 0-11.525-8.07-43.087-101.557-45.724-126.616z" fill="#b2b2b2"/></svg>

+ 1 - 0
editor/icons/BoneMapperHandle.svg

@@ -0,0 +1 @@
+<svg enable-background="new 0 0 12 12" height="12" viewBox="0 0 12 12" width="12" xmlns="http://www.w3.org/2000/svg"><circle cx="6" cy="6" fill-opacity=".2941" r="5"/><circle cx="6" cy="6" fill="#fff" r="4"/></svg>

+ 1 - 0
editor/icons/BoneMapperHandleCircle.svg

@@ -0,0 +1 @@
+<svg enable-background="new 0 0 12 12" height="12" viewBox="0 0 12 12" width="12" xmlns="http://www.w3.org/2000/svg"><circle cx="6" cy="6" fill="#fff" r="3"/></svg>

+ 1 - 0
editor/icons/BoneMapperHandleSelected.svg

@@ -0,0 +1 @@
+<svg enable-background="new -506.5 517.5 12 12" height="12" viewBox="-506.5 517.5 12 12" width="12" xmlns="http://www.w3.org/2000/svg"><circle cx="-500.5" cy="523.5" fill-opacity=".2941" r="5"/><g fill="#fff"><circle cx="-500.5" cy="523.5" r="4"/><path d="m-499.5 517.5h5v5h-1v-4h-4z"/><path d="m-494.5 524.5v5h-5v-1h4v-4z"/><path d="m-501.5 529.5h-5v-5h1v4h4z"/><path d="m-506.5 522.5v-5h5v1h-4v4z"/></g></svg>

+ 144 - 0
editor/import/post_import_plugin_skeleton_renamer.cpp

@@ -0,0 +1,144 @@
+/*************************************************************************/
+/*  post_import_plugin_skeleton_renamer.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 "post_import_plugin_skeleton_renamer.h"
+
+#include "editor/import/scene_import_settings.h"
+#include "scene/3d/importer_mesh_instance_3d.h"
+#include "scene/3d/skeleton_3d.h"
+#include "scene/animation/animation_player.h"
+#include "scene/resources/bone_map.h"
+
+void PostImportPluginSkeletonRenamer::get_internal_import_options(InternalImportCategory p_category, List<ResourceImporter::ImportOption> *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));
+	}
+}
+
+void PostImportPluginSkeletonRenamer::internal_process(InternalImportCategory p_category, Node *p_base_scene, Node *p_node, Ref<Resource> p_resource, const Dictionary &p_options) {
+	if (p_category == INTERNAL_IMPORT_CATEGORY_SKELETON_3D_NODE) {
+		// Prepare objects.
+		Object *map = p_options["retarget/bone_map"].get_validated_object();
+		if (!map || !bool(p_options["retarget/bone_renamer/rename_bones"])) {
+			return;
+		}
+		BoneMap *bone_map = Object::cast_to<BoneMap>(map);
+		Skeleton3D *skeleton = Object::cast_to<Skeleton3D>(p_node);
+
+		// Rename bones in Skeleton3D.
+		{
+			int len = skeleton->get_bone_count();
+			for (int i = 0; i < len; i++) {
+				StringName bn = bone_map->find_profile_bone_name(skeleton->get_bone_name(i));
+				if (bn) {
+					skeleton->set_bone_name(i, bn);
+				}
+			}
+		}
+
+		// Rename bones in Skin.
+		{
+			TypedArray<Node> nodes = p_base_scene->find_children("*", "ImporterMeshInstance3D");
+			while (nodes.size()) {
+				ImporterMeshInstance3D *mi = Object::cast_to<ImporterMeshInstance3D>(nodes.pop_back());
+				Ref<Skin> skin = mi->get_skin();
+				if (skin.is_valid()) {
+					Node *node = mi->get_node(mi->get_skeleton_path());
+					if (node) {
+						Skeleton3D *mesh_skeleton = Object::cast_to<Skeleton3D>(node);
+						if (mesh_skeleton && node == skeleton) {
+							int len = skin->get_bind_count();
+							for (int i = 0; i < len; i++) {
+								StringName bn = bone_map->find_profile_bone_name(skin->get_bind_name(i));
+								if (bn) {
+									skin->set_bind_name(i, bn);
+								}
+							}
+						}
+					}
+				}
+			}
+		}
+
+		// Rename bones in AnimationPlayer.
+		{
+			TypedArray<Node> nodes = p_base_scene->find_children("*", "AnimationPlayer");
+			while (nodes.size()) {
+				AnimationPlayer *ap = Object::cast_to<AnimationPlayer>(nodes.pop_back());
+				List<StringName> anims;
+				ap->get_animation_list(&anims);
+				for (const StringName &name : anims) {
+					Ref<Animation> anim = ap->get_animation(name);
+					int len = anim->get_track_count();
+					for (int i = 0; i < len; i++) {
+						if (anim->track_get_path(i).get_subname_count() != 1 || !(anim->track_get_type(i) == Animation::TYPE_POSITION_3D || anim->track_get_type(i) == Animation::TYPE_ROTATION_3D || anim->track_get_type(i) == Animation::TYPE_SCALE_3D)) {
+							continue;
+						}
+						String track_path = String(anim->track_get_path(i).get_concatenated_names());
+						Node *node = (ap->get_node(ap->get_root()))->get_node(NodePath(track_path));
+						if (node) {
+							Skeleton3D *track_skeleton = Object::cast_to<Skeleton3D>(node);
+							if (track_skeleton && track_skeleton == skeleton) {
+								StringName bn = bone_map->find_profile_bone_name(anim->track_get_path(i).get_subname(0));
+								if (bn) {
+									anim->track_set_path(i, track_path + ":" + bn);
+								}
+							}
+						}
+					}
+				}
+			}
+		}
+
+		// Rename bones in all Nodes by calling method.
+		{
+			Vector<Variant> vargs;
+			vargs.push_back(p_base_scene);
+			vargs.push_back(skeleton);
+			vargs.push_back(bone_map);
+			const Variant **argptrs = (const Variant **)alloca(sizeof(const Variant **) * vargs.size());
+			const Variant *args = vargs.ptr();
+			uint32_t argcount = vargs.size();
+			for (uint32_t i = 0; i < argcount; i++) {
+				argptrs[i] = &args[i];
+			}
+
+			TypedArray<Node> nodes = p_base_scene->find_children("*");
+			while (nodes.size()) {
+				Node *nd = Object::cast_to<Node>(nodes.pop_back());
+				Callable::CallError ce;
+				nd->callp("_notify_skeleton_bones_renamed", argptrs, argcount, ce);
+			}
+		}
+	}
+}
+
+PostImportPluginSkeletonRenamer::PostImportPluginSkeletonRenamer() {
+}

+ 46 - 0
editor/import/post_import_plugin_skeleton_renamer.h

@@ -0,0 +1,46 @@
+/*************************************************************************/
+/*  post_import_plugin_skeleton_renamer.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 POST_IMPORT_PLUGIN_SKELETON_RENAMER_H
+#define POST_IMPORT_PLUGIN_SKELETON_RENAMER_H
+
+#include "resource_importer_scene.h"
+
+class PostImportPluginSkeletonRenamer : public EditorScenePostImportPlugin {
+	GDCLASS(PostImportPluginSkeletonRenamer, EditorScenePostImportPlugin);
+
+public:
+	virtual void get_internal_import_options(InternalImportCategory p_category, List<ResourceImporter::ImportOption> *r_options) override;
+	virtual void internal_process(InternalImportCategory p_category, Node *p_base_scene, Node *p_node, Ref<Resource> p_resource, const Dictionary &p_options) override;
+
+	PostImportPluginSkeletonRenamer();
+};
+
+#endif // POST_IMPORT_PLUGIN_SKELETON_RENAMER_H

+ 64 - 13
editor/import/resource_importer_scene.cpp

@@ -232,6 +232,7 @@ void EditorScenePostImportPlugin::_bind_methods() {
 	BIND_ENUM_CONSTANT(INTERNAL_IMPORT_CATEGORY_MATERIAL);
 	BIND_ENUM_CONSTANT(INTERNAL_IMPORT_CATEGORY_ANIMATION);
 	BIND_ENUM_CONSTANT(INTERNAL_IMPORT_CATEGORY_ANIMATION_NODE);
+	BIND_ENUM_CONSTANT(INTERNAL_IMPORT_CATEGORY_SKELETON_3D_NODE);
 	BIND_ENUM_CONSTANT(INTERNAL_IMPORT_CATEGORY_MAX);
 }
 
@@ -765,6 +766,27 @@ Node *ResourceImporterScene::_post_fix_node(Node *p_node, Node *p_root, HashMap<
 		return nullptr;
 	}
 
+	{
+		//make sure this is unique
+		node_settings = node_settings.duplicate(true);
+		//fill node settings for this node with default values
+		List<ImportOption> iopts;
+		if (Object::cast_to<ImporterMeshInstance3D>(p_node)) {
+			get_internal_import_options(INTERNAL_IMPORT_CATEGORY_MESH_3D_NODE, &iopts);
+		} else if (Object::cast_to<AnimationPlayer>(p_node)) {
+			get_internal_import_options(INTERNAL_IMPORT_CATEGORY_ANIMATION_NODE, &iopts);
+		} else if (Object::cast_to<Skeleton3D>(p_node)) {
+			get_internal_import_options(INTERNAL_IMPORT_CATEGORY_SKELETON_3D_NODE, &iopts);
+		} else {
+			get_internal_import_options(INTERNAL_IMPORT_CATEGORY_NODE, &iopts);
+		}
+		for (const ImportOption &E : iopts) {
+			if (!node_settings.has(E.option.name)) {
+				node_settings[E.option.name] = E.default_value;
+			}
+		}
+	}
+
 	{
 		ObjectID node_id = p_node->get_instance_id();
 		for (int i = 0; i < post_importer_plugins.size(); i++) {
@@ -785,6 +807,16 @@ Node *ResourceImporterScene::_post_fix_node(Node *p_node, Node *p_root, HashMap<
 		}
 	}
 
+	if (Object::cast_to<Skeleton3D>(p_node)) {
+		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);
+			if (ObjectDB::get_instance(node_id) == nullptr) { //may have been erased, so do not continue
+				break;
+			}
+		}
+	}
+
 	if (Object::cast_to<ImporterMeshInstance3D>(p_node)) {
 		ImporterMeshInstance3D *mi = Object::cast_to<ImporterMeshInstance3D>(p_node);
 
@@ -799,6 +831,16 @@ Node *ResourceImporterScene::_post_fix_node(Node *p_node, Node *p_root, HashMap<
 
 						if (!mat_id.is_empty() && p_material_data.has(mat_id)) {
 							Dictionary matdata = p_material_data[mat_id];
+							{
+								//fill node settings for this node with default values
+								List<ImportOption> iopts;
+								get_internal_import_options(INTERNAL_IMPORT_CATEGORY_MATERIAL, &iopts);
+								for (const ImportOption &E : iopts) {
+									if (!matdata.has(E.option.name)) {
+										matdata[E.option.name] = E.default_value;
+									}
+								}
+							}
 
 							for (int j = 0; j < post_importer_plugins.size(); j++) {
 								post_importer_plugins.write[j]->internal_process(EditorScenePostImportPlugin::INTERNAL_IMPORT_CATEGORY_MATERIAL, p_root, p_node, mat, matdata);
@@ -966,19 +1008,6 @@ Node *ResourceImporterScene::_post_fix_node(Node *p_node, Node *p_root, HashMap<
 	if (Object::cast_to<AnimationPlayer>(p_node)) {
 		AnimationPlayer *ap = Object::cast_to<AnimationPlayer>(p_node);
 
-		{
-			//make sure this is unique
-			node_settings = node_settings.duplicate(true);
-			//fill node settings for this node with default values
-			List<ImportOption> iopts;
-			get_internal_import_options(INTERNAL_IMPORT_CATEGORY_ANIMATION_NODE, &iopts);
-			for (const ImportOption &E : iopts) {
-				if (!node_settings.has(E.option.name)) {
-					node_settings[E.option.name] = E.default_value;
-				}
-			}
-		}
-
 		for (int i = 0; i < post_importer_plugins.size(); i++) {
 			post_importer_plugins.write[i]->internal_process(EditorScenePostImportPlugin::INTERNAL_IMPORT_CATEGORY_ANIMATION_NODE, p_root, p_node, Ref<Resource>(), node_settings);
 		}
@@ -1385,6 +1414,10 @@ void ResourceImporterScene::get_internal_import_options(InternalImportCategory p
 				r_options->push_back(ImportOption(PropertyInfo(Variant::BOOL, "slice_" + itos(i + 1) + "/save_to_file/keep_custom_tracks"), false));
 			}
 		} 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::OBJECT, "retarget/bone_map", PROPERTY_HINT_RESOURCE_TYPE, "BoneMap", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_UPDATE_ALL_IF_MODIFIED), Variant()));
+		} break;
 		default: {
 		}
 	}
@@ -1499,6 +1532,12 @@ 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;
+			}
+		} break;
 		default: {
 		}
 	}
@@ -1534,6 +1573,8 @@ bool ResourceImporterScene::get_internal_option_update_view_required(InternalImp
 		} break;
 		case INTERNAL_IMPORT_CATEGORY_ANIMATION_NODE: {
 		} break;
+		case INTERNAL_IMPORT_CATEGORY_SKELETON_3D_NODE: {
+		} break;
 		default: {
 		}
 	}
@@ -1622,6 +1663,16 @@ void ResourceImporterScene::_generate_meshes(Node *p_node, const Dictionary &p_m
 
 				if (!mesh_id.is_empty() && p_mesh_data.has(mesh_id)) {
 					Dictionary mesh_settings = p_mesh_data[mesh_id];
+					{
+						//fill node settings for this node with default values
+						List<ImportOption> iopts;
+						get_internal_import_options(INTERNAL_IMPORT_CATEGORY_MESH, &iopts);
+						for (const ImportOption &E : iopts) {
+							if (!mesh_settings.has(E.option.name)) {
+								mesh_settings[E.option.name] = E.default_value;
+							}
+						}
+					}
 
 					if (mesh_settings.has("generate/shadow_meshes")) {
 						int shadow_meshes = mesh_settings["generate/shadow_meshes"];

+ 2 - 0
editor/import/resource_importer_scene.h

@@ -106,6 +106,7 @@ public:
 		INTERNAL_IMPORT_CATEGORY_MATERIAL,
 		INTERNAL_IMPORT_CATEGORY_ANIMATION,
 		INTERNAL_IMPORT_CATEGORY_ANIMATION_NODE,
+		INTERNAL_IMPORT_CATEGORY_SKELETON_3D_NODE,
 		INTERNAL_IMPORT_CATEGORY_MAX
 	};
 
@@ -259,6 +260,7 @@ public:
 		INTERNAL_IMPORT_CATEGORY_MATERIAL = EditorScenePostImportPlugin::INTERNAL_IMPORT_CATEGORY_MATERIAL,
 		INTERNAL_IMPORT_CATEGORY_ANIMATION = EditorScenePostImportPlugin::INTERNAL_IMPORT_CATEGORY_ANIMATION,
 		INTERNAL_IMPORT_CATEGORY_ANIMATION_NODE = EditorScenePostImportPlugin::INTERNAL_IMPORT_CATEGORY_ANIMATION_NODE,
+		INTERNAL_IMPORT_CATEGORY_SKELETON_3D_NODE = EditorScenePostImportPlugin::INTERNAL_IMPORT_CATEGORY_SKELETON_3D_NODE,
 		INTERNAL_IMPORT_CATEGORY_MAX = EditorScenePostImportPlugin::INTERNAL_IMPORT_CATEGORY_MAX
 	};
 

+ 11 - 0
editor/import/scene_import_settings.cpp

@@ -339,6 +339,8 @@ void SceneImportSettings::_fill_scene(Node *p_node, TreeItem *p_parent_item) {
 				category = ResourceImporterScene::INTERNAL_IMPORT_CATEGORY_MESH_3D_NODE;
 			} else if (Object::cast_to<AnimationPlayer>(p_node)) {
 				category = ResourceImporterScene::INTERNAL_IMPORT_CATEGORY_ANIMATION_NODE;
+			} else if (Object::cast_to<Skeleton3D>(p_node)) {
+				category = ResourceImporterScene::INTERNAL_IMPORT_CATEGORY_SKELETON_3D_NODE;
 			} else {
 				category = ResourceImporterScene::INTERNAL_IMPORT_CATEGORY_NODE;
 			}
@@ -617,6 +619,13 @@ SceneImportSettings *SceneImportSettings::get_singleton() {
 	return singleton;
 }
 
+Node *SceneImportSettings::get_selected_node() {
+	if (selected_id == "") {
+		return nullptr;
+	}
+	return node_map[selected_id].node;
+}
+
 void SceneImportSettings::_select(Tree *p_from, String p_type, String p_id) {
 	selecting = true;
 	scene_import_settings_data->hide_options = false;
@@ -657,6 +666,8 @@ void SceneImportSettings::_select(Tree *p_from, String p_type, String p_id) {
 				scene_import_settings_data->hide_options = editing_animation;
 			} else if (Object::cast_to<AnimationPlayer>(nd.node)) {
 				scene_import_settings_data->category = ResourceImporterScene::INTERNAL_IMPORT_CATEGORY_ANIMATION_NODE;
+			} else if (Object::cast_to<Skeleton3D>(nd.node)) {
+				scene_import_settings_data->category = ResourceImporterScene::INTERNAL_IMPORT_CATEGORY_SKELETON_3D_NODE;
 			} else {
 				scene_import_settings_data->category = ResourceImporterScene::INTERNAL_IMPORT_CATEGORY_NODE;
 				scene_import_settings_data->hide_options = editing_animation;

+ 1 - 0
editor/import/scene_import_settings.h

@@ -201,6 +201,7 @@ public:
 	void update_view();
 	void open_settings(const String &p_path, bool p_for_animation = false);
 	static SceneImportSettings *get_singleton();
+	Node *get_selected_node();
 	SceneImportSettings();
 	~SceneImportSettings();
 };

+ 439 - 0
editor/plugins/bone_map_editor_plugin.cpp

@@ -0,0 +1,439 @@
+/*************************************************************************/
+/*  bone_map_editor_plugin.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 "bone_map_editor_plugin.h"
+
+#include "editor/editor_scale.h"
+#include "editor/import/post_import_plugin_skeleton_renamer.h"
+#include "editor/import/scene_import_settings.h"
+
+void BoneMapperButton::fetch_textures() {
+	if (selected) {
+		set_normal_texture(get_theme_icon(SNAME("BoneMapperHandleSelected"), SNAME("EditorIcons")));
+	} else {
+		set_normal_texture(get_theme_icon(SNAME("BoneMapperHandle"), SNAME("EditorIcons")));
+	}
+	set_offset(SIDE_LEFT, 0);
+	set_offset(SIDE_RIGHT, 0);
+	set_offset(SIDE_TOP, 0);
+	set_offset(SIDE_BOTTOM, 0);
+
+	circle = memnew(TextureRect);
+	circle->set_texture(get_theme_icon(SNAME("BoneMapperHandleCircle"), SNAME("EditorIcons")));
+	add_child(circle);
+	set_state(BONE_MAP_STATE_UNSET);
+}
+
+StringName BoneMapperButton::get_profile_bone_name() const {
+	return profile_bone_name;
+}
+
+void BoneMapperButton::set_state(BoneMapState p_state) {
+	switch (p_state) {
+		case BONE_MAP_STATE_UNSET: {
+			circle->set_modulate(EditorSettings::get_singleton()->get("editors/bone_mapper/handle_colors/unset"));
+		} break;
+		case BONE_MAP_STATE_SET: {
+			circle->set_modulate(EditorSettings::get_singleton()->get("editors/bone_mapper/handle_colors/set"));
+		} break;
+		case BONE_MAP_STATE_ERROR: {
+			circle->set_modulate(EditorSettings::get_singleton()->get("editors/bone_mapper/handle_colors/error"));
+		} break;
+		default: {
+		} break;
+	}
+}
+
+void BoneMapperButton::_notification(int p_what) {
+	switch (p_what) {
+		case NOTIFICATION_ENTER_TREE: {
+			fetch_textures();
+		} break;
+	}
+}
+
+BoneMapperButton::BoneMapperButton(const StringName p_profile_bone_name, bool p_selected) {
+	profile_bone_name = p_profile_bone_name;
+	selected = p_selected;
+}
+
+BoneMapperButton::~BoneMapperButton() {
+}
+
+void BoneMapperItem::create_editor() {
+	skeleton_bone_selector = memnew(EditorPropertyTextEnum);
+	skeleton_bone_selector->setup(skeleton_bone_names);
+	skeleton_bone_selector->set_label(profile_bone_name);
+	skeleton_bone_selector->set_selectable(false);
+	skeleton_bone_selector->set_object_and_property(bone_map.ptr(), "bone_map/" + String(profile_bone_name));
+	skeleton_bone_selector->update_property();
+	skeleton_bone_selector->connect("property_changed", callable_mp(this, &BoneMapperItem::_value_changed));
+	add_child(skeleton_bone_selector);
+}
+
+void BoneMapperItem::_update_property() {
+	if (skeleton_bone_selector->get_edited_object() && skeleton_bone_selector->get_edited_property()) {
+		skeleton_bone_selector->update_property();
+	}
+}
+
+void BoneMapperItem::_value_changed(const String &p_property, Variant p_value, const String &p_name, bool p_changing) {
+	bone_map->set(p_property, p_value);
+}
+
+void BoneMapperItem::_notification(int p_what) {
+	switch (p_what) {
+		case NOTIFICATION_ENTER_TREE: {
+			create_editor();
+			bone_map->connect("bone_map_updated", callable_mp(this, &BoneMapperItem::_update_property));
+		} break;
+		case NOTIFICATION_EXIT_TREE: {
+			if (!bone_map.is_null() && bone_map->is_connected("bone_map_updated", callable_mp(this, &BoneMapperItem::_update_property))) {
+				bone_map->disconnect("bone_map_updated", callable_mp(this, &BoneMapperItem::_update_property));
+			}
+		} break;
+	}
+}
+
+void BoneMapperItem::_bind_methods() {
+}
+
+BoneMapperItem::BoneMapperItem(Ref<BoneMap> &p_bone_map, PackedStringArray p_skeleton_bone_names, const StringName &p_profile_bone_name) {
+	bone_map = p_bone_map;
+	skeleton_bone_names = p_skeleton_bone_names;
+	profile_bone_name = p_profile_bone_name;
+}
+
+BoneMapperItem::~BoneMapperItem() {
+}
+
+void BoneMapper::create_editor() {
+	profile_group_selector = memnew(EditorPropertyEnum);
+	profile_group_selector->set_label("Group");
+	profile_group_selector->set_selectable(false);
+	profile_group_selector->set_object_and_property(this, "current_group_idx");
+	profile_group_selector->update_property();
+	profile_group_selector->connect("property_changed", callable_mp(this, &BoneMapper::_value_changed));
+	add_child(profile_group_selector);
+
+	bone_mapper_field = memnew(AspectRatioContainer);
+	bone_mapper_field->set_stretch_mode(AspectRatioContainer::STRETCH_FIT);
+	bone_mapper_field->set_custom_minimum_size(Vector2(0, 256.0) * EDSCALE);
+	bone_mapper_field->set_h_size_flags(Control::SIZE_FILL);
+	add_child(bone_mapper_field);
+
+	profile_bg = memnew(ColorRect);
+	profile_bg->set_color(Color(0, 0, 0, 1));
+	profile_bg->set_h_size_flags(Control::SIZE_FILL);
+	profile_bg->set_v_size_flags(Control::SIZE_FILL);
+	bone_mapper_field->add_child(profile_bg);
+
+	profile_texture = memnew(TextureRect);
+	profile_texture->set_stretch_mode(TextureRect::STRETCH_KEEP_ASPECT_CENTERED);
+	profile_texture->set_ignore_texture_size(true);
+	profile_texture->set_h_size_flags(Control::SIZE_FILL);
+	profile_texture->set_v_size_flags(Control::SIZE_FILL);
+	bone_mapper_field->add_child(profile_texture);
+
+	mapper_item_vbox = memnew(VBoxContainer);
+	add_child(mapper_item_vbox);
+
+	separator = memnew(HSeparator);
+	add_child(separator);
+
+	recreate_items();
+}
+
+void BoneMapper::update_group_idx() {
+	if (!bone_map->get_profile().is_valid()) {
+		return;
+	}
+
+	PackedStringArray group_names;
+	int len = bone_map->get_profile()->get_group_size();
+	for (int i = 0; i < len; i++) {
+		group_names.push_back(bone_map->get_profile()->get_group_name(i));
+	}
+	if (current_group_idx >= len) {
+		current_group_idx = 0;
+	}
+	if (len > 0) {
+		profile_group_selector->setup(group_names);
+		profile_group_selector->update_property();
+		profile_group_selector->set_read_only(false);
+	}
+}
+
+void BoneMapper::set_current_group_idx(int p_group_idx) {
+	current_group_idx = p_group_idx;
+	recreate_editor();
+}
+
+int BoneMapper::get_current_group_idx() const {
+	return current_group_idx;
+}
+
+void BoneMapper::set_current_bone_idx(int p_bone_idx) {
+	current_bone_idx = p_bone_idx;
+	recreate_editor();
+}
+
+int BoneMapper::get_current_bone_idx() const {
+	return current_bone_idx;
+}
+
+void BoneMapper::recreate_editor() {
+	// Clear buttons.
+	int len = bone_mapper_buttons.size();
+	for (int i = 0; i < len; i++) {
+		profile_texture->remove_child(bone_mapper_buttons[i]);
+		memdelete(bone_mapper_buttons[i]);
+	}
+	bone_mapper_buttons.clear();
+
+	// Organize mapper items.
+	len = bone_mapper_items.size();
+	for (int i = 0; i < len; i++) {
+		bone_mapper_items[i]->set_visible(current_bone_idx == i);
+	}
+
+	Ref<SkeletonProfile> profile = bone_map->get_profile();
+	if (profile.is_valid()) {
+		SkeletonProfileHumanoid *hmn = Object::cast_to<SkeletonProfileHumanoid>(profile.ptr());
+		if (hmn) {
+			StringName hmn_group_name = profile->get_group_name(current_group_idx);
+			if (hmn_group_name == "Body") {
+				profile_texture->set_texture(get_theme_icon(SNAME("BoneMapHumanBody"), SNAME("EditorIcons")));
+			} else if (hmn_group_name == "Face") {
+				profile_texture->set_texture(get_theme_icon(SNAME("BoneMapHumanFace"), SNAME("EditorIcons")));
+			} else if (hmn_group_name == "LeftHand") {
+				profile_texture->set_texture(get_theme_icon(SNAME("BoneMapHumanLeftHand"), SNAME("EditorIcons")));
+			} else if (hmn_group_name == "RightHand") {
+				profile_texture->set_texture(get_theme_icon(SNAME("BoneMapHumanRightHand"), SNAME("EditorIcons")));
+			}
+		} else {
+			profile_texture->set_texture(profile->get_texture(current_group_idx));
+		}
+	} else {
+		profile_texture->set_texture(Ref<Texture2D>());
+	}
+
+	if (!profile.is_valid()) {
+		return;
+	}
+
+	for (int i = 0; i < len; i++) {
+		if (profile->get_group(i) == profile->get_group_name(current_group_idx)) {
+			BoneMapperButton *mb = memnew(BoneMapperButton(profile->get_bone_name(i), current_bone_idx == i));
+			mb->connect("pressed", callable_mp(this, &BoneMapper::set_current_bone_idx), varray(i), CONNECT_DEFERRED);
+			mb->set_h_grow_direction(GROW_DIRECTION_BOTH);
+			mb->set_v_grow_direction(GROW_DIRECTION_BOTH);
+			Vector2 vc = profile->get_handle_offset(i);
+			bone_mapper_buttons.push_back(mb);
+			profile_texture->add_child(mb);
+			mb->set_anchor(SIDE_LEFT, vc.x);
+			mb->set_anchor(SIDE_RIGHT, vc.x);
+			mb->set_anchor(SIDE_TOP, vc.y);
+			mb->set_anchor(SIDE_BOTTOM, vc.y);
+		}
+	}
+
+	_update_state();
+}
+
+void BoneMapper::clear_items() {
+	// Clear items.
+	int len = bone_mapper_items.size();
+	for (int i = 0; i < len; i++) {
+		mapper_item_vbox->remove_child(bone_mapper_items[i]);
+		memdelete(bone_mapper_items[i]);
+	}
+	bone_mapper_items.clear();
+}
+
+void BoneMapper::recreate_items() {
+	clear_items();
+	// Create items by profile.
+	Ref<SkeletonProfile> profile = bone_map->get_profile();
+	if (profile.is_valid()) {
+		PackedStringArray skeleton_bone_names;
+		skeleton_bone_names.push_back(String());
+
+		int len = skeleton->get_bone_count();
+		for (int i = 0; i < len; i++) {
+			skeleton_bone_names.push_back(skeleton->get_bone_name(i));
+		}
+
+		len = profile->get_bone_size();
+		for (int i = 0; i < len; i++) {
+			StringName bn = profile->get_bone_name(i);
+			bone_mapper_items.append(memnew(BoneMapperItem(bone_map, skeleton_bone_names, bn)));
+			mapper_item_vbox->add_child(bone_mapper_items[i]);
+		}
+	}
+
+	update_group_idx();
+	recreate_editor();
+}
+
+void BoneMapper::_update_state() {
+	int len = bone_mapper_buttons.size();
+	for (int i = 0; i < len; i++) {
+		StringName sbn = bone_map->get_skeleton_bone_name(bone_mapper_buttons[i]->get_profile_bone_name());
+		if (skeleton->find_bone(sbn) >= 0) {
+			if (bone_map->get_skeleton_bone_name_count(sbn) == 1) {
+				bone_mapper_buttons[i]->set_state(BoneMapperButton::BONE_MAP_STATE_SET);
+			} else {
+				bone_mapper_buttons[i]->set_state(BoneMapperButton::BONE_MAP_STATE_ERROR);
+			}
+		} else {
+			bone_mapper_buttons[i]->set_state(BoneMapperButton::BONE_MAP_STATE_UNSET);
+		}
+	}
+}
+
+void BoneMapper::_value_changed(const String &p_property, Variant p_value, const String &p_name, bool p_changing) {
+	set(p_property, p_value);
+	recreate_editor();
+}
+
+void BoneMapper::_bind_methods() {
+	ClassDB::bind_method(D_METHOD("set_current_group_idx", "current_group_idx"), &BoneMapper::set_current_group_idx);
+	ClassDB::bind_method(D_METHOD("get_current_group_idx"), &BoneMapper::get_current_group_idx);
+	ClassDB::bind_method(D_METHOD("set_current_bone_idx", "current_bone_idx"), &BoneMapper::set_current_bone_idx);
+	ClassDB::bind_method(D_METHOD("get_current_bone_idx"), &BoneMapper::get_current_bone_idx);
+	ADD_PROPERTY(PropertyInfo(Variant::INT, "current_group_idx"), "set_current_group_idx", "get_current_group_idx");
+	ADD_PROPERTY(PropertyInfo(Variant::INT, "current_bone_idx"), "set_current_bone_idx", "get_current_bone_idx");
+}
+
+void BoneMapper::_notification(int p_what) {
+	switch (p_what) {
+		case NOTIFICATION_ENTER_TREE: {
+			create_editor();
+			bone_map->connect("bone_map_updated", callable_mp(this, &BoneMapper::_update_state));
+			bone_map->connect("profile_updated", callable_mp(this, &BoneMapper::recreate_items));
+		} break;
+		case NOTIFICATION_EXIT_TREE: {
+			clear_items();
+			if (!bone_map.is_null()) {
+				if (bone_map->is_connected("bone_map_updated", callable_mp(this, &BoneMapper::_update_state))) {
+					bone_map->disconnect("bone_map_updated", callable_mp(this, &BoneMapper::_update_state));
+				}
+				if (bone_map->is_connected("profile_updated", callable_mp(this, &BoneMapper::recreate_items))) {
+					bone_map->disconnect("profile_updated", callable_mp(this, &BoneMapper::recreate_items));
+				}
+			}
+		}
+	}
+}
+
+BoneMapper::BoneMapper(Skeleton3D *p_skeleton, Ref<BoneMap> &p_bone_map) {
+	skeleton = p_skeleton;
+	bone_map = p_bone_map;
+}
+
+BoneMapper::~BoneMapper() {
+}
+
+void BoneMapEditor::create_editors() {
+	if (!skeleton) {
+		return;
+	}
+	bone_mapper = memnew(BoneMapper(skeleton, bone_map));
+	add_child(bone_mapper);
+}
+
+void BoneMapEditor::fetch_objects() {
+	// Hackey... but it may be the easist way to get a selected object from "ImporterScene".
+	SceneImportSettings *si = SceneImportSettings::get_singleton();
+	if (!si) {
+		return;
+	}
+	Node *selected = si->get_selected_node();
+	if (selected) {
+		Skeleton3D *sk = Object::cast_to<Skeleton3D>(selected);
+		if (!sk) {
+			return;
+		}
+		skeleton = sk;
+	} else {
+		// Editor should not exist.
+		skeleton = nullptr;
+	}
+}
+
+void BoneMapEditor::_notification(int p_what) {
+	switch (p_what) {
+		case NOTIFICATION_ENTER_TREE: {
+			fetch_objects();
+			create_editors();
+		} break;
+		case NOTIFICATION_EXIT_TREE: {
+			remove_child(bone_mapper);
+			bone_mapper->queue_delete();
+		}
+	}
+}
+
+BoneMapEditor::BoneMapEditor(Ref<BoneMap> &p_bone_map) {
+	bone_map = p_bone_map;
+}
+
+BoneMapEditor::~BoneMapEditor() {
+}
+
+bool EditorInspectorPluginBoneMap::can_handle(Object *p_object) {
+	return Object::cast_to<BoneMap>(p_object) != nullptr;
+}
+
+void EditorInspectorPluginBoneMap::parse_begin(Object *p_object) {
+	BoneMap *bm = Object::cast_to<BoneMap>(p_object);
+	if (!bm) {
+		return;
+	}
+	Ref<BoneMap> r(bm);
+	editor = memnew(BoneMapEditor(r));
+	add_custom_control(editor);
+}
+
+BoneMapEditorPlugin::BoneMapEditorPlugin() {
+	// Register properties in editor settings.
+	EDITOR_DEF("editors/bone_mapper/handle_colors/set", Color(0.1, 0.6, 0.25));
+	EDITOR_DEF("editors/bone_mapper/handle_colors/error", Color(0.8, 0.2, 0.2));
+	EDITOR_DEF("editors/bone_mapper/handle_colors/unset", Color(0.3, 0.3, 0.3));
+
+	Ref<EditorInspectorPluginBoneMap> inspector_plugin;
+	inspector_plugin.instantiate();
+	add_inspector_plugin(inspector_plugin);
+
+	Ref<PostImportPluginSkeletonRenamer> post_import_plugin_renamer;
+	post_import_plugin_renamer.instantiate();
+	add_scene_post_import_plugin(post_import_plugin_renamer);
+}

+ 176 - 0
editor/plugins/bone_map_editor_plugin.h

@@ -0,0 +1,176 @@
+/*************************************************************************/
+/*  bone_map_editor_plugin.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 BONE_MAP_EDITOR_H
+#define BONE_MAP_EDITOR_H
+
+#include "editor/editor_node.h"
+#include "editor/editor_plugin.h"
+#include "editor/editor_properties.h"
+#include "scene/3d/skeleton_3d.h"
+#include "scene/gui/color_rect.h"
+#include "scene/gui/dialogs.h"
+#include "scene/resources/bone_map.h"
+#include "scene/resources/texture.h"
+
+class BoneMapperButton : public TextureButton {
+	GDCLASS(BoneMapperButton, TextureButton);
+
+public:
+	enum BoneMapState {
+		BONE_MAP_STATE_UNSET,
+		BONE_MAP_STATE_SET,
+		BONE_MAP_STATE_ERROR
+	};
+
+private:
+	StringName profile_bone_name;
+	bool selected = false;
+
+	TextureRect *circle;
+
+	void fetch_textures();
+
+protected:
+	void _notification(int p_what);
+
+public:
+	StringName get_profile_bone_name() const;
+	void set_state(BoneMapState p_state);
+
+	BoneMapperButton(const StringName p_profile_bone_name, bool p_selected);
+	~BoneMapperButton();
+};
+
+class BoneMapperItem : public VBoxContainer {
+	GDCLASS(BoneMapperItem, VBoxContainer);
+
+	int button_id = -1;
+	StringName profile_bone_name;
+
+	PackedStringArray skeleton_bone_names;
+	Ref<BoneMap> bone_map;
+
+	EditorPropertyTextEnum *skeleton_bone_selector;
+
+	void _update_property();
+
+protected:
+	void _notification(int p_what);
+	static void _bind_methods();
+	virtual void _value_changed(const String &p_property, Variant p_value, const String &p_name, bool p_changing);
+	virtual void create_editor();
+
+public:
+	void assign_button_id(int p_button_id);
+
+	BoneMapperItem(Ref<BoneMap> &p_bone_map, PackedStringArray p_skeleton_bone_names, const StringName &p_profile_bone_name = StringName());
+	~BoneMapperItem();
+};
+
+class BoneMapper : public VBoxContainer {
+	GDCLASS(BoneMapper, VBoxContainer);
+
+	Skeleton3D *skeleton;
+	Ref<BoneMap> bone_map;
+
+	Vector<BoneMapperItem *> bone_mapper_items;
+
+	VBoxContainer *mapper_item_vbox;
+	HSeparator *separator;
+
+	int current_group_idx = 0;
+	int current_bone_idx = -1;
+
+	AspectRatioContainer *bone_mapper_field;
+	EditorPropertyEnum *profile_group_selector;
+	ColorRect *profile_bg;
+	TextureRect *profile_texture;
+	Vector<BoneMapperButton *> bone_mapper_buttons;
+
+	void create_editor();
+	void recreate_editor();
+	void clear_items();
+	void recreate_items();
+	void update_group_idx();
+	void _update_state();
+
+protected:
+	void _notification(int p_what);
+	static void _bind_methods();
+	virtual void _value_changed(const String &p_property, Variant p_value, const String &p_name, bool p_changing);
+
+public:
+	void set_current_group_idx(int p_group_idx);
+	int get_current_group_idx() const;
+	void set_current_bone_idx(int p_bone_idx);
+	int get_current_bone_idx() const;
+
+	BoneMapper(Skeleton3D *p_skeleton, Ref<BoneMap> &p_bone_map);
+	~BoneMapper();
+};
+
+class BoneMapEditor : public VBoxContainer {
+	GDCLASS(BoneMapEditor, VBoxContainer);
+
+	Skeleton3D *skeleton;
+	Ref<BoneMap> bone_map;
+	BoneMapper *bone_mapper;
+
+	void fetch_objects();
+	void clear_editors();
+	void create_editors();
+
+protected:
+	void _notification(int p_what);
+
+public:
+	BoneMapEditor(Ref<BoneMap> &p_bone_map);
+	~BoneMapEditor();
+};
+
+class EditorInspectorPluginBoneMap : public EditorInspectorPlugin {
+	GDCLASS(EditorInspectorPluginBoneMap, EditorInspectorPlugin);
+	BoneMapEditor *editor;
+
+public:
+	virtual bool can_handle(Object *p_object) override;
+	virtual void parse_begin(Object *p_object) override;
+};
+
+class BoneMapEditorPlugin : public EditorPlugin {
+	GDCLASS(BoneMapEditorPlugin, EditorPlugin);
+
+public:
+	virtual String get_name() const override { return "BoneMap"; }
+	BoneMapEditorPlugin();
+};
+
+#endif // BONE_MAP_EDITOR_H

+ 18 - 0
modules/mono/glue/GodotSharp/GodotSharp/Core/NodePath.cs

@@ -169,6 +169,21 @@ namespace Godot
             return new NodePath(godot_icall_NodePath_get_as_property_path(GetPtr(this)));
         }
 
+        /// <summary>
+        /// Returns all names concatenated with a slash character (<c>/</c>).
+        /// </summary>
+        /// <example>
+        /// <code>
+        /// var nodepath = new NodePath("Path2D/PathFollow2D/Sprite2D:texture:load_path");
+        /// GD.Print(nodepath.GetConcatenatedNames()); // Path2D/PathFollow2D/Sprite2D
+        /// </code>
+        /// </example>
+        /// <returns>The names concatenated with <c>/</c>.</returns>
+        public string GetConcatenatedNames()
+        {
+            return godot_icall_NodePath_get_concatenated_names(GetPtr(this));
+        }
+
         /// <summary>
         /// Returns all subnames concatenated with a colon character (<c>:</c>)
         /// as separator, i.e. the right side of the first colon in a node path.
@@ -268,6 +283,9 @@ namespace Godot
         [MethodImpl(MethodImplOptions.InternalCall)]
         private static extern IntPtr godot_icall_NodePath_get_as_property_path(IntPtr ptr);
 
+        [MethodImpl(MethodImplOptions.InternalCall)]
+        private static extern string godot_icall_NodePath_get_concatenated_names(IntPtr ptr);
+
         [MethodImpl(MethodImplOptions.InternalCall)]
         private static extern string godot_icall_NodePath_get_concatenated_subnames(IntPtr ptr);
 

+ 5 - 0
modules/mono/glue/nodepath_glue.cpp

@@ -68,6 +68,10 @@ MonoString *godot_icall_NodePath_get_subname(NodePath *p_ptr, uint32_t p_idx) {
 	return GDMonoMarshal::mono_string_from_godot(p_ptr->get_subname(p_idx));
 }
 
+MonoString *godot_icall_NodePath_get_concatenated_names(NodePath *p_ptr) {
+	return GDMonoMarshal::mono_string_from_godot(p_ptr->get_concatenated_names());
+}
+
 MonoString *godot_icall_NodePath_get_concatenated_subnames(NodePath *p_ptr) {
 	return GDMonoMarshal::mono_string_from_godot(p_ptr->get_concatenated_subnames());
 }
@@ -85,6 +89,7 @@ void godot_register_nodepath_icalls() {
 	GDMonoUtils::add_internal_call("Godot.NodePath::godot_icall_NodePath_Dtor", godot_icall_NodePath_Dtor);
 	GDMonoUtils::add_internal_call("Godot.NodePath::godot_icall_NodePath_operator_String", godot_icall_NodePath_operator_String);
 	GDMonoUtils::add_internal_call("Godot.NodePath::godot_icall_NodePath_get_as_property_path", godot_icall_NodePath_get_as_property_path);
+	GDMonoUtils::add_internal_call("Godot.NodePath::godot_icall_NodePath_get_concatenated_names", godot_icall_NodePath_get_concatenated_names);
 	GDMonoUtils::add_internal_call("Godot.NodePath::godot_icall_NodePath_get_concatenated_subnames", godot_icall_NodePath_get_concatenated_subnames);
 	GDMonoUtils::add_internal_call("Godot.NodePath::godot_icall_NodePath_get_name", godot_icall_NodePath_get_name);
 	GDMonoUtils::add_internal_call("Godot.NodePath::godot_icall_NodePath_get_name_count", godot_icall_NodePath_get_name_count);

+ 21 - 0
scene/3d/bone_attachment_3d.cpp

@@ -376,6 +376,24 @@ void BoneAttachment3D::on_bone_pose_update(int p_bone_index) {
 		}
 	}
 }
+#ifdef TOOLS_ENABLED
+void BoneAttachment3D::_notify_skeleton_bones_renamed(Node *p_base_scene, Skeleton3D *p_skeleton, Ref<BoneMap> p_bone_map) {
+	const Skeleton3D *parent = nullptr;
+	if (use_external_skeleton) {
+		if (external_skeleton_node_cache.is_valid()) {
+			parent = Object::cast_to<Skeleton3D>(ObjectDB::get_instance(external_skeleton_node_cache));
+		}
+	} else {
+		parent = Object::cast_to<Skeleton3D>(get_parent());
+	}
+	if (parent && parent == p_skeleton) {
+		StringName bn = p_bone_map->find_profile_bone_name(bone_name);
+		if (bn) {
+			set_bone_name(bn);
+		}
+	}
+}
+#endif // TOOLS_ENABLED
 
 BoneAttachment3D::BoneAttachment3D() {
 }
@@ -398,6 +416,9 @@ void BoneAttachment3D::_bind_methods() {
 	ClassDB::bind_method(D_METHOD("get_use_external_skeleton"), &BoneAttachment3D::get_use_external_skeleton);
 	ClassDB::bind_method(D_METHOD("set_external_skeleton", "external_skeleton"), &BoneAttachment3D::set_external_skeleton);
 	ClassDB::bind_method(D_METHOD("get_external_skeleton"), &BoneAttachment3D::get_external_skeleton);
+#ifdef TOOLS_ENABLED
+	ClassDB::bind_method(D_METHOD("_notify_skeleton_bones_renamed"), &BoneAttachment3D::_notify_skeleton_bones_renamed);
+#endif // TOOLS_ENABLED
 
 	ADD_PROPERTY(PropertyInfo(Variant::STRING_NAME, "bone_name"), "set_bone_name", "get_bone_name");
 	ADD_PROPERTY(PropertyInfo(Variant::INT, "bone_idx"), "set_bone_idx", "get_bone_idx");

+ 6 - 0
scene/3d/bone_attachment_3d.h

@@ -32,6 +32,9 @@
 #define BONE_ATTACHMENT_H
 
 #include "scene/3d/skeleton_3d.h"
+#ifdef TOOLS_ENABLED
+#include "scene/resources/bone_map.h"
+#endif // TOOLS_ENABLED
 
 class BoneAttachment3D : public Node3D {
 	GDCLASS(BoneAttachment3D, Node3D);
@@ -68,6 +71,9 @@ protected:
 	void _notification(int p_what);
 
 	static void _bind_methods();
+#ifdef TOOLS_ENABLED
+	virtual void _notify_skeleton_bones_renamed(Node *p_base_scene, Skeleton3D *p_skeleton, Ref<BoneMap> p_bone_map);
+#endif // TOOLS_ENABLED
 
 public:
 	virtual TypedArray<String> get_configuration_warnings() const override;

+ 6 - 0
scene/register_scene_types.cpp

@@ -143,6 +143,7 @@
 #include "scene/resources/animation_library.h"
 #include "scene/resources/audio_stream_sample.h"
 #include "scene/resources/bit_map.h"
+#include "scene/resources/bone_map.h"
 #include "scene/resources/box_shape_3d.h"
 #include "scene/resources/camera_effects.h"
 #include "scene/resources/capsule_shape_2d.h"
@@ -189,6 +190,7 @@
 #include "scene/resources/skeleton_modification_3d_twoboneik.h"
 #include "scene/resources/skeleton_modification_stack_2d.h"
 #include "scene/resources/skeleton_modification_stack_3d.h"
+#include "scene/resources/skeleton_profile.h"
 #include "scene/resources/sky.h"
 #include "scene/resources/sky_material.h"
 #include "scene/resources/sphere_shape_3d.h"
@@ -871,6 +873,10 @@ void register_scene_types() {
 	GDREGISTER_CLASS(BitMap);
 	GDREGISTER_CLASS(Gradient);
 
+	GDREGISTER_CLASS(SkeletonProfile);
+	GDREGISTER_CLASS(SkeletonProfileHumanoid);
+	GDREGISTER_CLASS(BoneMap);
+
 	OS::get_singleton()->yield(); // may take time to init
 
 	GDREGISTER_CLASS(AudioStreamPlayer);

+ 172 - 0
scene/resources/bone_map.cpp

@@ -0,0 +1,172 @@
+/*************************************************************************/
+/*  bone_map.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 "bone_map.h"
+
+bool BoneMap::_set(const StringName &p_path, const Variant &p_value) {
+	String path = p_path;
+	if (path.begins_with("bone_map/")) {
+		String which = path.get_slicec('/', 1);
+		set_skeleton_bone_name(which, p_value);
+		return true;
+	}
+	return true;
+}
+
+bool BoneMap::_get(const StringName &p_path, Variant &r_ret) const {
+	String path = p_path;
+	if (path.begins_with("bone_map/")) {
+		String which = path.get_slicec('/', 1);
+		r_ret = get_skeleton_bone_name(which);
+		return true;
+	}
+	return true;
+}
+
+Ref<SkeletonProfile> BoneMap::get_profile() const {
+	return profile;
+}
+
+void BoneMap::set_profile(const Ref<SkeletonProfile> &p_profile) {
+	bool is_changed = profile != p_profile;
+	if (is_changed) {
+		if (!profile.is_null() && profile->is_connected("profile_updated", callable_mp(this, &BoneMap::_update_profile))) {
+			profile->disconnect("profile_updated", callable_mp(this, &BoneMap::_update_profile));
+		}
+		profile = p_profile;
+		if (!profile.is_null()) {
+			profile->connect("profile_updated", callable_mp(this, &BoneMap::_update_profile));
+		}
+		_update_profile();
+	}
+	notify_property_list_changed();
+}
+
+StringName BoneMap::get_skeleton_bone_name(StringName p_profile_bone_name) const {
+	ERR_FAIL_COND_V(!bone_map.has(p_profile_bone_name), StringName());
+	return bone_map.get(p_profile_bone_name);
+}
+
+void BoneMap::set_skeleton_bone_name(StringName p_profile_bone_name, const StringName p_skeleton_bone_name) {
+	ERR_FAIL_COND(!bone_map.has(p_profile_bone_name));
+	bone_map.insert(p_profile_bone_name, p_skeleton_bone_name);
+	emit_signal("bone_map_updated");
+}
+
+StringName BoneMap::find_profile_bone_name(StringName p_skeleton_bone_name) const {
+	StringName profile_bone_name = StringName();
+	HashMap<StringName, StringName>::ConstIterator E = bone_map.begin();
+	while (E) {
+		if (E->value == p_skeleton_bone_name) {
+			profile_bone_name = E->key;
+			break;
+		}
+		++E;
+	}
+	return profile_bone_name;
+}
+
+int BoneMap::get_skeleton_bone_name_count(const StringName p_skeleton_bone_name) const {
+	int count = 0;
+	HashMap<StringName, StringName>::ConstIterator E = bone_map.begin();
+	while (E) {
+		if (E->value == p_skeleton_bone_name) {
+			++count;
+		}
+		++E;
+	}
+	return count;
+}
+
+void BoneMap::_update_profile() {
+	_validate_bone_map();
+	emit_signal("profile_updated");
+}
+
+void BoneMap::_validate_bone_map() {
+	Ref<SkeletonProfile> current_profile = get_profile();
+	if (current_profile.is_valid()) {
+		// Insert missing profile bones into bone map.
+		int len = current_profile->get_bone_size();
+		StringName profile_bone_name;
+		for (int i = 0; i < len; i++) {
+			profile_bone_name = current_profile->get_bone_name(i);
+			if (!bone_map.has(profile_bone_name)) {
+				bone_map.insert(profile_bone_name, StringName());
+			}
+		}
+		// Remove bones that do not exist in the profile from the map.
+		Vector<StringName> delete_bones;
+		StringName k;
+		HashMap<StringName, StringName>::ConstIterator E = bone_map.begin();
+		while (E) {
+			k = E->key;
+			if (!current_profile->has_bone(k)) {
+				delete_bones.push_back(k);
+			}
+			++E;
+		}
+		len = delete_bones.size();
+		for (int i = 0; i < len; i++) {
+			bone_map.erase(delete_bones[i]);
+		}
+	} else {
+		bone_map.clear();
+	}
+	emit_signal("retarget_option_updated");
+}
+
+void BoneMap::_bind_methods() {
+	ClassDB::bind_method(D_METHOD("get_profile"), &BoneMap::get_profile);
+	ClassDB::bind_method(D_METHOD("set_profile", "profile"), &BoneMap::set_profile);
+
+	ClassDB::bind_method(D_METHOD("get_skeleton_bone_name", "profile_bone_name"), &BoneMap::get_skeleton_bone_name);
+	ClassDB::bind_method(D_METHOD("set_skeleton_bone_name", "profile_bone_name", "skeleton_bone_name"), &BoneMap::set_skeleton_bone_name);
+
+	ClassDB::bind_method(D_METHOD("find_profile_bone_name", "skeleton_bone_name"), &BoneMap::find_profile_bone_name);
+
+	ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "profile", PROPERTY_HINT_RESOURCE_TYPE, "SkeletonProfile"), "set_profile", "get_profile");
+
+	ADD_SIGNAL(MethodInfo("bone_map_updated"));
+	ADD_SIGNAL(MethodInfo("profile_updated"));
+}
+
+void BoneMap::_validate_property(PropertyInfo &property) const {
+	//
+}
+
+BoneMap::BoneMap() {
+	_validate_bone_map();
+}
+
+BoneMap::~BoneMap() {
+}
+
+//////////////////////////////////////

+ 69 - 0
scene/resources/bone_map.h

@@ -0,0 +1,69 @@
+/*************************************************************************/
+/*  bone_map.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 BONE_MAP_H
+#define BONE_MAP_H
+
+#include "skeleton_profile.h"
+
+class BoneMap : public Resource {
+	GDCLASS(BoneMap, Resource);
+
+	Ref<SkeletonProfile> profile;
+	HashMap<StringName, StringName> bone_map;
+
+	void _update_profile();
+	void _validate_bone_map();
+
+protected:
+	bool _get(const StringName &p_path, Variant &r_ret) const;
+	bool _set(const StringName &p_path, const Variant &p_value);
+	virtual void _validate_property(PropertyInfo &property) const override;
+	static void _bind_methods();
+
+public:
+	int get_profile_type() const;
+	void set_profile_type(const int p_profile_type);
+
+	Ref<SkeletonProfile> get_profile() const;
+	void set_profile(const Ref<SkeletonProfile> &p_profile);
+
+	int get_skeleton_bone_name_count(const StringName p_skeleton_bone_name) const;
+
+	StringName get_skeleton_bone_name(StringName p_profile_bone_name) const;
+	void set_skeleton_bone_name(StringName p_profile_bone_name, const StringName p_skeleton_bone_name);
+
+	StringName find_profile_bone_name(StringName p_skeleton_bone_name) const;
+
+	BoneMap();
+	~BoneMap();
+};
+
+#endif // BONE_MAP_H

+ 514 - 0
scene/resources/skeleton_profile.cpp

@@ -0,0 +1,514 @@
+/*************************************************************************/
+/*  skeleton_profile.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 "skeleton_profile.h"
+
+bool SkeletonProfile::_set(const StringName &p_path, const Variant &p_value) {
+	ERR_FAIL_COND_V(is_read_only, false);
+	String path = p_path;
+
+	if (path.begins_with("group/")) {
+		int which = path.get_slicec('/', 1).to_int();
+		String what = path.get_slicec('/', 2);
+		ERR_FAIL_INDEX_V(which, groups.size(), false);
+
+		if (what == "group_name") {
+			set_group_name(which, p_value);
+		} else if (what == "texture") {
+			set_texture(which, p_value);
+		}
+		return true;
+	}
+
+	if (path.begins_with("bone/")) {
+		int which = path.get_slicec('/', 1).to_int();
+		String what = path.get_slicec('/', 2);
+		ERR_FAIL_INDEX_V(which, bones.size(), false);
+
+		if (what == "bone_name") {
+			set_bone_name(which, p_value);
+		} else if (what == "handle_offset") {
+			set_handle_offset(which, p_value);
+		} else if (what == "group") {
+			set_group(which, p_value);
+		}
+		return true;
+	}
+	return true;
+}
+
+bool SkeletonProfile::_get(const StringName &p_path, Variant &r_ret) const {
+	String path = p_path;
+
+	if (path.begins_with("group/")) {
+		int which = path.get_slicec('/', 1).to_int();
+		String what = path.get_slicec('/', 2);
+		ERR_FAIL_INDEX_V(which, groups.size(), false);
+
+		if (what == "group_name") {
+			r_ret = get_group_name(which);
+		} else if (what == "texture") {
+			r_ret = get_texture(which);
+		}
+		return true;
+	}
+
+	if (path.begins_with("bone/")) {
+		int which = path.get_slicec('/', 1).to_int();
+		String what = path.get_slicec('/', 2);
+		ERR_FAIL_INDEX_V(which, bones.size(), false);
+
+		if (what == "bone_name") {
+			r_ret = get_bone_name(which);
+		} else if (what == "handle_offset") {
+			r_ret = get_handle_offset(which);
+		} else if (what == "group") {
+			r_ret = get_group(which);
+		}
+		return true;
+	}
+	return true;
+}
+
+void SkeletonProfile::_validate_property(PropertyInfo &property) const {
+	if (is_read_only) {
+		if (property.name == ("group_size") || property.name == ("bone_size")) {
+			property.usage = PROPERTY_USAGE_NO_EDITOR;
+			return;
+		}
+	}
+}
+
+void SkeletonProfile::_get_property_list(List<PropertyInfo> *p_list) const {
+	if (is_read_only) {
+		return;
+	}
+	String group_names = "";
+	for (int i = 0; i < groups.size(); i++) {
+		String path = "group/" + itos(i) + "/";
+		p_list->push_back(PropertyInfo(Variant::STRING_NAME, path + "group_name"));
+		p_list->push_back(PropertyInfo(Variant::OBJECT, path + "texture", PROPERTY_HINT_RESOURCE_TYPE, "Texture2D"));
+		if (i > 0) {
+			group_names = group_names + ",";
+		}
+		group_names = group_names + groups[i].group_name;
+	}
+	for (int i = 0; i < bones.size(); i++) {
+		String path = "bone/" + itos(i) + "/";
+		p_list->push_back(PropertyInfo(Variant::STRING_NAME, path + "bone_name"));
+		p_list->push_back(PropertyInfo(Variant::VECTOR2, path + "handle_offset"));
+		p_list->push_back(PropertyInfo(Variant::STRING_NAME, path + "group", PROPERTY_HINT_ENUM, group_names));
+	}
+}
+
+int SkeletonProfile::get_group_size() {
+	return groups.size();
+}
+
+void SkeletonProfile::set_group_size(int p_size) {
+	if (is_read_only) {
+		return;
+	}
+	ERR_FAIL_COND(p_size < 0);
+	groups.resize(p_size);
+	emit_signal("profile_updated");
+	notify_property_list_changed();
+}
+
+StringName SkeletonProfile::get_group_name(int p_group_idx) const {
+	ERR_FAIL_INDEX_V(p_group_idx, groups.size(), StringName());
+	return groups[p_group_idx].group_name;
+}
+
+void SkeletonProfile::set_group_name(int p_group_idx, const StringName p_group_name) {
+	if (is_read_only) {
+		return;
+	}
+	ERR_FAIL_INDEX(p_group_idx, groups.size());
+	groups.write[p_group_idx].group_name = p_group_name;
+	emit_signal("profile_updated");
+}
+
+Ref<Texture2D> SkeletonProfile::get_texture(int p_group_idx) const {
+	ERR_FAIL_INDEX_V(p_group_idx, groups.size(), Ref<Texture2D>());
+	return groups[p_group_idx].texture;
+}
+
+void SkeletonProfile::set_texture(int p_group_idx, const Ref<Texture2D> &p_texture) {
+	if (is_read_only) {
+		return;
+	}
+	ERR_FAIL_INDEX(p_group_idx, groups.size());
+	groups.write[p_group_idx].texture = p_texture;
+	emit_signal("profile_updated");
+}
+
+int SkeletonProfile::get_bone_size() {
+	return bones.size();
+}
+
+void SkeletonProfile::set_bone_size(int p_size) {
+	if (is_read_only) {
+		return;
+	}
+	ERR_FAIL_COND(p_size < 0);
+	bones.resize(p_size);
+	emit_signal("profile_updated");
+	notify_property_list_changed();
+}
+
+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;
+}
+
+void SkeletonProfile::set_bone_name(int p_bone_idx, const StringName p_bone_name) {
+	if (is_read_only) {
+		return;
+	}
+	ERR_FAIL_INDEX(p_bone_idx, bones.size());
+	bones.write[p_bone_idx].bone_name = p_bone_name;
+	emit_signal("profile_updated");
+}
+
+Vector2 SkeletonProfile::get_handle_offset(int p_bone_idx) const {
+	ERR_FAIL_INDEX_V(p_bone_idx, bones.size(), Vector2());
+	return bones[p_bone_idx].handle_offset;
+}
+
+void SkeletonProfile::set_handle_offset(int p_bone_idx, const Vector2 p_handle_offset) {
+	if (is_read_only) {
+		return;
+	}
+	ERR_FAIL_INDEX(p_bone_idx, bones.size());
+	bones.write[p_bone_idx].handle_offset = p_handle_offset;
+	emit_signal("profile_updated");
+}
+
+StringName SkeletonProfile::get_group(int p_bone_idx) const {
+	ERR_FAIL_INDEX_V(p_bone_idx, bones.size(), StringName());
+	return bones[p_bone_idx].group;
+}
+
+void SkeletonProfile::set_group(int p_bone_idx, const StringName p_group) {
+	if (is_read_only) {
+		return;
+	}
+	ERR_FAIL_INDEX(p_bone_idx, bones.size());
+	bones.write[p_bone_idx].group = p_group;
+	emit_signal("profile_updated");
+}
+
+bool SkeletonProfile::has_bone(StringName p_bone_name) {
+	bool is_found = false;
+	for (int i = 0; i < bones.size(); i++) {
+		if (bones[i].bone_name == p_bone_name) {
+			is_found = true;
+			break;
+		}
+	}
+	return is_found;
+}
+
+void SkeletonProfile::_bind_methods() {
+	ClassDB::bind_method(D_METHOD("set_group_size", "size"), &SkeletonProfile::set_group_size);
+	ClassDB::bind_method(D_METHOD("get_group_size"), &SkeletonProfile::get_group_size);
+
+	ClassDB::bind_method(D_METHOD("get_group_name", "group_idx"), &SkeletonProfile::get_group_name);
+	ClassDB::bind_method(D_METHOD("set_group_name", "group_idx", "group_name"), &SkeletonProfile::set_group_name);
+
+	ClassDB::bind_method(D_METHOD("get_texture", "group_idx"), &SkeletonProfile::get_texture);
+	ClassDB::bind_method(D_METHOD("set_texture", "group_idx", "texture"), &SkeletonProfile::set_texture);
+
+	ClassDB::bind_method(D_METHOD("set_bone_size", "size"), &SkeletonProfile::set_bone_size);
+	ClassDB::bind_method(D_METHOD("get_bone_size"), &SkeletonProfile::get_bone_size);
+
+	ClassDB::bind_method(D_METHOD("get_bone_name", "bone_idx"), &SkeletonProfile::get_bone_name);
+	ClassDB::bind_method(D_METHOD("set_bone_name", "bone_idx", "bone_name"), &SkeletonProfile::set_bone_name);
+
+	ClassDB::bind_method(D_METHOD("get_handle_offset", "bone_idx"), &SkeletonProfile::get_handle_offset);
+	ClassDB::bind_method(D_METHOD("set_handle_offset", "bone_idx", "handle_offset"), &SkeletonProfile::set_handle_offset);
+
+	ClassDB::bind_method(D_METHOD("get_group", "bone_idx"), &SkeletonProfile::get_group);
+	ClassDB::bind_method(D_METHOD("set_group", "bone_idx", "group"), &SkeletonProfile::set_group);
+
+	ADD_PROPERTY(PropertyInfo(Variant::INT, "group_size", PROPERTY_HINT_RANGE, "0,100,1", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_ARRAY, "Groups,group/"), "set_group_size", "get_group_size");
+	ADD_PROPERTY(PropertyInfo(Variant::INT, "bone_size", PROPERTY_HINT_RANGE, "0,100,1", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_ARRAY, "Bones,bone/"), "set_bone_size", "get_bone_size");
+
+	ADD_SIGNAL(MethodInfo("profile_updated"));
+}
+
+SkeletonProfile::SkeletonProfile() {
+}
+
+SkeletonProfile::~SkeletonProfile() {
+}
+
+SkeletonProfileHumanoid::SkeletonProfileHumanoid() {
+	is_read_only = true;
+
+	groups.resize(4);
+
+	groups.write[0].group_name = "Body";
+	groups.write[1].group_name = "Face";
+	groups.write[2].group_name = "LeftHand";
+	groups.write[3].group_name = "RightHand";
+
+	bones.resize(56);
+
+	bones.write[0].bone_name = "Root";
+	bones.write[0].handle_offset = Vector2(0.5, 0.91);
+	bones.write[0].group = "Body";
+
+	bones.write[1].bone_name = "Hips";
+	bones.write[1].handle_offset = Vector2(0.5, 0.5);
+	bones.write[1].group = "Body";
+
+	bones.write[2].bone_name = "Spine";
+	bones.write[2].handle_offset = Vector2(0.5, 0.43);
+	bones.write[2].group = "Body";
+
+	bones.write[3].bone_name = "Chest";
+	bones.write[3].handle_offset = Vector2(0.5, 0.36);
+	bones.write[3].group = "Body";
+
+	bones.write[4].bone_name = "UpperChest";
+	bones.write[4].handle_offset = Vector2(0.5, 0.29);
+	bones.write[4].group = "Body";
+
+	bones.write[5].bone_name = "Neck";
+	bones.write[5].handle_offset = Vector2(0.5, 0.23);
+	bones.write[5].group = "Body";
+
+	bones.write[6].bone_name = "Head";
+	bones.write[6].handle_offset = Vector2(0.5, 0.18);
+	bones.write[6].group = "Body";
+
+	bones.write[7].bone_name = "LeftEye";
+	bones.write[7].handle_offset = Vector2(0.6, 0.46);
+	bones.write[7].group = "Face";
+
+	bones.write[8].bone_name = "RightEye";
+	bones.write[8].handle_offset = Vector2(0.37, 0.46);
+	bones.write[8].group = "Face";
+
+	bones.write[9].bone_name = "Jaw";
+	bones.write[9].handle_offset = Vector2(0.46, 0.75);
+	bones.write[9].group = "Face";
+
+	bones.write[10].bone_name = "LeftShoulder";
+	bones.write[10].handle_offset = Vector2(0.55, 0.235);
+	bones.write[10].group = "Body";
+
+	bones.write[11].bone_name = "LeftUpperArm";
+	bones.write[11].handle_offset = Vector2(0.6, 0.24);
+	bones.write[11].group = "Body";
+
+	bones.write[12].bone_name = "LeftLowerArm";
+	bones.write[12].handle_offset = Vector2(0.7, 0.24);
+	bones.write[12].group = "Body";
+
+	bones.write[13].bone_name = "LeftHand";
+	bones.write[13].handle_offset = Vector2(0.82, 0.235);
+	bones.write[13].group = "Body";
+
+	bones.write[14].bone_name = "LeftThumbProximal";
+	bones.write[14].handle_offset = Vector2(0.4, 0.8);
+	bones.write[14].group = "LeftHand";
+
+	bones.write[15].bone_name = "LeftThumbIntermediate";
+	bones.write[15].handle_offset = Vector2(0.3, 0.69);
+	bones.write[15].group = "LeftHand";
+
+	bones.write[16].bone_name = "LeftThumbDistal";
+	bones.write[16].handle_offset = Vector2(0.23, 0.555);
+	bones.write[16].group = "LeftHand";
+
+	bones.write[17].bone_name = "LeftIndexProximal";
+	bones.write[17].handle_offset = Vector2(0.413, 0.52);
+	bones.write[17].group = "LeftHand";
+
+	bones.write[18].bone_name = "LeftIndexIntermediate";
+	bones.write[18].handle_offset = Vector2(0.403, 0.36);
+	bones.write[18].group = "LeftHand";
+
+	bones.write[19].bone_name = "LeftIndexDistal";
+	bones.write[19].handle_offset = Vector2(0.403, 0.255);
+	bones.write[19].group = "LeftHand";
+
+	bones.write[20].bone_name = "LeftMiddleProximal";
+	bones.write[20].handle_offset = Vector2(0.5, 0.51);
+	bones.write[20].group = "LeftHand";
+
+	bones.write[21].bone_name = "LeftMiddleIntermediate";
+	bones.write[21].handle_offset = Vector2(0.5, 0.345);
+	bones.write[21].group = "LeftHand";
+
+	bones.write[22].bone_name = "LeftMiddleDistal";
+	bones.write[22].handle_offset = Vector2(0.5, 0.22);
+	bones.write[22].group = "LeftHand";
+
+	bones.write[23].bone_name = "LeftRingProximal";
+	bones.write[23].handle_offset = Vector2(0.586, 0.52);
+	bones.write[23].group = "LeftHand";
+
+	bones.write[24].bone_name = "LeftRingIntermediate";
+	bones.write[24].handle_offset = Vector2(0.59, 0.36);
+	bones.write[24].group = "LeftHand";
+
+	bones.write[25].bone_name = "LeftRingDistal";
+	bones.write[25].handle_offset = Vector2(0.591, 0.25);
+	bones.write[25].group = "LeftHand";
+
+	bones.write[26].bone_name = "LeftLittleProximal";
+	bones.write[26].handle_offset = Vector2(0.663, 0.543);
+	bones.write[26].group = "LeftHand";
+
+	bones.write[27].bone_name = "LeftLittleIntermediate";
+	bones.write[27].handle_offset = Vector2(0.672, 0.415);
+	bones.write[27].group = "LeftHand";
+
+	bones.write[28].bone_name = "LeftLittleDistal";
+	bones.write[28].handle_offset = Vector2(0.672, 0.32);
+	bones.write[28].group = "LeftHand";
+
+	bones.write[29].bone_name = "RightShoulder";
+	bones.write[29].handle_offset = Vector2(0.45, 0.235);
+	bones.write[29].group = "Body";
+
+	bones.write[30].bone_name = "RightUpperArm";
+	bones.write[30].handle_offset = Vector2(0.4, 0.24);
+	bones.write[30].group = "Body";
+
+	bones.write[31].bone_name = "RightLowerArm";
+	bones.write[31].handle_offset = Vector2(0.3, 0.24);
+	bones.write[31].group = "Body";
+
+	bones.write[32].bone_name = "RightHand";
+	bones.write[32].handle_offset = Vector2(0.18, 0.235);
+	bones.write[32].group = "Body";
+
+	bones.write[33].bone_name = "RightThumbProximal";
+	bones.write[33].handle_offset = Vector2(0.6, 0.8);
+	bones.write[33].group = "RightHand";
+
+	bones.write[34].bone_name = "RightThumbIntermediate";
+	bones.write[34].handle_offset = Vector2(0.7, 0.69);
+	bones.write[34].group = "RightHand";
+
+	bones.write[35].bone_name = "RightThumbDistal";
+	bones.write[35].handle_offset = Vector2(0.77, 0.555);
+	bones.write[35].group = "RightHand";
+
+	bones.write[36].bone_name = "RightIndexProximal";
+	bones.write[36].handle_offset = Vector2(0.587, 0.52);
+	bones.write[36].group = "RightHand";
+
+	bones.write[37].bone_name = "RightIndexIntermediate";
+	bones.write[37].handle_offset = Vector2(0.597, 0.36);
+	bones.write[37].group = "RightHand";
+
+	bones.write[38].bone_name = "RightIndexDistal";
+	bones.write[38].handle_offset = Vector2(0.597, 0.255);
+	bones.write[38].group = "RightHand";
+
+	bones.write[39].bone_name = "RightMiddleProximal";
+	bones.write[39].handle_offset = Vector2(0.5, 0.51);
+	bones.write[39].group = "RightHand";
+
+	bones.write[40].bone_name = "RightMiddleIntermediate";
+	bones.write[40].handle_offset = Vector2(0.5, 0.345);
+	bones.write[40].group = "RightHand";
+
+	bones.write[41].bone_name = "RightMiddleDistal";
+	bones.write[41].handle_offset = Vector2(0.5, 0.22);
+	bones.write[41].group = "RightHand";
+
+	bones.write[42].bone_name = "RightRingProximal";
+	bones.write[42].handle_offset = Vector2(0.414, 0.52);
+	bones.write[42].group = "RightHand";
+
+	bones.write[43].bone_name = "RightRingIntermediate";
+	bones.write[43].handle_offset = Vector2(0.41, 0.36);
+	bones.write[43].group = "RightHand";
+
+	bones.write[44].bone_name = "RightRingDistal";
+	bones.write[44].handle_offset = Vector2(0.409, 0.25);
+	bones.write[44].group = "RightHand";
+
+	bones.write[45].bone_name = "RightLittleProximal";
+	bones.write[45].handle_offset = Vector2(0.337, 0.543);
+	bones.write[45].group = "RightHand";
+
+	bones.write[46].bone_name = "RightLittleIntermediate";
+	bones.write[46].handle_offset = Vector2(0.328, 0.415);
+	bones.write[46].group = "RightHand";
+
+	bones.write[47].bone_name = "RightLittleDistal";
+	bones.write[47].handle_offset = Vector2(0.328, 0.32);
+	bones.write[47].group = "RightHand";
+
+	bones.write[48].bone_name = "LeftUpperLeg";
+	bones.write[48].handle_offset = Vector2(0.549, 0.49);
+	bones.write[48].group = "Body";
+
+	bones.write[49].bone_name = "LeftLowerLeg";
+	bones.write[49].handle_offset = Vector2(0.548, 0.683);
+	bones.write[49].group = "Body";
+
+	bones.write[50].bone_name = "LeftFoot";
+	bones.write[50].handle_offset = Vector2(0.545, 0.9);
+	bones.write[50].group = "Body";
+
+	bones.write[51].bone_name = "LeftToes";
+	bones.write[51].handle_offset = Vector2(0.545, 0.95);
+	bones.write[51].group = "Body";
+
+	bones.write[52].bone_name = "RightUpperLeg";
+	bones.write[52].handle_offset = Vector2(0.451, 0.49);
+	bones.write[52].group = "Body";
+
+	bones.write[53].bone_name = "RightLowerLeg";
+	bones.write[53].handle_offset = Vector2(0.452, 0.683);
+	bones.write[53].group = "Body";
+
+	bones.write[54].bone_name = "RightFoot";
+	bones.write[54].handle_offset = Vector2(0.455, 0.9);
+	bones.write[54].group = "Body";
+
+	bones.write[55].bone_name = "RightToes";
+	bones.write[55].handle_offset = Vector2(0.455, 0.95);
+	bones.write[55].group = "Body";
+}
+
+SkeletonProfileHumanoid::~SkeletonProfileHumanoid() {
+}
+
+//////////////////////////////////////

+ 100 - 0
scene/resources/skeleton_profile.h

@@ -0,0 +1,100 @@
+/*************************************************************************/
+/*  skeleton_profile.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 SKELETON_PROFILE_H
+#define SKELETON_PROFILE_H
+
+#include "texture.h"
+
+class SkeletonProfile : public Resource {
+	GDCLASS(SkeletonProfile, Resource);
+
+protected:
+	// Note: SkeletonProfileHumanoid which extends SkeletonProfile exists to unify standard bone names.
+	// That is what is_read_only is for, so don't make it public.
+	bool is_read_only = false;
+
+	struct SkeletonProfileGroup {
+		StringName group_name;
+		Ref<Texture2D> texture;
+	};
+
+	struct SkeletonProfileBone {
+		StringName bone_name;
+		Vector2 handle_offset;
+		StringName group;
+	};
+
+	Vector<SkeletonProfileGroup> groups;
+	Vector<SkeletonProfileBone> bones;
+
+	bool _get(const StringName &p_path, Variant &r_ret) const;
+	bool _set(const StringName &p_path, const Variant &p_value);
+	virtual void _validate_property(PropertyInfo &property) const override;
+	void _get_property_list(List<PropertyInfo> *p_list) const;
+	static void _bind_methods();
+
+public:
+	int get_group_size();
+	void set_group_size(int p_size);
+
+	StringName get_group_name(int p_group_idx) const;
+	void set_group_name(int p_group_idx, const StringName p_group_name);
+
+	Ref<Texture2D> get_texture(int p_group_idx) const;
+	void set_texture(int p_group_idx, const Ref<Texture2D> &p_texture);
+
+	int get_bone_size();
+	void set_bone_size(int p_size);
+
+	StringName get_bone_name(int p_bone_idx) const;
+	void set_bone_name(int p_bone_idx, const StringName p_bone_name);
+
+	Vector2 get_handle_offset(int p_bone_idx) const;
+	void set_handle_offset(int p_bone_idx, const Vector2 p_handle_offset);
+
+	StringName get_group(int p_bone_idx) const;
+	void set_group(int p_bone_idx, const StringName p_group);
+
+	bool has_bone(StringName p_bone_name);
+
+	SkeletonProfile();
+	~SkeletonProfile();
+};
+
+class SkeletonProfileHumanoid : public SkeletonProfile {
+	GDCLASS(SkeletonProfileHumanoid, SkeletonProfile);
+
+public:
+	SkeletonProfileHumanoid();
+	~SkeletonProfileHumanoid();
+};
+
+#endif // SKELETON_PROFILE_H

Některé soubory nejsou zobrazeny, neboť je v těchto rozdílových datech změněno mnoho souborů