Browse Source

Add per-bone meta to Skeleton3D

Individual bones are not represented as `Node`s in Godot, in order to support meta functionality for them the skeleton has to carry the information similarly to how other per-bone properties are handled.
- Also adds support for GLTF import/export
demolke 1 year ago
parent
commit
0468bea899

+ 32 - 0
doc/classes/Skeleton3D.xml

@@ -99,6 +99,21 @@
 				Returns the global rest transform for [param bone_idx].
 				Returns the global rest transform for [param bone_idx].
 			</description>
 			</description>
 		</method>
 		</method>
+		<method name="get_bone_meta" qualifiers="const">
+			<return type="Variant" />
+			<param index="0" name="bone_idx" type="int" />
+			<param index="1" name="key" type="StringName" />
+			<description>
+				Returns bone metadata for [param bone_idx] with [param key].
+			</description>
+		</method>
+		<method name="get_bone_meta_list" qualifiers="const">
+			<return type="StringName[]" />
+			<param index="0" name="bone_idx" type="int" />
+			<description>
+				Returns a list of all metadata keys for [param bone_idx].
+			</description>
+		</method>
 		<method name="get_bone_name" qualifiers="const">
 		<method name="get_bone_name" qualifiers="const">
 			<return type="String" />
 			<return type="String" />
 			<param index="0" name="bone_idx" type="int" />
 			<param index="0" name="bone_idx" type="int" />
@@ -171,6 +186,14 @@
 				Use for invalidating caches in IK solvers and other nodes which process bones.
 				Use for invalidating caches in IK solvers and other nodes which process bones.
 			</description>
 			</description>
 		</method>
 		</method>
+		<method name="has_bone_meta" qualifiers="const">
+			<return type="bool" />
+			<param index="0" name="bone_idx" type="int" />
+			<param index="1" name="key" type="StringName" />
+			<description>
+				Returns whether there exists any bone metadata for [param bone_idx] with key [param key].
+			</description>
+		</method>
 		<method name="is_bone_enabled" qualifiers="const">
 		<method name="is_bone_enabled" qualifiers="const">
 			<return type="bool" />
 			<return type="bool" />
 			<param index="0" name="bone_idx" type="int" />
 			<param index="0" name="bone_idx" type="int" />
@@ -263,6 +286,15 @@
 				[b]Note:[/b] The pose transform needs to be a global pose! To convert a world transform from a [Node3D] to a global bone pose, multiply the [method Transform3D.affine_inverse] of the node's [member Node3D.global_transform] by the desired world transform.
 				[b]Note:[/b] The pose transform needs to be a global pose! To convert a world transform from a [Node3D] to a global bone pose, multiply the [method Transform3D.affine_inverse] of the node's [member Node3D.global_transform] by the desired world transform.
 			</description>
 			</description>
 		</method>
 		</method>
+		<method name="set_bone_meta">
+			<return type="void" />
+			<param index="0" name="bone_idx" type="int" />
+			<param index="1" name="key" type="StringName" />
+			<param index="2" name="value" type="Variant" />
+			<description>
+				Sets bone metadata for [param bone_idx], will set the [param key] meta to [param value].
+			</description>
+		</method>
 		<method name="set_bone_name">
 		<method name="set_bone_name">
 			<return type="void" />
 			<return type="void" />
 			<param index="0" name="bone_idx" type="int" />
 			<param index="0" name="bone_idx" type="int" />

+ 118 - 0
editor/add_metadata_dialog.cpp

@@ -0,0 +1,118 @@
+/**************************************************************************/
+/*  add_metadata_dialog.cpp                                               */
+/**************************************************************************/
+/*                         This file is part of:                          */
+/*                             GODOT ENGINE                               */
+/*                        https://godotengine.org                         */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur.                  */
+/*                                                                        */
+/* Permission is hereby granted, free of charge, to any person obtaining  */
+/* a copy of this software and associated documentation files (the        */
+/* "Software"), to deal in the Software without restriction, including    */
+/* without limitation the rights to use, copy, modify, merge, publish,    */
+/* distribute, sublicense, and/or sell copies of the Software, and to     */
+/* permit persons to whom the Software is furnished to do so, subject to  */
+/* the following conditions:                                              */
+/*                                                                        */
+/* The above copyright notice and this permission notice shall be         */
+/* included in all copies or substantial portions of the Software.        */
+/*                                                                        */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,        */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF     */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY   */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,   */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE      */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                 */
+/**************************************************************************/
+
+#include "add_metadata_dialog.h"
+
+AddMetadataDialog::AddMetadataDialog() {
+	VBoxContainer *vbc = memnew(VBoxContainer);
+	add_child(vbc);
+
+	HBoxContainer *hbc = memnew(HBoxContainer);
+	vbc->add_child(hbc);
+	hbc->add_child(memnew(Label(TTR("Name:"))));
+
+	add_meta_name = memnew(LineEdit);
+	add_meta_name->set_custom_minimum_size(Size2(200 * EDSCALE, 1));
+	hbc->add_child(add_meta_name);
+	hbc->add_child(memnew(Label(TTR("Type:"))));
+
+	add_meta_type = memnew(OptionButton);
+
+	hbc->add_child(add_meta_type);
+
+	Control *spacing = memnew(Control);
+	vbc->add_child(spacing);
+	spacing->set_custom_minimum_size(Size2(0, 10 * EDSCALE));
+
+	set_ok_button_text(TTR("Add"));
+	register_text_enter(add_meta_name);
+
+	validation_panel = memnew(EditorValidationPanel);
+	vbc->add_child(validation_panel);
+	validation_panel->add_line(EditorValidationPanel::MSG_ID_DEFAULT, TTR("Metadata name is valid."));
+	validation_panel->set_update_callback(callable_mp(this, &AddMetadataDialog::_check_meta_name));
+	validation_panel->set_accept_button(get_ok_button());
+
+	add_meta_name->connect(SceneStringName(text_changed), callable_mp(validation_panel, &EditorValidationPanel::update).unbind(1));
+}
+
+void AddMetadataDialog::_complete_init(const StringName &p_title) {
+	add_meta_name->grab_focus();
+	add_meta_name->set_text("");
+	validation_panel->update();
+
+	set_title(vformat(TTR("Add Metadata Property for \"%s\""), p_title));
+
+	// Skip if we already completed the initialization.
+	if (add_meta_type->get_item_count()) {
+		return;
+	}
+
+	// Theme icons can be retrieved only the Window has been initialized.
+	for (int i = 0; i < Variant::VARIANT_MAX; i++) {
+		if (i == Variant::NIL || i == Variant::RID || i == Variant::CALLABLE || i == Variant::SIGNAL) {
+			continue; //not editable by inspector.
+		}
+		String type = i == Variant::OBJECT ? String("Resource") : Variant::get_type_name(Variant::Type(i));
+
+		add_meta_type->add_icon_item(get_editor_theme_icon(type), type, i);
+	}
+}
+
+void AddMetadataDialog::open(const StringName p_title, List<StringName> &p_existing_metas) {
+	this->_existing_metas = p_existing_metas;
+	_complete_init(p_title);
+	popup_centered();
+}
+
+StringName AddMetadataDialog::get_meta_name() {
+	return add_meta_name->get_text();
+}
+
+Variant AddMetadataDialog::get_meta_defval() {
+	Variant defval;
+	Callable::CallError ce;
+	Variant::construct(Variant::Type(add_meta_type->get_selected_id()), defval, nullptr, 0, ce);
+	return defval;
+}
+
+void AddMetadataDialog::_check_meta_name() {
+	const String meta_name = add_meta_name->get_text();
+
+	if (meta_name.is_empty()) {
+		validation_panel->set_message(EditorValidationPanel::MSG_ID_DEFAULT, TTR("Metadata name can't be empty."), EditorValidationPanel::MSG_ERROR);
+	} else if (!meta_name.is_valid_ascii_identifier()) {
+		validation_panel->set_message(EditorValidationPanel::MSG_ID_DEFAULT, TTR("Metadata name must be a valid identifier."), EditorValidationPanel::MSG_ERROR);
+	} else if (_existing_metas.find(meta_name)) {
+		validation_panel->set_message(EditorValidationPanel::MSG_ID_DEFAULT, vformat(TTR("Metadata with name \"%s\" already exists."), meta_name), EditorValidationPanel::MSG_ERROR);
+	} else if (meta_name[0] == '_') {
+		validation_panel->set_message(EditorValidationPanel::MSG_ID_DEFAULT, TTR("Names starting with _ are reserved for editor-only metadata."), EditorValidationPanel::MSG_ERROR);
+	}
+}

