Merge pull request #99700 from hpvb/scene_tree_editor_performance

Improve Scene Tree editor performance
This commit is contained in:
Rémi Verschelde 2024-12-16 17:16:00 +01:00 committed by GitHub
commit 08508d2e01
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 818 additions and 181 deletions

View file

@ -89,6 +89,7 @@ enum PropertyHint {
PROPERTY_HINT_DICTIONARY_TYPE,
PROPERTY_HINT_TOOL_BUTTON,
PROPERTY_HINT_ONESHOT, ///< the property will be changed by self after setting, such as AudioStreamPlayer.playing, Particles.emitting.
PROPERTY_HINT_NO_NODEPATH, /// < this property will not contain a NodePath, regardless of type (Array, Dictionary, List, etc.). Needed for SceneTreeDock.
PROPERTY_HINT_MAX,
};

View file

@ -2943,7 +2943,7 @@
<constant name="PROPERTY_HINT_ONESHOT" value="40" enum="PropertyHint">
Hints that a property will be changed on its own after setting, such as [member AudioStreamPlayer.playing] or [member GPUParticles3D.emitting].
</constant>
<constant name="PROPERTY_HINT_MAX" value="41" enum="PropertyHint">
<constant name="PROPERTY_HINT_MAX" value="42" enum="PropertyHint">
Represents the size of the [enum PropertyHint] enum.
</constant>
<constant name="PROPERTY_USAGE_NONE" value="0" enum="PropertyUsageFlags" is_bitfield="true">

View file

@ -1072,6 +1072,11 @@
Emitted when the node's editor description field changed.
</description>
</signal>
<signal name="editor_state_changed">
<description>
Emitted when an attribute of the node that is relevant to the editor is changed. Only emitted in the editor.
</description>
</signal>
<signal name="ready">
<description>
Emitted when the node is considered ready, after [method _ready] is called.

View file

@ -36,6 +36,12 @@
Calls the [param method] on the actual TreeItem and its children recursively. Pass parameters as a comma separated list.
</description>
</method>
<method name="clear_buttons">
<return type="void" />
<description>
Removes all buttons from all columns of this item.
</description>
</method>
<method name="clear_custom_bg_color">
<return type="void" />
<param index="0" name="column" type="int" />

View file

