From ada95cb543f21bb9f39d5fc0d1f55bcdcf0480e4 Mon Sep 17 00:00:00 2001 From: "Silc Lizard (Tokage) Renew" <61938263+TokageItLab@users.noreply.github.com> Date: Tue, 30 Sep 2025 11:16:06 +0900 Subject: [PATCH] Add LimitAngularVelocityModifier3D --- .../LimitAngularVelocityModifier3D.xml | 102 +++++ .../icons/LimitAngularVelocityModifier3D.svg | 1 + .../3d/limit_angular_velocity_modifier_3d.cpp | 412 ++++++++++++++++++ scene/3d/limit_angular_velocity_modifier_3d.h | 110 +++++ scene/register_scene_types.cpp | 2 + 5 files changed, 627 insertions(+) create mode 100644 doc/classes/LimitAngularVelocityModifier3D.xml create mode 100644 editor/icons/LimitAngularVelocityModifier3D.svg create mode 100644 scene/3d/limit_angular_velocity_modifier_3d.cpp create mode 100644 scene/3d/limit_angular_velocity_modifier_3d.h diff --git a/doc/classes/LimitAngularVelocityModifier3D.xml b/doc/classes/LimitAngularVelocityModifier3D.xml new file mode 100644 index 00000000000..32b83d58d57 --- /dev/null +++ b/doc/classes/LimitAngularVelocityModifier3D.xml @@ -0,0 +1,102 @@ + + + + Limit bone rotation angular velocity. + + + This modifier limits bone rotation angular velocity by comparing poses between previous and current frame. + You can add bone chains by specifying their root and end bones, then add the bones between them to a list. Modifier processes either that list or the bones excluding those in the list depending on the option [member exclude]. + + + + + + + + Clear all chains. + + + + + + + Returns the end bone index of the bone chain. + + + + + + + Returns the end bone name of the bone chain. + + + + + + + Returns the root bone index of the bone chain. + + + + + + + Returns the root bone name of the bone chain. + + + + + + Sets the reference pose for angle comparison to the current pose with the influence of constraints removed. This function is automatically triggered when joints change or upon activation. + + + + + + + + Sets the end bone index of the bone chain. + + + + + + + + Sets the end bone name of the bone chain. + [b]Note:[/b] End bone must be the root bone or a child of the root bone. + + + + + + + + Sets the root bone index of the bone chain. + + + + + + + + Sets the root bone name of the bone chain. + + + + + + The number of chains. + + + If [code]true[/code], the modifier processes bones not included in the bone list. + If [code]false[/code], the bones processed by the modifier are equal to the bone list. + + + The number of joints in the list which created by chains dynamically. + + + The maximum angular velocity per second. + + + diff --git a/editor/icons/LimitAngularVelocityModifier3D.svg b/editor/icons/LimitAngularVelocityModifier3D.svg new file mode 100644 index 00000000000..f6e910eb4a6 --- /dev/null +++ b/editor/icons/LimitAngularVelocityModifier3D.svg @@ -0,0 +1 @@ + diff --git a/scene/3d/limit_angular_velocity_modifier_3d.cpp b/scene/3d/limit_angular_velocity_modifier_3d.cpp new file mode 100644 index 00000000000..89008a39021 --- /dev/null +++ b/scene/3d/limit_angular_velocity_modifier_3d.cpp @@ -0,0 +1,412 @@ +/**************************************************************************/ +/* limit_angular_velocity_modifier_3d.cpp */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#include "limit_angular_velocity_modifier_3d.h" + +bool LimitAngularVelocityModifier3D::_set(const StringName &p_path, const Variant &p_value) { + String path = p_path; + + if (path.begins_with("chains/")) { + int which = path.get_slicec('/', 1).to_int(); + String what = path.get_slicec('/', 2); + ERR_FAIL_INDEX_V(which, (int)chains.size(), false); + + if (what == "root_bone_name") { + set_root_bone_name(which, p_value); + } else if (what == "root_bone") { + set_root_bone(which, p_value); + } else if (what == "end_bone_name") { + set_end_bone_name(which, p_value); + } else if (what == "end_bone") { + set_end_bone(which, p_value); + } else { + return false; + } + } + return true; +} + +bool LimitAngularVelocityModifier3D::_get(const StringName &p_path, Variant &r_ret) const { + String path = p_path; + + if (path.begins_with("chains/")) { + int which = path.get_slicec('/', 1).to_int(); + String what = path.get_slicec('/', 2); + ERR_FAIL_INDEX_V(which, (int)chains.size(), false); + + if (what == "root_bone_name") { + r_ret = get_root_bone_name(which); + } else if (what == "root_bone") { + r_ret = get_root_bone(which); + } else if (what == "end_bone_name") { + r_ret = get_end_bone_name(which); + } else if (what == "end_bone") { + r_ret = get_end_bone(which); + } else { + return false; + } + } + if (path.begins_with("joints/")) { + int which = path.get_slicec('/', 1).to_int(); + String what = path.get_slicec('/', 2); + ERR_FAIL_COND_V(!joints.has(which), false); + if (what == "bone_name") { + r_ret = _get_joint_bone_name(which); + } else { + return false; + } + } + return true; +} + +void LimitAngularVelocityModifier3D::_get_property_list(List *p_list) const { + String enum_hint; + Skeleton3D *skeleton = get_skeleton(); + if (skeleton) { + enum_hint = skeleton->get_concatenated_bone_names(); + } + + for (uint32_t i = 0; i < chains.size(); i++) { + String path = "chains/" + itos(i) + "/"; + p_list->push_back(PropertyInfo(Variant::STRING, path + "root_bone_name", PROPERTY_HINT_ENUM_SUGGESTION, enum_hint)); + p_list->push_back(PropertyInfo(Variant::INT, path + "root_bone", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR)); + p_list->push_back(PropertyInfo(Variant::STRING, path + "end_bone_name", PROPERTY_HINT_ENUM_SUGGESTION, enum_hint)); + p_list->push_back(PropertyInfo(Variant::INT, path + "end_bone", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR)); + } + + for (const KeyValue &E : joints) { + String path = "joints/" + itos(E.key) + "/"; + p_list->push_back(PropertyInfo(Variant::STRING, path + "bone_name", PROPERTY_HINT_ENUM_SUGGESTION, enum_hint, PROPERTY_USAGE_EDITOR | PROPERTY_USAGE_READ_ONLY)); + } +} + +void LimitAngularVelocityModifier3D::_validate_property(PropertyInfo &p_property) const { + if (p_property.name == "joint_count") { + p_property.usage = PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_ARRAY | PROPERTY_USAGE_READ_ONLY; + p_property.class_name = "Joints,joints/,static,const"; + } +} + +void LimitAngularVelocityModifier3D::_notification(int p_what) { + switch (p_what) { + case NOTIFICATION_ENTER_TREE: { + _make_joints_dirty(); + } break; + } +} + +// Setting. + +void LimitAngularVelocityModifier3D::set_root_bone_name(int p_index, const String &p_bone_name) { + ERR_FAIL_INDEX(p_index, (int)chains.size()); + chains[p_index].root_bone.name = p_bone_name; + Skeleton3D *sk = get_skeleton(); + if (sk) { + set_root_bone(p_index, sk->find_bone(chains[p_index].root_bone.name)); + } +} + +String LimitAngularVelocityModifier3D::get_root_bone_name(int p_index) const { + ERR_FAIL_INDEX_V(p_index, (int)chains.size(), String()); + return chains[p_index].root_bone.name; +} + +void LimitAngularVelocityModifier3D::set_root_bone(int p_index, int p_bone) { + ERR_FAIL_INDEX(p_index, (int)chains.size()); + bool changed = chains[p_index].root_bone.bone != p_bone; + chains[p_index].root_bone.bone = p_bone; + Skeleton3D *sk = get_skeleton(); + if (sk) { + if (chains[p_index].root_bone.bone <= -1 || chains[p_index].root_bone.bone >= sk->get_bone_count()) { + WARN_PRINT("Root bone index out of range!"); + chains[p_index].root_bone.bone = -1; + } else { + chains[p_index].root_bone.name = sk->get_bone_name(chains[p_index].root_bone.bone); + } + } + if (changed) { + _make_joints_dirty(); + } +} + +int LimitAngularVelocityModifier3D::get_root_bone(int p_index) const { + ERR_FAIL_INDEX_V(p_index, (int)chains.size(), -1); + return chains[p_index].root_bone.bone; +} + +void LimitAngularVelocityModifier3D::set_end_bone_name(int p_index, const String &p_bone_name) { + ERR_FAIL_INDEX(p_index, (int)chains.size()); + chains[p_index].end_bone.name = p_bone_name; + Skeleton3D *sk = get_skeleton(); + if (sk) { + set_end_bone(p_index, sk->find_bone(chains[p_index].end_bone.name)); + } +} + +String LimitAngularVelocityModifier3D::get_end_bone_name(int p_index) const { + ERR_FAIL_INDEX_V(p_index, (int)chains.size(), String()); + return chains[p_index].end_bone.name; +} + +void LimitAngularVelocityModifier3D::set_end_bone(int p_index, int p_bone) { + ERR_FAIL_INDEX(p_index, (int)chains.size()); + bool changed = chains[p_index].end_bone.bone != p_bone; + chains[p_index].end_bone.bone = p_bone; + Skeleton3D *sk = get_skeleton(); + if (sk) { + if (chains[p_index].end_bone.bone <= -1 || chains[p_index].end_bone.bone >= sk->get_bone_count()) { + WARN_PRINT("End bone index out of range!"); + chains[p_index].end_bone.bone = -1; + } else { + chains[p_index].end_bone.name = sk->get_bone_name(chains[p_index].end_bone.bone); + } + } + if (changed) { + _make_joints_dirty(); + } + notify_property_list_changed(); +} + +int LimitAngularVelocityModifier3D::get_end_bone(int p_index) const { + ERR_FAIL_INDEX_V(p_index, (int)chains.size(), -1); + return chains[p_index].end_bone.bone; +} + +void LimitAngularVelocityModifier3D::set_chain_count(int p_count) { + ERR_FAIL_COND(p_count < 0); + chains.resize(p_count); + _make_joints_dirty(); + notify_property_list_changed(); +} + +int LimitAngularVelocityModifier3D::get_chain_count() const { + return chains.size(); +} + +void LimitAngularVelocityModifier3D::clear_chains() { + set_chain_count(0); +} + +String LimitAngularVelocityModifier3D::_get_joint_bone_name(int p_bone) const { + ERR_FAIL_COND_V(!joints.has(p_bone), String()); + return joints[p_bone]; +} + +int LimitAngularVelocityModifier3D::_get_joint_count() const { + return joints.size(); +} + +void LimitAngularVelocityModifier3D::set_max_angular_velocity(double p_angular_velocity) { + max_angular_velocity = p_angular_velocity; +} + +double LimitAngularVelocityModifier3D::get_max_angular_velocity() const { + return max_angular_velocity; +} + +void LimitAngularVelocityModifier3D::set_exclude(bool p_exclude) { + exclude = p_exclude; +} + +bool LimitAngularVelocityModifier3D::is_exclude() const { + return exclude; +} + +void LimitAngularVelocityModifier3D::_bind_methods() { + // Setting. + ClassDB::bind_method(D_METHOD("set_root_bone_name", "index", "bone_name"), &LimitAngularVelocityModifier3D::set_root_bone_name); + ClassDB::bind_method(D_METHOD("get_root_bone_name", "index"), &LimitAngularVelocityModifier3D::get_root_bone_name); + ClassDB::bind_method(D_METHOD("set_root_bone", "index", "bone"), &LimitAngularVelocityModifier3D::set_root_bone); + ClassDB::bind_method(D_METHOD("get_root_bone", "index"), &LimitAngularVelocityModifier3D::get_root_bone); + + ClassDB::bind_method(D_METHOD("set_end_bone_name", "index", "bone_name"), &LimitAngularVelocityModifier3D::set_end_bone_name); + ClassDB::bind_method(D_METHOD("get_end_bone_name", "index"), &LimitAngularVelocityModifier3D::get_end_bone_name); + ClassDB::bind_method(D_METHOD("set_end_bone", "index", "bone"), &LimitAngularVelocityModifier3D::set_end_bone); + ClassDB::bind_method(D_METHOD("get_end_bone", "index"), &LimitAngularVelocityModifier3D::get_end_bone); + + ClassDB::bind_method(D_METHOD("set_chain_count", "count"), &LimitAngularVelocityModifier3D::set_chain_count); + ClassDB::bind_method(D_METHOD("get_chain_count"), &LimitAngularVelocityModifier3D::get_chain_count); + ClassDB::bind_method(D_METHOD("clear_chains"), &LimitAngularVelocityModifier3D::clear_chains); + + ClassDB::bind_method(D_METHOD("set_max_angular_velocity", "angular_velocity"), &LimitAngularVelocityModifier3D::set_max_angular_velocity); + ClassDB::bind_method(D_METHOD("get_max_angular_velocity"), &LimitAngularVelocityModifier3D::get_max_angular_velocity); + ClassDB::bind_method(D_METHOD("set_exclude", "exclude"), &LimitAngularVelocityModifier3D::set_exclude); + ClassDB::bind_method(D_METHOD("is_exclude"), &LimitAngularVelocityModifier3D::is_exclude); + + ClassDB::bind_method(D_METHOD("reset"), &LimitAngularVelocityModifier3D::reset); + + ClassDB::bind_method(D_METHOD("_get_joint_count"), &LimitAngularVelocityModifier3D::_get_joint_count); + + ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "max_angular_velocity", PROPERTY_HINT_RANGE, "0,720,or_greater,radians_as_degrees,suffix:" + String(U"°") + "/s"), "set_max_angular_velocity", "get_max_angular_velocity"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "exclude"), "set_exclude", "is_exclude"); + ADD_ARRAY_COUNT("Chains", "chain_count", "set_chain_count", "get_chain_count", "chains/"); + ADD_ARRAY_COUNT("Joints", "joint_count", "", "_get_joint_count", "joints/"); +} + +void LimitAngularVelocityModifier3D::_set_active(bool p_active) { + if (p_active) { + reset(); + } +} + +void LimitAngularVelocityModifier3D::_skeleton_changed(Skeleton3D *p_old, Skeleton3D *p_new) { + _make_joints_dirty(); +} + +void LimitAngularVelocityModifier3D::_validate_bone_names() { + for (uint32_t i = 0; i < chains.size(); i++) { + // Prior bone name. + if (!chains[i].root_bone.name.is_empty()) { + set_root_bone_name(i, chains[i].root_bone.name); + } else if (chains[i].root_bone.bone != -1) { + set_root_bone(i, chains[i].root_bone.bone); + } + // Prior bone name. + if (!chains[i].end_bone.name.is_empty()) { + set_end_bone_name(i, chains[i].end_bone.name); + } else if (chains[i].end_bone.bone != -1) { + set_end_bone(i, chains[i].end_bone.bone); + } + } +} + +void LimitAngularVelocityModifier3D::_make_joints_dirty() { + if (joints_dirty) { + return; + } + joints_dirty = true; + callable_mp(this, &LimitAngularVelocityModifier3D::_update_joints).call_deferred(); +} + +void LimitAngularVelocityModifier3D::_update_joints() { + joints.clear(); + bones.clear(); + + Skeleton3D *sk = get_skeleton(); + if (!sk) { + joints_dirty = false; + return; + } + + LocalVector tmp_joints; + for (uint32_t i = 0; i < chains.size(); i++) { + tmp_joints.clear(); + Chain cn = chains[i]; + int current_bone = cn.end_bone.bone; + int root_bone = cn.root_bone.bone; + if (current_bone < 0 || root_bone < 0) { + continue; + } + // Validation. + bool valid = false; + while (current_bone >= 0) { + if (current_bone == root_bone) { + valid = true; + break; + } + current_bone = sk->get_bone_parent(current_bone); + } + if (!valid) { + ERR_FAIL_EDMSG("Chains[" + itos(i) + "]: End bone must be the same as or a child of root bone."); + continue; + } + current_bone = cn.end_bone.bone; + while (current_bone != root_bone) { + tmp_joints.push_back(current_bone); + current_bone = sk->get_bone_parent(current_bone); + } + tmp_joints.push_back(current_bone); + for (uint32_t j = 0; j < tmp_joints.size(); j++) { + int bn = tmp_joints[j]; + if (!joints.has(bn)) { + joints.insert(bn, sk->get_bone_name(bn)); + } + } + } + + if (exclude) { + for (int b = 0; b < sk->get_bone_count(); b++) { + if (joints.has(b)) { + continue; + } + BoneRot br; + br.first = b; + br.second = sk->get_bone_pose_rotation(b); + bones.push_back(br); + } + } else { + for (const KeyValue &E : joints) { + BoneRot br; + br.first = E.key; + br.second = sk->get_bone_pose_rotation(E.key); + bones.push_back(br); + } + } + + joints_dirty = false; + reset(); +} + +void LimitAngularVelocityModifier3D::_process_modification(double p_delta) { + Skeleton3D *skeleton = get_skeleton(); + if (!skeleton) { + return; + } + + if (init_needed) { + // Note: + // The pose retrieval within `_update_joints()` is done outside the skeleton's update process, + // so it ignores the pose resulting from the previous modifier's modification. + // This causes unintended initialization when `active` is set to true, so it must be initialized here. + for (uint32_t i = 0; i < bones.size(); i++) { + bones[i].second = skeleton->get_bone_pose_rotation(bones[i].first); + } + init_needed = false; + } + + double limit_in_frame = max_angular_velocity * p_delta; + for (uint32_t i = 0; i < bones.size(); i++) { + int bn = bones[i].first; + Quaternion dest = skeleton->get_bone_pose_rotation(bn); + double diff = bones[i].second.angle_to(dest); + if (!Math::is_zero_approx(diff)) { + bones[i].second = bones[i].second.slerp(dest, MIN(1.0, limit_in_frame / diff)); + } + skeleton->set_bone_pose_rotation(bn, bones[i].second); + } +} + +void LimitAngularVelocityModifier3D::reset() { + init_needed = true; +} + +LimitAngularVelocityModifier3D::~LimitAngularVelocityModifier3D() { + clear_chains(); +} diff --git a/scene/3d/limit_angular_velocity_modifier_3d.h b/scene/3d/limit_angular_velocity_modifier_3d.h new file mode 100644 index 00000000000..21c0ee35186 --- /dev/null +++ b/scene/3d/limit_angular_velocity_modifier_3d.h @@ -0,0 +1,110 @@ +/**************************************************************************/ +/* limit_angular_velocity_modifier_3d.h */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#pragma once + +#include "scene/3d/skeleton_modifier_3d.h" + +class LimitAngularVelocityModifier3D : public SkeletonModifier3D { + GDCLASS(LimitAngularVelocityModifier3D, SkeletonModifier3D); + +public: + struct BoneJoint { + StringName name; + int bone = -1; + }; + + struct Chain { + BoneJoint root_bone; + BoneJoint end_bone; + }; + + typedef Pair BoneRot; + +private: + bool exclude = false; + double max_angular_velocity = Math::TAU; + + LocalVector chains; + RBMap joints; + LocalVector bones; + + bool joints_dirty = false; + bool init_needed = true; + +protected: + bool _get(const StringName &p_path, Variant &r_ret) const; + bool _set(const StringName &p_path, const Variant &p_value); + void _get_property_list(List *p_list) const; + void _validate_property(PropertyInfo &p_property) const; + + static void _bind_methods(); + + void _notification(int p_what); + + virtual void _set_active(bool p_active) override; + virtual void _skeleton_changed(Skeleton3D *p_old, Skeleton3D *p_new) override; + + virtual void _validate_bone_names() override; + + void _make_joints_dirty(); + void _update_joints(); + + // For editor. + String _get_joint_bone_name(int p_bone) const; + int _get_joint_count() const; + + virtual void _process_modification(double p_delta) override; + +public: + void set_root_bone_name(int p_index, const String &p_bone_name); + String get_root_bone_name(int p_index) const; + void set_root_bone(int p_index, int p_bone); + int get_root_bone(int p_index) const; + + void set_end_bone_name(int p_index, const String &p_bone_name); + String get_end_bone_name(int p_index) const; + void set_end_bone(int p_index, int p_bone); + int get_end_bone(int p_index) const; + + void set_chain_count(int p_count); + int get_chain_count() const; + void clear_chains(); + + void set_max_angular_velocity(double p_angular_velocity); + double get_max_angular_velocity() const; + + void set_exclude(bool p_exclude); + bool is_exclude() const; + + void reset(); + + ~LimitAngularVelocityModifier3D(); +}; diff --git a/scene/register_scene_types.cpp b/scene/register_scene_types.cpp index 57135e03483..68452b78868 100644 --- a/scene/register_scene_types.cpp +++ b/scene/register_scene_types.cpp @@ -239,6 +239,7 @@ #include "scene/3d/light_3d.h" #include "scene/3d/lightmap_gi.h" #include "scene/3d/lightmap_probe.h" +#include "scene/3d/limit_angular_velocity_modifier_3d.h" #include "scene/3d/look_at_modifier_3d.h" #include "scene/3d/marker_3d.h" #include "scene/3d/mesh_instance_3d.h" @@ -682,6 +683,7 @@ void register_scene_types() { GDREGISTER_CLASS(FABRIK3D); GDREGISTER_CLASS(CCDIK3D); GDREGISTER_CLASS(JacobianIK3D); + GDREGISTER_CLASS(LimitAngularVelocityModifier3D); #ifndef XR_DISABLED GDREGISTER_CLASS(XRCamera3D);