+ 66 - 0
editor/add_metadata_dialog.h

@@ -0,0 +1,66 @@
+/**************************************************************************/
+/*  add_metadata_dialog.h                                                 */
+/**************************************************************************/
+/*                         This file is part of:                          */
+/*                             GODOT ENGINE                               */
+/*                        https://godotengine.org                         */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur.                  */
+/*                                                                        */
+/* Permission is hereby granted, free of charge, to any person obtaining  */
+/* a copy of this software and associated documentation files (the        */
+/* "Software"), to deal in the Software without restriction, including    */
+/* without limitation the rights to use, copy, modify, merge, publish,    */
+/* distribute, sublicense, and/or sell copies of the Software, and to     */
+/* permit persons to whom the Software is furnished to do so, subject to  */
+/* the following conditions:                                              */
+/*                                                                        */
+/* The above copyright notice and this permission notice shall be         */
+/* included in all copies or substantial portions of the Software.        */
+/*                                                                        */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,        */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF     */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY   */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,   */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE      */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                 */
+/**************************************************************************/
+
+#ifndef ADD_METADATA_DIALOG_H
+#define ADD_METADATA_DIALOG_H
+
+#include "core/object/callable_method_pointer.h"
+#include "editor/editor_help.h"
+#include "editor/editor_undo_redo_manager.h"
+#include "editor/gui/editor_validation_panel.h"
+#include "editor/themes/editor_scale.h"
+#include "scene/gui/button.h"
+#include "scene/gui/dialogs.h"
+#include "scene/gui/item_list.h"
+#include "scene/gui/line_edit.h"
+#include "scene/gui/option_button.h"
+#include "scene/gui/tree.h"
+
+class AddMetadataDialog : public ConfirmationDialog {
+	GDCLASS(AddMetadataDialog, ConfirmationDialog);
+
+public:
+	AddMetadataDialog();
+	void open(const StringName p_title, List<StringName> &p_existing_metas);
+
+	StringName get_meta_name();
+	Variant get_meta_defval();
+
+private:
+	List<StringName> _existing_metas;
+
+	void _check_meta_name();
+	void _complete_init(const StringName &p_label);
+
+	LineEdit *add_meta_name = nullptr;
+	OptionButton *add_meta_type = nullptr;
+	EditorValidationPanel *validation_panel = nullptr;
+};
+#endif // ADD_METADATA_DIALOG_H

+ 21 - 79
editor/editor_inspector.cpp

@@ -32,6 +32,7 @@
 #include "editor_inspector.compat.inc"
 #include "editor_inspector.compat.inc"
 
 
 #include "core/os/keyboard.h"
 #include "core/os/keyboard.h"
+#include "editor/add_metadata_dialog.h"
 #include "editor/doc_tools.h"
 #include "editor/doc_tools.h"
 #include "editor/editor_feature_profile.h"
 #include "editor/editor_feature_profile.h"
 #include "editor/editor_main_screen.h"
 #include "editor/editor_main_screen.h"
@@ -4245,92 +4246,33 @@ Variant EditorInspector::get_property_clipboard() const {
 	return property_clipboard;
 	return property_clipboard;
 }
 }
 
 