@ -735,6 +735,7 @@ ConnectDialog::ConnectDialog() {
from_signal->set_editable(false);
tree = memnew(SceneTreeEditor(false));
tree->set_update_when_invisible(false);
tree->set_connecting_signal(true);
tree->set_show_enabled_subscene(true);
tree->set_v_size_flags(Control::SIZE_FILL | Control::SIZE_EXPAND);

File diff suppressed because it is too large Load diff

View file

@ -58,6 +58,57 @@ class SceneTreeEditor : public Control {
BUTTON_UNIQUE = 9,
};
struct CachedNode {
Node *node = nullptr;
TreeItem *item = nullptr;
int index = -1;
bool dirty = true;
bool has_moved_children = false;
bool removed = false;
// Store the iterator for faster removal. This is safe as
// HashMap never moves elements.
HashMap<Node *, CachedNode>::Iterator cache_iterator;
// This is safe because it gets compared to a uint8_t.
uint16_t delete_serial = UINT16_MAX;
// To know whether to update children or not.
bool can_process = false;
CachedNode() = delete; // Always an error.
CachedNode(Node *p_node, TreeItem *p_item) :
node(p_node), item(p_item) {}
};
struct NodeCache {
~NodeCache() {
clear();
}
NodeCache(SceneTreeEditor *p_editor) :
editor(p_editor) {}
HashMap<Node *, CachedNode>::Iterator add(Node *p_node, TreeItem *p_item);
HashMap<Node *, CachedNode>::Iterator get(Node *p_node, bool p_deleted_ok = true);
void remove(Node *p_node, bool p_recursive = false);
void mark_dirty(Node *p_node, bool p_parents = true);
void mark_children_dirty(Node *p_node, bool p_recursive = false);
void delete_pending();
void clear();
SceneTreeEditor *editor;
HashMap<Node *, CachedNode> cache;
HashSet<CachedNode *> to_delete;
Node *current_scene_node = nullptr;
Node *current_pinned_node = nullptr;
bool current_has_pin = false;
bool force_update = false;
uint8_t delete_serial = 0;
};
NodeCache node_cache;
Tree *tree = nullptr;
Node *selected = nullptr;
ObjectID instance_node;
@ -77,17 +128,30 @@ class SceneTreeEditor : public Control {
bool auto_expand_selected = true;
bool connect_to_script_mode = false;
bool connecting_signal = false;
bool update_when_invisible = true;
int blocked;
void _compute_hash(Node *p_node, uint64_t &hash);
void _reset();
void _update_node_path(Node *p_node, bool p_recursive = true);
void _update_node_subtree(Node *p_node, TreeItem *p_parent, bool p_force = false);
void _update_node(Node *p_node, TreeItem *p_item, bool p_part_of_subscene);
void _update_if_clean();
void _add_nodes(Node *p_node, TreeItem *p_parent);
void _test_update_tree();
bool _update_filter(TreeItem *p_parent = nullptr, bool p_scroll_to_selected = false);
bool _item_matches_all_terms(TreeItem *p_item, const PackedStringArray &p_terms);
void _tree_changed();
void _tree_process_mode_changed();
void _move_node_children(HashMap<Node *, CachedNode>::Iterator &p_I);
void _move_node_item(TreeItem *p_parent, HashMap<Node *, CachedNode>::Iterator &p_I);
void _node_child_order_changed(Node *p_node);
void _node_editor_state_changed(Node *p_node);
void _node_added(Node *p_node);
void _node_removed(Node *p_node);
void _node_renamed(Node *p_node);
@ -142,6 +206,7 @@ class SceneTreeEditor : public Control {
void _rmb_select(const Vector2 &p_pos, MouseButton p_button = MouseButton::RIGHT);
void _warning_changed(Node *p_for_node);
void _update_marking_list(const HashSet<Node *> &p_marked);
Timer *update_timer = nullptr;
@ -182,6 +247,7 @@ public:
void set_auto_expand_selected(bool p_auto, bool p_update_settings);
void set_connect_to_script_mode(bool p_enable);
void set_connecting_signal(bool p_enable);
void set_update_when_invisible(bool p_enable);
Tree *get_scene_tree() { return tree; }

View file

@ -76,6 +76,7 @@ ReparentDialog::ReparentDialog() {
add_child(vbc);
tree = memnew(SceneTreeEditor(false));
tree->set_update_when_invisible(false);
tree->set_show_enabled_subscene(true);
tree->get_scene_tree()->connect("item_activated", callable_mp(this, &ReparentDialog::_reparent));
vbc->add_margin_child(TTR("Select new parent:"), tree, true);

View file

@ -78,17 +78,31 @@ void SceneTreeDock::_quick_open(const String &p_file_path) {
instantiate_scenes({ p_file_path }, scene_tree->get_selected());
}
static void _restore_treeitem_custom_color(TreeItem *p_item) {
if (!p_item) {
return;
}
Color custom_color = p_item->get_meta(SNAME("custom_color"), Color(0, 0, 0, 0));
if (custom_color != Color(0, 0, 0, 0)) {
p_item->set_custom_color(0, custom_color);
} else {
p_item->clear_custom_color(0);
}
}
void SceneTreeDock::_inspect_hovered_node() {
select_node_hovered_at_end_of_drag = true;
Tree *tree = scene_tree->get_scene_tree();
TreeItem *item = tree->get_item_with_metadata(node_hovered_now->get_path());
_restore_treeitem_custom_color(tree_item_inspected);
tree_item_inspected = item;
if (item) {
if (tree_item_inspected) {
tree_item_inspected->clear_custom_color(0);
}
tree_item_inspected = item;
tree_item_inspected->set_custom_color(0, get_theme_color(SNAME("accent_color"), EditorStringName(Editor)));
Color accent_color = get_theme_color(SNAME("accent_color"), EditorStringName(Editor));
tree_item_inspected->set_custom_color(0, accent_color);
}
EditorSelectionHistory *editor_history = EditorNode::get_singleton()->get_editor_selection_history();
editor_history->add_object(node_hovered_now->get_instance_id());
InspectorDock::get_inspector_singleton()->edit(node_hovered_now);
@ -1716,7 +1730,7 @@ void SceneTreeDock::_notification(int p_what) {
case NOTIFICATION_DRAG_END: {
_reset_hovering_timer();
if (tree_item_inspected) {
tree_item_inspected->clear_custom_color(0);
_restore_treeitem_custom_color(tree_item_inspected);
tree_item_inspected = nullptr;
} else {
return;
@ -1963,6 +1977,49 @@ bool SceneTreeDock::_update_node_path(Node *p_root_node, NodePath &r_node_path,
return false;
}
_ALWAYS_INLINE_ static bool _recurse_into_property(const PropertyInfo &p_property) {
// Only check these types for NodePaths.
static const Variant::Type property_type_check[] = { Variant::OBJECT, Variant::NODE_PATH, Variant::ARRAY, Variant::DICTIONARY };
if (!(p_property.usage & (PROPERTY_USAGE_STORAGE | PROPERTY_USAGE_EDITOR))) {
return false;
}
// Avoid otherwise acceptable types if we marked them as irrelevant.
if (p_property.hint == PROPERTY_HINT_NO_NODEPATH) {
return false;
}
for (Variant::Type type : property_type_check) {
if (p_property.type == type) {
return true;
}
}
return false;
}
void SceneTreeDock::_check_object_properties_recursive(Node *p_root_node, Object *p_obj, HashMap<Node *, NodePath> *p_renames, bool p_inside_resource) const {
EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();
List<PropertyInfo> properties;
p_obj->get_property_list(&properties);
for (const PropertyInfo &E : properties) {
if (!_recurse_into_property(E)) {
continue;
}
StringName propertyname = E.name;
Variant old_variant = p_obj->get(propertyname);
Variant updated_variant = old_variant;
if (_check_node_path_recursive(p_root_node, updated_variant, p_renames, p_inside_resource)) {
undo_redo->add_do_property(p_obj, propertyname, updated_variant);
undo_redo->add_undo_property(p_obj, propertyname, old_variant);
}
}
}
bool SceneTreeDock::_check_node_path_recursive(Node *p_root_node, Variant &r_variant, HashMap<Node *, NodePath> *p_renames, bool p_inside_resource) const {
switch (r_variant.get_type()) {
case Variant::NODE_PATH: {
@ -2027,27 +2084,18 @@ bool SceneTreeDock::_check_node_path_recursive(Node *p_root_node, Variant &r_var
break;
}
if (Object::cast_to<Material>(resource)) {
// For performance reasons, assume that Materials don't have NodePaths in them.
// TODO This check could be removed when String performance has improved.
break;
}
if (!resource->is_built_in()) {
// For performance reasons, assume that scene paths are no concern for external resources.
break;
}
List<PropertyInfo> properties;
resource->get_property_list(&properties);
for (const PropertyInfo &E : properties) {
if (!(E.usage & (PROPERTY_USAGE_STORAGE | PROPERTY_USAGE_EDITOR))) {
continue;
}
String propertyname = E.name;
Variant old_variant = resource->get(propertyname);
Variant updated_variant = old_variant;
if (_check_node_path_recursive(p_root_node, updated_variant, p_renames, true)) {
EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();
undo_redo->add_do_property(resource, propertyname, updated_variant);
undo_redo->add_undo_property(resource, propertyname, old_variant);
}
}
_check_object_properties_recursive(p_root_node, resource, p_renames, true);
} break;
default: {
@ -2173,22 +2221,7 @@ void SceneTreeDock::perform_node_renames(Node *p_base, HashMap<Node *, NodePath>
}
// Renaming node paths used in node properties.
List<PropertyInfo> properties;
p_base->get_property_list(&properties);
for (const PropertyInfo &E : properties) {
if (!(E.usage & (PROPERTY_USAGE_STORAGE | PROPERTY_USAGE_EDITOR))) {
continue;
}
String propertyname = E.name;
Variant old_variant = p_base->get(propertyname);
Variant updated_variant = old_variant;
if (_check_node_path_recursive(p_base, updated_variant, p_renames)) {
EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();
undo_redo->add_do_property(p_base, propertyname, updated_variant);
undo_redo->add_undo_property(p_base, propertyname, old_variant);
}
}
_check_object_properties_recursive(p_base, p_base, p_renames);
for (int i = 0; i < p_base->get_child_count(); i++) {
perform_node_renames(p_base->get_child(i), p_renames, r_rem_anims);

View file

@ -302,6 +302,7 @@ class SceneTreeDock : public VBoxContainer {
static void _update_configuration_warning();
bool _update_node_path(Node *p_root_node, NodePath &r_node_path, HashMap<Node *, NodePath> *p_renames) const;
void _check_object_properties_recursive(Node *p_root_node, Object *p_obj, HashMap<Node *, NodePath> *p_renames, bool p_inside_resource = false) const;
bool _check_node_path_recursive(Node *p_root_node, Variant &r_variant, HashMap<Node *, NodePath> *p_renames, bool p_inside_resource = false) const;
bool _check_node_recursive(Variant &r_variant, Node *p_node, Node *p_by_node, const String type_hint, String &r_warn_message);
void _replace_node(Node *p_node, Node *p_by_node, bool p_keep_properties = true, bool p_remove_old = true);

View file

@ -1258,6 +1258,18 @@ void TreeItem::deselect(int p_column) {
_cell_deselected(p_column);
}
void TreeItem::clear_buttons() {
int i = 0;
for (Cell &cell : cells) {
if (!cell.buttons.is_empty()) {
cell.buttons.clear();
cell.cached_minimum_size_dirty = true;
_changed_notify(i);
}
++i;
}
}
void TreeItem::add_button(int p_column, const Ref<Texture2D> &p_button, int p_id, bool p_disabled, const String &p_tooltip) {
ERR_FAIL_INDEX(p_column, cells.size());
ERR_FAIL_COND(!p_button.is_valid());
@ -1768,6 +1780,7 @@ void TreeItem::_bind_methods() {
ClassDB::bind_method(D_METHOD("set_custom_as_button", "column", "enable"), &TreeItem::set_custom_as_button);
ClassDB::bind_method(D_METHOD("is_custom_set_as_button", "column"), &TreeItem::is_custom_set_as_button);
ClassDB::bind_method(D_METHOD("clear_buttons"), &TreeItem::clear_buttons);
ClassDB::bind_method(D_METHOD("add_button", "column", "button", "id", "disabled", "tooltip_text"), &TreeItem::add_button, DEFVAL(-1), DEFVAL(false), DEFVAL(""));
ClassDB::bind_method(D_METHOD("get_button_count", "column"), &TreeItem::get_button_count);
ClassDB::bind_method(D_METHOD("get_button_tooltip_text", "column", "button_index"), &TreeItem::get_button_tooltip_text);
@ -3726,7 +3739,7 @@ void Tree::gui_input(const Ref<InputEvent> &p_event) {
_determine_hovered_item();
bool rtl = is_layout_rtl();
if (pressing_for_editor && popup_pressing_edited_item && (popup_pressing_edited_item->get_cell_mode(popup_pressing_edited_item_column) == TreeItem::CELL_MODE_RANGE)) {
if (pressing_for_editor && popup_pressing_edited_item && !popup_pressing_edited_item->cells.is_empty() && (popup_pressing_edited_item->get_cell_mode(popup_pressing_edited_item_column) == TreeItem::CELL_MODE_RANGE)) {
/* This needs to happen now, because the popup can be closed when pressing another item, and must remain the popup edited item until it actually closes */
popup_edited_item = popup_pressing_edited_item;
popup_edited_item_col = popup_pressing_edited_item_column;

View file

@ -277,6 +277,7 @@ public:
void set_icon_max_width(int p_column, int p_max);
int get_icon_max_width(int p_column) const;
void clear_buttons();
void add_button(int p_column, const Ref<Texture2D> &p_button, int p_id = -1, bool p_disabled = false, const String &p_tooltip = "");
int get_button_count(int p_column) const;
String get_button_tooltip_text(int p_column, int p_index) const;

View file

@ -678,6 +678,8 @@ void Node::set_process_mode(ProcessMode p_mode) {
if (Engine::get_singleton()->is_editor_hint()) {
get_tree()->emit_signal(SNAME("tree_process_mode_changed"));
}
_emit_editor_state_changed();
#endif
}
@ -2163,6 +2165,7 @@ void Node::set_unique_name_in_owner(bool p_enabled) {
}
update_configuration_warnings();
_emit_editor_state_changed();
}
bool Node::is_unique_name_in_owner() const {
@ -2200,6 +2203,8 @@ void Node::set_owner(Node *p_owner) {
if (data.unique_name_in_owner) {
_acquire_unique_name_in_owner();
}
_emit_editor_state_changed();
}
Node *Node::get_owner() const {
@ -2383,6 +2388,9 @@ void Node::add_to_group(const StringName &p_identifier, bool p_persistent) {
gd.persistent = p_persistent;
data.grouped[p_identifier] = gd;
if (p_persistent) {
_emit_editor_state_changed();
}
}
void Node::remove_from_group(const StringName &p_identifier) {
@ -2393,11 +2401,21 @@ void Node::remove_from_group(const StringName &p_identifier) {
return;
}
#ifdef TOOLS_ENABLED
bool persistent = E->value.persistent;
#endif
if (data.tree) {
data.tree->remove_from_group(E->key, this);
}
data.grouped.remove(E);
#ifdef TOOLS_ENABLED
if (persistent) {
_emit_editor_state_changed();
}
#endif
}
TypedArray<StringName> Node::_get_groups() const {
@ -2560,6 +2578,7 @@ Ref<Tween> Node::create_tween() {
void Node::set_scene_file_path(const String &p_scene_file_path) {
ERR_THREAD_GUARD
data.scene_file_path = p_scene_file_path;
_emit_editor_state_changed();
}
String Node::get_scene_file_path() const {
@ -2592,6 +2611,8 @@ void Node::set_editable_instance(Node *p_node, bool p_editable) {
} else {
p_node->data.editable_instance = true;
}
p_node->_emit_editor_state_changed();
}
bool Node::is_editable_instance(const Node *p_node) const {
@ -2702,6 +2723,7 @@ Ref<SceneState> Node::get_scene_instance_state() const {
void Node::set_scene_inherited_state(const Ref<SceneState> &p_state) {
ERR_THREAD_GUARD
data.inherited_state = p_state;
_emit_editor_state_changed();
}
Ref<SceneState> Node::get_scene_inherited_state() const {
@ -2950,6 +2972,14 @@ void Node::remap_nested_resources(Ref<Resource> p_resource, const HashMap<Ref<Re
}
}
}
void Node::_emit_editor_state_changed() {
// This is required for the SceneTreeEditor to properly keep track of when an update is needed.
// This signal might be expensive and not needed for anything outside of the editor.
if (Engine::get_singleton()->is_editor_hint()) {
emit_signal(SNAME("editor_state_changed"));
}
}
#endif
// Duplicate node's properties.
@ -3843,6 +3873,7 @@ void Node::_bind_methods() {
ADD_SIGNAL(MethodInfo("child_order_changed"));
ADD_SIGNAL(MethodInfo("replacing_by", PropertyInfo(Variant::OBJECT, "node", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT, "Node")));
ADD_SIGNAL(MethodInfo("editor_description_changed", PropertyInfo(Variant::OBJECT, "node", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT, "Node")));
ADD_SIGNAL(MethodInfo("editor_state_changed"));
ADD_PROPERTY(PropertyInfo(Variant::STRING_NAME, "name", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NONE), "set_name", "get_name");
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "unique_name_in_owner", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR), "set_unique_name_in_owner", "is_unique_name_in_owner");
@ -3966,11 +3997,13 @@ bool Node::has_meta(const StringName &p_name) const {
void Node::set_meta(const StringName &p_name, const Variant &p_value) {
ERR_THREAD_GUARD;
Object::set_meta(p_name, p_value);
_emit_editor_state_changed();
}
void Node::remove_meta(const StringName &p_name) {
ERR_THREAD_GUARD;
Object::remove_meta(p_name);
_emit_editor_state_changed();
}
Variant Node::get_meta(const StringName &p_name, const Variant &p_default) const {
@ -4020,12 +4053,33 @@ void Node::get_signals_connected_to_this(List<Connection> *p_connections) const
Error Node::connect(const StringName &p_signal, const Callable &p_callable, uint32_t p_flags) {
ERR_THREAD_GUARD_V(ERR_INVALID_PARAMETER);
return Object::connect(p_signal, p_callable, p_flags);
Error retval = Object::connect(p_signal, p_callable, p_flags);
#ifdef TOOLS_ENABLED
if (p_flags & CONNECT_PERSIST) {
_emit_editor_state_changed();
}
#endif
return retval;
}
void Node::disconnect(const StringName &p_signal, const Callable &p_callable) {
ERR_THREAD_GUARD;
#ifdef TOOLS_ENABLED
// Already under thread guard, don't check again.
int old_connection_count = Object::get_persistent_signal_connection_count();
#endif
Object::disconnect(p_signal, p_callable);
#ifdef TOOLS_ENABLED
int new_connection_count = Object::get_persistent_signal_connection_count();
if (old_connection_count != new_connection_count) {
_emit_editor_state_changed();
}
#endif
}
bool Node::is_connected(const StringName &p_signal, const Callable &p_callable) const {

View file

@ -331,6 +331,13 @@ private:
Variant _call_deferred_thread_group_bind(const Variant **p_args, int p_argcount, Callable::CallError &r_error);
Variant _call_thread_safe_bind(const Variant **p_args, int p_argcount, Callable::CallError &r_error);
// Editor only signal to keep the SceneTreeEditor in sync.
#ifdef TOOLS_ENABLED
void _emit_editor_state_changed();
#else
void _emit_editor_state_changed() {}
#endif
protected:
void _block() { data.blocked++; }
void _unblock() { data.blocked--; }

View file

@ -1740,7 +1740,7 @@ void ArrayMesh::_get_property_list(List<PropertyInfo> *p_list) const {
}
for (int i = 0; i < surfaces.size(); i++) {
p_list->push_back(PropertyInfo(Variant::STRING, "surface_" + itos(i) + "/name", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_EDITOR));
p_list->push_back(PropertyInfo(Variant::STRING, "surface_" + itos(i) + "/name", PROPERTY_HINT_NO_NODEPATH, "", PROPERTY_USAGE_EDITOR));
if (surfaces[i].is_2d) {
p_list->push_back(PropertyInfo(Variant::OBJECT, "surface_" + itos(i) + "/material", PROPERTY_HINT_RESOURCE_TYPE, "CanvasItemMaterial,ShaderMaterial", PROPERTY_USAGE_EDITOR));
} else {
@ -2308,10 +2308,10 @@ void ArrayMesh::_bind_methods() {
ClassDB::bind_method(D_METHOD("_set_surfaces", "surfaces"), &ArrayMesh::_set_surfaces);
ClassDB::bind_method(D_METHOD("_get_surfaces"), &ArrayMesh::_get_surfaces);
ADD_PROPERTY(PropertyInfo(Variant::PACKED_STRING_ARRAY, "_blend_shape_names", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR | PROPERTY_USAGE_INTERNAL), "_set_blend_shape_names", "_get_blend_shape_names");
ADD_PROPERTY(PropertyInfo(Variant::ARRAY, "_surfaces", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR | PROPERTY_USAGE_INTERNAL), "_set_surfaces", "_get_surfaces");
ADD_PROPERTY(PropertyInfo(Variant::PACKED_STRING_ARRAY, "_blend_shape_names", PROPERTY_HINT_NO_NODEPATH, "", PROPERTY_USAGE_NO_EDITOR | PROPERTY_USAGE_INTERNAL), "_set_blend_shape_names", "_get_blend_shape_names");
ADD_PROPERTY(PropertyInfo(Variant::ARRAY, "_surfaces", PROPERTY_HINT_NO_NODEPATH, "", PROPERTY_USAGE_NO_EDITOR | PROPERTY_USAGE_INTERNAL), "_set_surfaces", "_get_surfaces");
ADD_PROPERTY(PropertyInfo(Variant::INT, "blend_shape_mode", PROPERTY_HINT_ENUM, "Normalized,Relative"), "set_blend_shape_mode", "get_blend_shape_mode");
ADD_PROPERTY(PropertyInfo(Variant::AABB, "custom_aabb", PROPERTY_HINT_NONE, "suffix:m"), "set_custom_aabb", "get_custom_aabb");
ADD_PROPERTY(PropertyInfo(Variant::AABB, "custom_aabb", PROPERTY_HINT_NO_NODEPATH, "suffix:m"), "set_custom_aabb", "get_custom_aabb");
ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "shadow_mesh", PROPERTY_HINT_RESOURCE_TYPE, "ArrayMesh"), "set_shadow_mesh", "get_shadow_mesh");
}