-void EditorInspector::_add_meta_confirm() {
-	String name = add_meta_name->get_text();
-
-	object->editor_set_section_unfold("metadata", true); // Ensure metadata is unfolded when adding a new metadata.
-
-	Variant defval;
-	Callable::CallError ce;
-	Variant::construct(Variant::Type(add_meta_type->get_selected_id()), defval, nullptr, 0, ce);
-	EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();
-	undo_redo->create_action(vformat(TTR("Add metadata %s"), name));
-	undo_redo->add_do_method(object, "set_meta", name, defval);
-	undo_redo->add_undo_method(object, "remove_meta", name);
-	undo_redo->commit_action();
-}
-
-void EditorInspector::_check_meta_name() {
-	const String meta_name = add_meta_name->get_text();
-
-	if (meta_name.is_empty()) {
-		validation_panel->set_message(EditorValidationPanel::MSG_ID_DEFAULT, TTR("Metadata name can't be empty."), EditorValidationPanel::MSG_ERROR);
-	} else if (!meta_name.is_valid_ascii_identifier()) {
-		validation_panel->set_message(EditorValidationPanel::MSG_ID_DEFAULT, TTR("Metadata name must be a valid identifier."), EditorValidationPanel::MSG_ERROR);
-	} else if (object->has_meta(meta_name)) {
-		validation_panel->set_message(EditorValidationPanel::MSG_ID_DEFAULT, vformat(TTR("Metadata with name \"%s\" already exists."), meta_name), EditorValidationPanel::MSG_ERROR);
-	} else if (meta_name[0] == '_') {
-		validation_panel->set_message(EditorValidationPanel::MSG_ID_DEFAULT, TTR("Names starting with _ are reserved for editor-only metadata."), EditorValidationPanel::MSG_ERROR);
-	}
-}
-
 void EditorInspector::_show_add_meta_dialog() {
 void EditorInspector::_show_add_meta_dialog() {
 	if (!add_meta_dialog) {
 	if (!add_meta_dialog) {
-		add_meta_dialog = memnew(ConfirmationDialog);
-
-		VBoxContainer *vbc = memnew(VBoxContainer);
-		add_meta_dialog->add_child(vbc);
-
-		HBoxContainer *hbc = memnew(HBoxContainer);
-		vbc->add_child(hbc);
-		hbc->add_child(memnew(Label(TTR("Name:"))));
-
-		add_meta_name = memnew(LineEdit);
-		add_meta_name->set_custom_minimum_size(Size2(200 * EDSCALE, 1));
-		hbc->add_child(add_meta_name);
-		hbc->add_child(memnew(Label(TTR("Type:"))));
-
-		add_meta_type = memnew(OptionButton);
-		for (int i = 0; i < Variant::VARIANT_MAX; i++) {
-			if (i == Variant::NIL || i == Variant::RID || i == Variant::CALLABLE || i == Variant::SIGNAL) {
-				continue; //not editable by inspector.
-			}
-			String type = i == Variant::OBJECT ? String("Resource") : Variant::get_type_name(Variant::Type(i));
-
-			add_meta_type->add_icon_item(get_editor_theme_icon(type), type, i);
-		}
-		hbc->add_child(add_meta_type);
-
-		Control *spacing = memnew(Control);
-		vbc->add_child(spacing);
-		spacing->set_custom_minimum_size(Size2(0, 10 * EDSCALE));
-
-		add_meta_dialog->set_ok_button_text(TTR("Add"));
-		add_child(add_meta_dialog);
-		add_meta_dialog->register_text_enter(add_meta_name);
+		add_meta_dialog = memnew(AddMetadataDialog());
 		add_meta_dialog->connect(SceneStringName(confirmed), callable_mp(this, &EditorInspector::_add_meta_confirm));
 		add_meta_dialog->connect(SceneStringName(confirmed), callable_mp(this, &EditorInspector::_add_meta_confirm));
-
-		validation_panel = memnew(EditorValidationPanel);
-		vbc->add_child(validation_panel);
-		validation_panel->add_line(EditorValidationPanel::MSG_ID_DEFAULT, TTR("Metadata name is valid."));
-		validation_panel->set_update_callback(callable_mp(this, &EditorInspector::_check_meta_name));
-		validation_panel->set_accept_button(add_meta_dialog->get_ok_button());
-
-		add_meta_name->connect(SceneStringName(text_changed), callable_mp(validation_panel, &EditorValidationPanel::update).unbind(1));
+		add_child(add_meta_dialog);
 	}
 	}
 
 
+	StringName dialog_title;
 	Node *node = Object::cast_to<Node>(object);
 	Node *node = Object::cast_to<Node>(object);
-	if (node) {
-		add_meta_dialog->set_title(vformat(TTR("Add Metadata Property for \"%s\""), node->get_name()));
-	} else {
-		// This should normally be reached when the object is derived from Resource.
-		add_meta_dialog->set_title(vformat(TTR("Add Metadata Property for \"%s\""), object->get_class()));
-	}
+	// If object is derived from Node use node name, if derived from Resource use classname.
+	dialog_title = node ? node->get_name() : StringName(object->get_class());
+
+	List<StringName> existing_meta_keys;
+	object->get_meta_list(&existing_meta_keys);
+	add_meta_dialog->open(dialog_title, existing_meta_keys);
+}
+
+void EditorInspector::_add_meta_confirm() {
+	// Ensure metadata is unfolded when adding a new metadata.
+	object->editor_set_section_unfold("metadata", true);
 
 
-	add_meta_dialog->popup_centered();
-	add_meta_name->grab_focus();
-	add_meta_name->set_text("");
-	validation_panel->update();
+	String name = add_meta_dialog->get_meta_name();
+	EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();
+	undo_redo->create_action(vformat(TTR("Add metadata %s"), name));
+	undo_redo->add_do_method(object, "set_meta", name, add_meta_dialog->get_meta_defval());
+	undo_redo->add_undo_method(object, "remove_meta", name);
+	undo_redo->commit_action();
 }
 }
 
 
 void EditorInspector::_bind_methods() {
 void EditorInspector::_bind_methods() {

+ 2 - 2
editor/editor_inspector.h

@@ -31,6 +31,7 @@
 #ifndef EDITOR_INSPECTOR_H
 #ifndef EDITOR_INSPECTOR_H
 #define EDITOR_INSPECTOR_H
 #define EDITOR_INSPECTOR_H
 
 
+#include "editor/add_metadata_dialog.h"
 #include "editor_property_name_processor.h"
 #include "editor_property_name_processor.h"
 #include "scene/gui/box_container.h"
 #include "scene/gui/box_container.h"
 #include "scene/gui/scroll_container.h"
 #include "scene/gui/scroll_container.h"
@@ -575,14 +576,13 @@ class EditorInspector : public ScrollContainer {
 
 
 	bool _is_property_disabled_by_feature_profile(const StringName &p_property);
 	bool _is_property_disabled_by_feature_profile(const StringName &p_property);
 
 
-	ConfirmationDialog *add_meta_dialog = nullptr;
+	AddMetadataDialog *add_meta_dialog = nullptr;
 	LineEdit *add_meta_name = nullptr;
 	LineEdit *add_meta_name = nullptr;
 	OptionButton *add_meta_type = nullptr;
 	OptionButton *add_meta_type = nullptr;
 	EditorValidationPanel *validation_panel = nullptr;
 	EditorValidationPanel *validation_panel = nullptr;
 
 
 	void _add_meta_confirm();
 	void _add_meta_confirm();
 	void _show_add_meta_dialog();
 	void _show_add_meta_dialog();
-	void _check_meta_name();
 
 
 protected:
 protected:
 	static void _bind_methods();
 	static void _bind_methods();

+ 143 - 30
editor/plugins/skeleton_3d_editor_plugin.cpp

@@ -52,7 +52,7 @@
 #include "scene/resources/skeleton_profile.h"
 #include "scene/resources/skeleton_profile.h"
 #include "scene/resources/surface_tool.h"
 #include "scene/resources/surface_tool.h"
 
 
-void BoneTransformEditor::create_editors() {
+void BonePropertiesEditor::create_editors() {
 	section = memnew(EditorInspectorSection);
 	section = memnew(EditorInspectorSection);
 	section->setup("trf_properties", label, this, Color(0.0f, 0.0f, 0.0f), true);
 	section->setup("trf_properties", label, this, Color(0.0f, 0.0f, 0.0f), true);
 	section->unfold();
 	section->unfold();
@@ -61,7 +61,7 @@ void BoneTransformEditor::create_editors() {
 	enabled_checkbox = memnew(EditorPropertyCheck());
 	enabled_checkbox = memnew(EditorPropertyCheck());
 	enabled_checkbox->set_label("Pose Enabled");
 	enabled_checkbox->set_label("Pose Enabled");
 	enabled_checkbox->set_selectable(false);
 	enabled_checkbox->set_selectable(false);
-	enabled_checkbox->connect("property_changed", callable_mp(this, &BoneTransformEditor::_value_changed));
+	enabled_checkbox->connect("property_changed", callable_mp(this, &BonePropertiesEditor::_value_changed));
 	section->get_vbox()->add_child(enabled_checkbox);
 	section->get_vbox()->add_child(enabled_checkbox);
 
 
 	// Position property.
 	// Position property.
@@ -69,8 +69,8 @@ void BoneTransformEditor::create_editors() {
 	position_property->setup(-10000, 10000, 0.001, true);
 	position_property->setup(-10000, 10000, 0.001, true);
 	position_property->set_label("Position");
 	position_property->set_label("Position");
 	position_property->set_selectable(false);
 	position_property->set_selectable(false);
-	position_property->connect("property_changed", callable_mp(this, &BoneTransformEditor::_value_changed));
-	position_property->connect("property_keyed", callable_mp(this, &BoneTransformEditor::_property_keyed));
+	position_property->connect("property_changed", callable_mp(this, &BonePropertiesEditor::_value_changed));
+	position_property->connect("property_keyed", callable_mp(this, &BonePropertiesEditor::_property_keyed));
 	section->get_vbox()->add_child(position_property);
 	section->get_vbox()->add_child(position_property);
 
 
 	// Rotation property.
 	// Rotation property.
@@ -78,8 +78,8 @@ void BoneTransformEditor::create_editors() {
 	rotation_property->setup(-10000, 10000, 0.001, true);
 	rotation_property->setup(-10000, 10000, 0.001, true);
 	rotation_property->set_label("Rotation");
 	rotation_property->set_label("Rotation");
 	rotation_property->set_selectable(false);
 	rotation_property->set_selectable(false);
-	rotation_property->connect("property_changed", callable_mp(this, &BoneTransformEditor::_value_changed));
-	rotation_property->connect("property_keyed", callable_mp(this, &BoneTransformEditor::_property_keyed));
+	rotation_property->connect("property_changed", callable_mp(this, &BonePropertiesEditor::_value_changed));
+	rotation_property->connect("property_keyed", callable_mp(this, &BonePropertiesEditor::_property_keyed));
 	section->get_vbox()->add_child(rotation_property);
 	section->get_vbox()->add_child(rotation_property);
 
 
 	// Scale property.
 	// Scale property.
@@ -87,8 +87,8 @@ void BoneTransformEditor::create_editors() {
 	scale_property->setup(-10000, 10000, 0.001, true, true);
 	scale_property->setup(-10000, 10000, 0.001, true, true);
 	scale_property->set_label("Scale");
 	scale_property->set_label("Scale");
 	scale_property->set_selectable(false);
 	scale_property->set_selectable(false);
-	scale_property->connect("property_changed", callable_mp(this, &BoneTransformEditor::_value_changed));
-	scale_property->connect("property_keyed", callable_mp(this, &BoneTransformEditor::_property_keyed));
+	scale_property->connect("property_changed", callable_mp(this, &BonePropertiesEditor::_value_changed));
+	scale_property->connect("property_keyed", callable_mp(this, &BonePropertiesEditor::_property_keyed));
 	section->get_vbox()->add_child(scale_property);
 	section->get_vbox()->add_child(scale_property);
 
 
 	// Transform/Matrix section.
 	// Transform/Matrix section.
@@ -102,50 +102,136 @@ void BoneTransformEditor::create_editors() {
 	rest_matrix->set_label("Transform");
 	rest_matrix->set_label("Transform");
 	rest_matrix->set_selectable(false);
 	rest_matrix->set_selectable(false);
 	rest_section->get_vbox()->add_child(rest_matrix);
 	rest_section->get_vbox()->add_child(rest_matrix);
+
+	// Bone Metadata property
+	meta_section = memnew(EditorInspectorSection);
+	meta_section->setup("bone_meta", TTR("Bone Metadata"), this, Color(.0f, .0f, .0f), true);
+	section->get_vbox()->add_child(meta_section);
+
+	add_metadata_button = EditorInspector::create_inspector_action_button(TTR("Add Bone Metadata"));
+	add_metadata_button->connect(SceneStringName(pressed), callable_mp(this, &BonePropertiesEditor::_show_add_meta_dialog));
+	section->get_vbox()->add_child(add_metadata_button);
+
+	EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();
+	undo_redo->connect("version_changed", callable_mp(this, &BonePropertiesEditor::_update_properties));
+	undo_redo->connect("history_changed", callable_mp(this, &BonePropertiesEditor::_update_properties));
 }
 }
 
 
-void BoneTransformEditor::_notification(int p_what) {
+void BonePropertiesEditor::_notification(int p_what) {
 	switch (p_what) {
 	switch (p_what) {
 		case NOTIFICATION_THEME_CHANGED: {
 		case NOTIFICATION_THEME_CHANGED: {
 			const Color section_color = get_theme_color(SNAME("prop_subsection"), EditorStringName(Editor));
 			const Color section_color = get_theme_color(SNAME("prop_subsection"), EditorStringName(Editor));
 			section->set_bg_color(section_color);
 			section->set_bg_color(section_color);
 			rest_section->set_bg_color(section_color);
 			rest_section->set_bg_color(section_color);
+			add_metadata_button->set_icon(get_editor_theme_icon(SNAME("Add")));
 		} break;
 		} break;
 	}
 	}
 }
 }
 
 
-void BoneTransformEditor::_value_changed(const String &p_property, const Variant &p_value, const String &p_name, bool p_changing) {
-	if (updating) {
+void BonePropertiesEditor::_value_changed(const String &p_property, const Variant &p_value, const String &p_name, bool p_changing) {
+	if (updating || !skeleton) {
+		return;
+	}
+
+	EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();
+	undo_redo->create_action(TTR("Set Bone Transform"), UndoRedo::MERGE_ENDS);
+	undo_redo->add_undo_property(skeleton, p_property, skeleton->get(p_property));
+	undo_redo->add_do_property(skeleton, p_property, p_value);
+
+	Skeleton3DEditor *se = Skeleton3DEditor::get_singleton();
+	if (se) {
+		undo_redo->add_do_method(se, "update_joint_tree");
+		undo_redo->add_undo_method(se, "update_joint_tree");
+	}
+
+	undo_redo->commit_action();
+}
+
+void BonePropertiesEditor::_meta_changed(const String &p_property, const Variant &p_value, const String &p_name, bool p_changing) {
+	if (!skeleton || p_property.get_slicec('/', 2) != "bone_meta") {
 		return;
 		return;
 	}
 	}
-	if (skeleton) {
-		EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();
-		undo_redo->create_action(TTR("Set Bone Transform"), UndoRedo::MERGE_ENDS);
-		undo_redo->add_undo_property(skeleton, p_property, skeleton->get(p_property));
-		undo_redo->add_do_property(skeleton, p_property, p_value);
-
-		Skeleton3DEditor *se = Skeleton3DEditor::get_singleton();
-		if (se) {
-			undo_redo->add_do_method(se, "update_joint_tree");
-			undo_redo->add_undo_method(se, "update_joint_tree");
-		}
 
 
-		undo_redo->commit_action();
+	int bone = p_property.get_slicec('/', 1).to_int();
+	if (bone >= skeleton->get_bone_count()) {
+		return;
 	}
 	}
+
+	String key = p_property.get_slicec('/', 3);
+	if (!skeleton->has_bone_meta(1, key)) {
+		return;
+	}
+
+	EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();
+	undo_redo->create_action(vformat(TTR("Modify metadata '%s' for bone '%s'"), key, skeleton->get_bone_name(bone)));
+	undo_redo->add_do_property(skeleton, p_property, p_value);
+	undo_redo->add_do_method(meta_editors[p_property], "update_property");
+	undo_redo->add_undo_property(skeleton, p_property, skeleton->get_bone_meta(bone, key));
+	undo_redo->add_undo_method(meta_editors[p_property], "update_property");
+	undo_redo->commit_action();
 }
 }
 
 
-BoneTransformEditor::BoneTransformEditor(Skeleton3D *p_skeleton) :
+void BonePropertiesEditor::_meta_deleted(const String &p_property) {
+	if (!skeleton || p_property.get_slicec('/', 2) != "bone_meta") {
+		return;
+	}
+
+	int bone = p_property.get_slicec('/', 1).to_int();
+	if (bone >= skeleton->get_bone_count()) {
+		return;
+	}
+
+	String key = p_property.get_slicec('/', 3);
+	if (!skeleton->has_bone_meta(1, key)) {
+		return;
+	}
+
+	EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();
+	undo_redo->create_action(vformat(TTR("Remove metadata '%s' from bone '%s'"), key, skeleton->get_bone_name(bone)));
+	undo_redo->add_do_property(skeleton, p_property, Variant());
+	undo_redo->add_undo_property(skeleton, p_property, skeleton->get_bone_meta(bone, key));
+	undo_redo->commit_action();
+
+	emit_signal(SNAME("property_deleted"), p_property);
+}
+
+void BonePropertiesEditor::_show_add_meta_dialog() {
+	if (!add_meta_dialog) {
+		add_meta_dialog = memnew(AddMetadataDialog());
+		add_meta_dialog->connect(SceneStringName(confirmed), callable_mp(this, &BonePropertiesEditor::_add_meta_confirm));
+		add_child(add_meta_dialog);
+	}
+
+	int bone = Skeleton3DEditor::get_singleton()->get_selected_bone();
+	StringName dialog_title = skeleton->get_bone_name(bone);
+
+	List<StringName> existing_meta_keys;
+	skeleton->get_bone_meta_list(bone, &existing_meta_keys);
+	add_meta_dialog->open(dialog_title, existing_meta_keys);
+}
+
+void BonePropertiesEditor::_add_meta_confirm() {
+	int bone = Skeleton3DEditor::get_singleton()->get_selected_bone();
+	String name = add_meta_dialog->get_meta_name();
+	EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();
+	undo_redo->create_action(vformat(TTR("Add metadata '%s' to bone '%s'"), name, skeleton->get_bone_name(bone)));
+	undo_redo->add_do_method(skeleton, "set_bone_meta", bone, name, add_meta_dialog->get_meta_defval());
+	undo_redo->add_undo_method(skeleton, "set_bone_meta", bone, name, Variant());
+	undo_redo->commit_action();
+}
+
+BonePropertiesEditor::BonePropertiesEditor(Skeleton3D *p_skeleton) :
 		skeleton(p_skeleton) {
 		skeleton(p_skeleton) {
 	create_editors();
 	create_editors();
 }
 }
 
 
-void BoneTransformEditor::set_keyable(const bool p_keyable) {
+void BonePropertiesEditor::set_keyable(const bool p_keyable) {
 	position_property->set_keying(p_keyable);
 	position_property->set_keying(p_keyable);
 	rotation_property->set_keying(p_keyable);
 	rotation_property->set_keying(p_keyable);
 	scale_property->set_keying(p_keyable);
 	scale_property->set_keying(p_keyable);
 }
 }
 
 
-void BoneTransformEditor::set_target(const String &p_prop) {
+void BonePropertiesEditor::set_target(const String &p_prop) {
 	enabled_checkbox->set_object_and_property(skeleton, p_prop + "enabled");
 	enabled_checkbox->set_object_and_property(skeleton, p_prop + "enabled");
 	enabled_checkbox->update_property();
 	enabled_checkbox->update_property();
 
 
@@ -162,7 +248,7 @@ void BoneTransformEditor::set_target(const String &p_prop) {
 	rest_matrix->update_property();
 	rest_matrix->update_property();
 }
 }
 
 
-void BoneTransformEditor::_property_keyed(const String &p_path, bool p_advance) {
+void BonePropertiesEditor::_property_keyed(const String &p_path, bool p_advance) {
 	AnimationTrackEditor *te = AnimationPlayerEditor::get_singleton()->get_track_editor();
 	AnimationTrackEditor *te = AnimationPlayerEditor::get_singleton()->get_track_editor();
 	if (!te || !te->has_keying()) {
 	if (!te || !te->has_keying()) {
 		return;
 		return;
@@ -183,16 +269,17 @@ void BoneTransformEditor::_property_keyed(const String &p_path, bool p_advance)
 	}
 	}
 }
 }
 
 
-void BoneTransformEditor::_update_properties() {
+void BonePropertiesEditor::_update_properties() {
 	if (!skeleton) {
 	if (!skeleton) {
 		return;
 		return;
 	}
 	}
 	int selected = Skeleton3DEditor::get_singleton()->get_selected_bone();
 	int selected = Skeleton3DEditor::get_singleton()->get_selected_bone();
 	List<PropertyInfo> props;
 	List<PropertyInfo> props;
+	HashSet<StringName> meta_seen;
 	skeleton->get_property_list(&props);
 	skeleton->get_property_list(&props);
 	for (const PropertyInfo &E : props) {
 	for (const PropertyInfo &E : props) {
 		PackedStringArray split = E.name.split("/");
 		PackedStringArray split = E.name.split("/");
-		if (split.size() == 3 && split[0] == "bones") {
+		if (split.size() >= 3 && split[0] == "bones") {
 			if (split[1].to_int() == selected) {
 			if (split[1].to_int() == selected) {
 				if (split[2] == "enabled") {
 				if (split[2] == "enabled") {
 					enabled_checkbox->set_read_only(E.usage & PROPERTY_USAGE_READ_ONLY);
 					enabled_checkbox->set_read_only(E.usage & PROPERTY_USAGE_READ_ONLY);
@@ -224,9 +311,35 @@ void BoneTransformEditor::_update_properties() {
 					rest_matrix->update_editor_property_status();
 					rest_matrix->update_editor_property_status();
 					rest_matrix->queue_redraw();
 					rest_matrix->queue_redraw();
 				}
 				}
+				if (split[2] == "bone_meta") {
+					meta_seen.insert(E.name);
+					if (!meta_editors.find(E.name)) {
+						EditorProperty *editor = EditorInspectorDefaultPlugin::get_editor_for_property(skeleton, E.type, E.name, PROPERTY_HINT_NONE, "", E.usage);
+						editor->set_label(split[3]);
+						editor->set_object_and_property(skeleton, E.name);
+						editor->set_deletable(true);
+						editor->set_selectable(false);
+						editor->connect("property_changed", callable_mp(this, &BonePropertiesEditor::_meta_changed));
+						editor->connect("property_deleted", callable_mp(this, &BonePropertiesEditor::_meta_deleted));
+
+						meta_section->get_vbox()->add_child(editor);
+						editor->update_property();
+						editor->update_editor_property_status();
+						editor->queue_redraw();
+
+						meta_editors[E.name] = editor;
+					}
+				}
 			}
 			}
 		}
 		}
 	}
 	}
+	// UI for any bone metadata prop not seen during the iteration has to be deleted
+	for (KeyValue<StringName, EditorProperty *> iter : meta_editors) {
+		if (!meta_seen.has(iter.key)) {
+			callable_mp((Node *)meta_section->get_vbox(), &Node::remove_child).call_deferred(iter.value);
+			meta_editors.remove(meta_editors.find(iter.key));
+		}
+	}
 }
 }
 
 
 Skeleton3DEditor *Skeleton3DEditor::singleton = nullptr;
 Skeleton3DEditor *Skeleton3DEditor::singleton = nullptr;
@@ -992,7 +1105,7 @@ void Skeleton3DEditor::create_editors() {
 	SET_DRAG_FORWARDING_GCD(joint_tree, Skeleton3DEditor);
 	SET_DRAG_FORWARDING_GCD(joint_tree, Skeleton3DEditor);
 	s_con->add_child(joint_tree);
 	s_con->add_child(joint_tree);
 
 
-	pose_editor = memnew(BoneTransformEditor(skeleton));
+	pose_editor = memnew(BonePropertiesEditor(skeleton));
 	pose_editor->set_label(TTR("Bone Transform"));
 	pose_editor->set_label(TTR("Bone Transform"));
 	pose_editor->set_visible(false);
 	pose_editor->set_visible(false);
 	add_child(pose_editor);
 	add_child(pose_editor);

+ 17 - 5
editor/plugins/skeleton_3d_editor_plugin.h

@@ -31,6 +31,7 @@
 #ifndef SKELETON_3D_EDITOR_PLUGIN_H
 #ifndef SKELETON_3D_EDITOR_PLUGIN_H
 #define SKELETON_3D_EDITOR_PLUGIN_H
 #define SKELETON_3D_EDITOR_PLUGIN_H
 
 
+#include "editor/add_metadata_dialog.h"
 #include "editor/editor_properties.h"
 #include "editor/editor_properties.h"
 #include "editor/gui/editor_file_dialog.h"
 #include "editor/gui/editor_file_dialog.h"
 #include "editor/plugins/editor_plugin.h"
 #include "editor/plugins/editor_plugin.h"
@@ -50,8 +51,8 @@ class Tree;
 class TreeItem;
 class TreeItem;
 class VSeparator;
 class VSeparator;
 
 
-class BoneTransformEditor : public VBoxContainer {
-	GDCLASS(BoneTransformEditor, VBoxContainer);
+class BonePropertiesEditor : public VBoxContainer {
+	GDCLASS(BonePropertiesEditor, VBoxContainer);
 
 
 	EditorInspectorSection *section = nullptr;
 	EditorInspectorSection *section = nullptr;
 
 
@@ -63,6 +64,10 @@ class BoneTransformEditor : public VBoxContainer {
 	EditorInspectorSection *rest_section = nullptr;
 	EditorInspectorSection *rest_section = nullptr;
 	EditorPropertyTransform3D *rest_matrix = nullptr;
 	EditorPropertyTransform3D *rest_matrix = nullptr;
 
 
+	EditorInspectorSection *meta_section = nullptr;
+	AddMetadataDialog *add_meta_dialog = nullptr;
+	Button *add_metadata_button = nullptr;
+
 	Rect2 background_rects[5];
 	Rect2 background_rects[5];
 
 
 	Skeleton3D *skeleton = nullptr;
 	Skeleton3D *skeleton = nullptr;
@@ -79,11 +84,18 @@ class BoneTransformEditor : public VBoxContainer {
 
 
 	void _property_keyed(const String &p_path, bool p_advance);
 	void _property_keyed(const String &p_path, bool p_advance);
 
 
+	void _meta_changed(const String &p_property, const Variant &p_value, const String &p_name, bool p_changing);
+	void _meta_deleted(const String &p_property);
+	void _show_add_meta_dialog();
+	void _add_meta_confirm();
+
+	HashMap<StringName, EditorProperty *> meta_editors;
+
 protected:
 protected:
 	void _notification(int p_what);
 	void _notification(int p_what);
 
 
 public:
 public:
-	BoneTransformEditor(Skeleton3D *p_skeleton);
+	BonePropertiesEditor(Skeleton3D *p_skeleton);
 
 
 	// Which transform target to modify.
 	// Which transform target to modify.
 	void set_target(const String &p_prop);
 	void set_target(const String &p_prop);
@@ -123,8 +135,8 @@ class Skeleton3DEditor : public VBoxContainer {
 	};
 	};
 
 
 	Tree *joint_tree = nullptr;
 	Tree *joint_tree = nullptr;
-	BoneTransformEditor *rest_editor = nullptr;
-	BoneTransformEditor *pose_editor = nullptr;
+	BonePropertiesEditor *rest_editor = nullptr;
+	BonePropertiesEditor *pose_editor = nullptr;
 
 
 	HBoxContainer *topmenu_bar = nullptr;
 	HBoxContainer *topmenu_bar = nullptr;
 	MenuButton *skeleton_options = nullptr;
 	MenuButton *skeleton_options = nullptr;

+ 4 - 0
modules/gltf/gltf_document.cpp

@@ -5534,6 +5534,10 @@ void GLTFDocument::_convert_skeleton_to_gltf(Skeleton3D *p_skeleton3d, Ref<GLTFS
 		joint_node->set_name(_gen_unique_name(p_state, skeleton->get_bone_name(bone_i)));
 		joint_node->set_name(_gen_unique_name(p_state, skeleton->get_bone_name(bone_i)));
 		joint_node->transform = skeleton->get_bone_pose(bone_i);
 		joint_node->transform = skeleton->get_bone_pose(bone_i);
 		joint_node->joint = true;
 		joint_node->joint = true;
+
+		if (p_skeleton3d->has_bone_meta(bone_i, "extras")) {
+			joint_node->set_meta("extras", p_skeleton3d->get_bone_meta(bone_i, "extras"));
+		}
 		GLTFNodeIndex current_node_i = p_state->nodes.size();
 		GLTFNodeIndex current_node_i = p_state->nodes.size();
 		p_state->scene_nodes.insert(current_node_i, skeleton);
 		p_state->scene_nodes.insert(current_node_i, skeleton);
 		p_state->nodes.push_back(joint_node);
 		p_state->nodes.push_back(joint_node);

+ 5 - 0
modules/gltf/skin_tool.cpp

@@ -602,6 +602,11 @@ Error SkinTool::_create_skeletons(
 			skeleton->set_bone_pose_rotation(bone_index, node->transform.basis.get_rotation_quaternion());
 			skeleton->set_bone_pose_rotation(bone_index, node->transform.basis.get_rotation_quaternion());
 			skeleton->set_bone_pose_scale(bone_index, node->transform.basis.get_scale());
 			skeleton->set_bone_pose_scale(bone_index, node->transform.basis.get_scale());
 
 
+			// Store bone-level GLTF extras in skeleton per bone meta.
+			if (node->has_meta("extras")) {
+				skeleton->set_bone_meta(bone_index, "extras", node->get_meta("extras"));
+			}
+
 			if (node->parent >= 0 && nodes[node->parent]->skeleton == skel_i) {
 			if (node->parent >= 0 && nodes[node->parent]->skeleton == skel_i) {
 				const int bone_parent = skeleton->find_bone(nodes[node->parent]->get_name());
 				const int bone_parent = skeleton->find_bone(nodes[node->parent]->get_name());
 				ERR_FAIL_COND_V(bone_parent < 0, FAILED);
 				ERR_FAIL_COND_V(bone_parent < 0, FAILED);

+ 57 - 0
modules/gltf/tests/test_gltf_extras.h

@@ -41,6 +41,7 @@
 #include "modules/gltf/gltf_document.h"
 #include "modules/gltf/gltf_document.h"
 #include "modules/gltf/gltf_state.h"
 #include "modules/gltf/gltf_state.h"
 #include "scene/3d/mesh_instance_3d.h"
 #include "scene/3d/mesh_instance_3d.h"
+#include "scene/3d/skeleton_3d.h"
 #include "scene/main/window.h"
 #include "scene/main/window.h"
 #include "scene/resources/3d/primitive_meshes.h"
 #include "scene/resources/3d/primitive_meshes.h"
 #include "scene/resources/material.h"
 #include "scene/resources/material.h"
@@ -158,6 +159,62 @@ TEST_CASE("[SceneTree][Node] GLTF test mesh and material meta export and import"
 	memdelete(original);
 	memdelete(original);
 	memdelete(loaded);
 	memdelete(loaded);
 }
 }
+
+TEST_CASE("[SceneTree][Node] GLTF test skeleton and bone export and import") {
+	// Setup scene.
+	Skeleton3D *skeleton = memnew(Skeleton3D);
+	skeleton->set_name("skeleton");
+	Dictionary skeleton_extras;
+	skeleton_extras["node_type"] = "skeleton";
+	skeleton->set_meta("extras", skeleton_extras);
+
+	skeleton->add_bone("parent");
+	skeleton->set_bone_rest(0, Transform3D());
+	Dictionary parent_bone_extras;
+	parent_bone_extras["bone"] = "i_am_parent_bone";
+	skeleton->set_bone_meta(0, "extras", parent_bone_extras);
+
+	skeleton->add_bone("child");
+	skeleton->set_bone_rest(1, Transform3D());
+	skeleton->set_bone_parent(1, 0);
+	Dictionary child_bone_extras;
+	child_bone_extras["bone"] = "i_am_child_bone";
+	skeleton->set_bone_meta(1, "extras", child_bone_extras);
+
+	// We have to have a mesh to link with skeleton or it will not get imported.
+	Ref<PlaneMesh> meshdata = memnew(PlaneMesh);
+	meshdata->set_name("planemesh");
+
+	MeshInstance3D *mesh = memnew(MeshInstance3D);
+	mesh->set_mesh(meshdata);
+	mesh->set_name("mesh_instance_3d");
+
+	Node3D *scene = memnew(Node3D);
+	SceneTree::get_singleton()->get_root()->add_child(scene);
+	scene->add_child(skeleton);
+	scene->add_child(mesh);
+	scene->set_name("node3d");
+
+	// Now that both skeleton and mesh are part of scene, link them.
+	mesh->set_skeleton_path(mesh->get_path_to(skeleton));
+
+	// Convert to GLFT and back.
+	String tempfile = OS::get_singleton()->get_cache_path().path_join("gltf_bone_extras");
+	Node *loaded = _gltf_export_then_import(scene, tempfile);
+
+	// Compare the results.
+	CHECK(loaded->get_name() == "node3d");
+	Skeleton3D *result = Object::cast_to<Skeleton3D>(loaded->find_child("Skeleton3D", false, true));
+	CHECK(result->get_bone_name(0) == "parent");
+	CHECK(Dictionary(result->get_bone_meta(0, "extras"))["bone"] == "i_am_parent_bone");
+	CHECK(result->get_bone_name(1) == "child");
+	CHECK(Dictionary(result->get_bone_meta(1, "extras"))["bone"] == "i_am_child_bone");
+
+	memdelete(skeleton);
+	memdelete(mesh);
+	memdelete(scene);
+	memdelete(loaded);
+}
 } // namespace TestGltfExtras
 } // namespace TestGltfExtras
 
 
 #endif // TOOLS_ENABLED
 #endif // TOOLS_ENABLED

+ 65 - 0
scene/3d/skeleton_3d.cpp

@@ -103,6 +103,8 @@ bool Skeleton3D::_set(const StringName &p_path, const Variant &p_value) {
 		set_bone_pose_rotation(which, p_value);
 		set_bone_pose_rotation(which, p_value);
 	} else if (what == "scale") {
 	} else if (what == "scale") {
 		set_bone_pose_scale(which, p_value);
 		set_bone_pose_scale(which, p_value);
+	} else if (what == "bone_meta") {
+		set_bone_meta(which, path.get_slicec('/', 3), p_value);
 #ifndef DISABLE_DEPRECATED
 #ifndef DISABLE_DEPRECATED
 	} else if (what == "pose" || what == "bound_children") {
 	} else if (what == "pose" || what == "bound_children") {
 		// Kept for compatibility from 3.x to 4.x.
 		// Kept for compatibility from 3.x to 4.x.
@@ -170,6 +172,8 @@ bool Skeleton3D::_get(const StringName &p_path, Variant &r_ret) const {
 		r_ret = get_bone_pose_rotation(which);
 		r_ret = get_bone_pose_rotation(which);
 	} else if (what == "scale") {
 	} else if (what == "scale") {
 		r_ret = get_bone_pose_scale(which);
 		r_ret = get_bone_pose_scale(which);
+	} else if (what == "bone_meta") {
+		r_ret = get_bone_meta(which, path.get_slicec('/', 3));
 	} else {
 	} else {
 		return false;
 		return false;
 	}
 	}
@@ -187,6 +191,11 @@ void Skeleton3D::_get_property_list(List<PropertyInfo> *p_list) const {
 		p_list->push_back(PropertyInfo(Variant::VECTOR3, prep + PNAME("position"), PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR));
 		p_list->push_back(PropertyInfo(Variant::VECTOR3, prep + PNAME("position"), PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR));
 		p_list->push_back(PropertyInfo(Variant::QUATERNION, prep + PNAME("rotation"), PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR));
 		p_list->push_back(PropertyInfo(Variant::QUATERNION, prep + PNAME("rotation"), PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR));
 		p_list->push_back(PropertyInfo(Variant::VECTOR3, prep + PNAME("scale"), PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR));
 		p_list->push_back(PropertyInfo(Variant::VECTOR3, prep + PNAME("scale"), PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR));
+
+		for (const KeyValue<StringName, Variant> &K : bones[i].metadata) {
+			PropertyInfo pi = PropertyInfo(bones[i].metadata[K.key].get_type(), prep + PNAME("bone_meta/") + K.key, PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR);
+			p_list->push_back(pi);
+		}
 	}
 	}
 
 
 	for (PropertyInfo &E : *p_list) {
 	for (PropertyInfo &E : *p_list) {
@@ -531,6 +540,57 @@ void Skeleton3D::set_bone_name(int p_bone, const String &p_name) {
 	version++;
 	version++;
 }
 }
 
 
+Variant Skeleton3D::get_bone_meta(int p_bone, const StringName &p_key) const {
+	const int bone_size = bones.size();
+	ERR_FAIL_INDEX_V(p_bone, bone_size, Variant());
+
+	if (!bones[p_bone].metadata.has(p_key)) {
+		return Variant();
+	}
+	return bones[p_bone].metadata[p_key];
+}
+
+TypedArray<StringName> Skeleton3D::_get_bone_meta_list_bind(int p_bone) const {
+	const int bone_size = bones.size();
+	ERR_FAIL_INDEX_V(p_bone, bone_size, TypedArray<StringName>());
+
+	TypedArray<StringName> _metaret;
+	for (const KeyValue<StringName, Variant> &K : bones[p_bone].metadata) {
+		_metaret.push_back(K.key);
+	}
+	return _metaret;
+}
+
+void Skeleton3D::get_bone_meta_list(int p_bone, List<StringName> *p_list) const {
+	const int bone_size = bones.size();
+	ERR_FAIL_INDEX(p_bone, bone_size);
+
+	for (const KeyValue<StringName, Variant> &K : bones[p_bone].metadata) {
+		p_list->push_back(K.key);
+	}
+}
+
+bool Skeleton3D::has_bone_meta(int p_bone, const StringName &p_key) const {
+	const int bone_size = bones.size();
+	ERR_FAIL_INDEX_V(p_bone, bone_size, false);
+
+	return bones[p_bone].metadata.has(p_key);
+}
+
+void Skeleton3D::set_bone_meta(int p_bone, const StringName &p_key, const Variant &p_value) {
+	const int bone_size = bones.size();
+	ERR_FAIL_INDEX(p_bone, bone_size);
+
+	if (p_value.get_type() == Variant::NIL) {
+		if (bones.write[p_bone].metadata.has(p_key)) {
+			bones.write[p_bone].metadata.erase(p_key);
+		}
+		return;
+	}
+
+	bones.write[p_bone].metadata.insert(p_key, p_value, false);
+}
+
 bool Skeleton3D::is_bone_parent_of(int p_bone, int p_parent_bone_id) const {
 bool Skeleton3D::is_bone_parent_of(int p_bone, int p_parent_bone_id) const {
 	int parent_of_bone = get_bone_parent(p_bone);
 	int parent_of_bone = get_bone_parent(p_bone);
 
 
@@ -1014,6 +1074,11 @@ void Skeleton3D::_bind_methods() {
 	ClassDB::bind_method(D_METHOD("get_bone_name", "bone_idx"), &Skeleton3D::get_bone_name);
 	ClassDB::bind_method(D_METHOD("get_bone_name", "bone_idx"), &Skeleton3D::get_bone_name);
 	ClassDB::bind_method(D_METHOD("set_bone_name", "bone_idx", "name"), &Skeleton3D::set_bone_name);
 	ClassDB::bind_method(D_METHOD("set_bone_name", "bone_idx", "name"), &Skeleton3D::set_bone_name);
 
 
+	ClassDB::bind_method(D_METHOD("get_bone_meta", "bone_idx", "key"), &Skeleton3D::get_bone_meta);
+	ClassDB::bind_method(D_METHOD("get_bone_meta_list", "bone_idx"), &Skeleton3D::_get_bone_meta_list_bind);
+	ClassDB::bind_method(D_METHOD("has_bone_meta", "bone_idx", "key"), &Skeleton3D::has_bone_meta);
+	ClassDB::bind_method(D_METHOD("set_bone_meta", "bone_idx", "key", "value"), &Skeleton3D::set_bone_meta);
+
 	ClassDB::bind_method(D_METHOD("get_concatenated_bone_names"), &Skeleton3D::get_concatenated_bone_names);
 	ClassDB::bind_method(D_METHOD("get_concatenated_bone_names"), &Skeleton3D::get_concatenated_bone_names);
 
 
 	ClassDB::bind_method(D_METHOD("get_bone_parent", "bone_idx"), &Skeleton3D::get_bone_parent);
 	ClassDB::bind_method(D_METHOD("get_bone_parent", "bone_idx"), &Skeleton3D::get_bone_parent);

+ 9 - 0
scene/3d/skeleton_3d.h

@@ -116,6 +116,8 @@ private:
 			}
 			}
 		}
 		}
 
 
+		HashMap<StringName, Variant> metadata;
+
 #ifndef DISABLE_DEPRECATED
 #ifndef DISABLE_DEPRECATED
 		Transform3D pose_global_no_override;
 		Transform3D pose_global_no_override;
 		real_t global_pose_override_amount = 0.0;
 		real_t global_pose_override_amount = 0.0;
@@ -193,6 +195,7 @@ protected:
 	void _get_property_list(List<PropertyInfo> *p_list) const;
 	void _get_property_list(List<PropertyInfo> *p_list) const;
 	void _validate_property(PropertyInfo &p_property) const;
 	void _validate_property(PropertyInfo &p_property) const;
 	void _notification(int p_what);
 	void _notification(int p_what);
+	TypedArray<StringName> _get_bone_meta_list_bind(int p_bone) const;
 	static void _bind_methods();
 	static void _bind_methods();
 
 
 	virtual void add_child_notify(Node *p_child) override;
 	virtual void add_child_notify(Node *p_child) override;
@@ -238,6 +241,12 @@ public:
 	void set_motion_scale(float p_motion_scale);
 	void set_motion_scale(float p_motion_scale);
 	float get_motion_scale() const;
 	float get_motion_scale() const;
 
 
+	// bone metadata
+	Variant get_bone_meta(int p_bone, const StringName &p_key) const;
+	void get_bone_meta_list(int p_bone, List<StringName> *p_list) const;
+	bool has_bone_meta(int p_bone, const StringName &p_key) const;
+	void set_bone_meta(int p_bone, const StringName &p_key, const Variant &p_value);
+
 	// Posing API
 	// Posing API
 	Transform3D get_bone_pose(int p_bone) const;
 	Transform3D get_bone_pose(int p_bone) const;
 	Vector3 get_bone_pose_position(int p_bone) const;
 	Vector3 get_bone_pose_position(int p_bone) const;

+ 78 - 0
tests/scene/test_skeleton_3d.h

@@ -0,0 +1,78 @@
+/**************************************************************************/
+/*  test_skeleton_3d.h                                                    */
+/**************************************************************************/
+/*                         This file is part of:                          */
+/*                             GODOT ENGINE                               */
+/*                        https://godotengine.org                         */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur.                  */
+/*                                                                        */
+/* Permission is hereby granted, free of charge, to any person obtaining  */
+/* a copy of this software and associated documentation files (the        */
+/* "Software"), to deal in the Software without restriction, including    */
+/* without limitation the rights to use, copy, modify, merge, publish,    */
+/* distribute, sublicense, and/or sell copies of the Software, and to     */
+/* permit persons to whom the Software is furnished to do so, subject to  */
+/* the following conditions:                                              */
+/*                                                                        */
+/* The above copyright notice and this permission notice shall be         */
+/* included in all copies or substantial portions of the Software.        */
+/*                                                                        */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,        */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF     */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY   */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,   */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE      */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                 */
+/**************************************************************************/
+
+#ifndef TEST_SKELETON_3D_H
+#define TEST_SKELETON_3D_H
+
+#include "tests/test_macros.h"
+
+#include "scene/3d/skeleton_3d.h"
+
+namespace TestSkeleton3D {
+
+TEST_CASE("[Skeleton3D] Test per-bone meta") {
+	Skeleton3D *skeleton = memnew(Skeleton3D);
+	skeleton->add_bone("root");
+	skeleton->set_bone_rest(0, Transform3D());
+
+	// Adding meta to bone.
+	skeleton->set_bone_meta(0, "key1", "value1");
+	skeleton->set_bone_meta(0, "key2", 12345);
+	CHECK_MESSAGE(skeleton->get_bone_meta(0, "key1") == "value1", "Bone meta missing.");
+	CHECK_MESSAGE(skeleton->get_bone_meta(0, "key2") == Variant(12345), "Bone meta missing.");
+
+	// Rename bone and check if meta persists.
+	skeleton->set_bone_name(0, "renamed_root");
+	CHECK_MESSAGE(skeleton->get_bone_meta(0, "key1") == "value1", "Bone meta missing.");
+	CHECK_MESSAGE(skeleton->get_bone_meta(0, "key2") == Variant(12345), "Bone meta missing.");
+
+	// Retrieve list of keys.
+	List<StringName> keys;
+	skeleton->get_bone_meta_list(0, &keys);
+	CHECK_MESSAGE(keys.size() == 2, "Wrong number of bone meta keys.");
+	CHECK_MESSAGE(keys.find("key1"), "key1 not found in bone meta list");
+	CHECK_MESSAGE(keys.find("key2"), "key2 not found in bone meta list");
+
+	// Removing meta.
+	skeleton->set_bone_meta(0, "key1", Variant());
+	skeleton->set_bone_meta(0, "key2", Variant());
+	CHECK_MESSAGE(!skeleton->has_bone_meta(0, "key1"), "Bone meta key1 should be deleted.");
+	CHECK_MESSAGE(!skeleton->has_bone_meta(0, "key2"), "Bone meta key2 should be deleted.");
+	List<StringName> should_be_empty_keys;
+	skeleton->get_bone_meta_list(0, &should_be_empty_keys);
+	CHECK_MESSAGE(should_be_empty_keys.size() == 0, "Wrong number of bone meta keys.");
+
+	// Deleting non-existing key should succeed.
+	skeleton->set_bone_meta(0, "non-existing-key", Variant());
+	memdelete(skeleton);
+}
+} // namespace TestSkeleton3D
+
+#endif // TEST_SKELETON_3D_H

+ 1 - 0
tests/test_main.cpp

@@ -160,6 +160,7 @@
 #include "tests/scene/test_path_3d.h"
 #include "tests/scene/test_path_3d.h"
 #include "tests/scene/test_path_follow_3d.h"
 #include "tests/scene/test_path_follow_3d.h"
 #include "tests/scene/test_primitives.h"
 #include "tests/scene/test_primitives.h"
+#include "tests/scene/test_skeleton_3d.h"
 #endif // _3D_DISABLED
 #endif // _3D_DISABLED
 
 
 #include "modules/modules_tests.gen.h"
 #include "modules/modules_tests.gen.h"