diff --git a/doc/classes/LookAtModifier3D.xml b/doc/classes/LookAtModifier3D.xml
new file mode 100644
index 00000000000..e85da06c3af
--- /dev/null
+++ b/doc/classes/LookAtModifier3D.xml
@@ -0,0 +1,156 @@
+
+
+
+ The [SkeletonModifier3D] rotates a bone to look a target.
+
+
+ This [SkeletonModifier3D] rotates a bone to look a target. This is helpful for moving character's head to look at the player, rotating a turret to look at a target, or any other case where you want to make a bone rotate towards something quickly and easily.
+ When applying multiple [LookAtModifier3D]s, the [LookAtModifier3D] assigned to the parent bone must be put above the [LookAtModifier3D] assigned to the child bone in the list in order for the child bone results to be correct.
+
+
+
+
+
+
+
+ Returns the remaining seconds of the time-based interpolation.
+
+
+
+
+
+ Returns whether the time-based interpolation is running or not. If [code]true[/code], it is equivalent to [method get_interpolation_remaining] being [code]0[/code].
+ This is useful to determine whether a [LookAtModifier3D] can be removed safely.
+
+
+
+
+
+ Returns whether the target is within the angle limitations. It is useful for unsetting the [member target_node] when the target is outside of the angle limitations.
+ [b]Note:[/b] The value is updated after [method SkeletonModifier3D._process_modification]. To retrieve this value correctly, we recommend using the signal [signal SkeletonModifier3D.modification_processed].
+
+
+
+
+
+ The bone index of the [Skeleton3D] that the modification will operate on.
+
+
+ The duration of the time-based interpolation. Interpolation is triggered at the following cases:
+ - When the target node is changed
+ - When an axis is flipped due to angle limitation
+ [b]Note:[/b] The flipping occurs when the target is outside the angle limitation and the internally computed secondary rotation axis of the forward vector is flipped. Visually, it occurs when the target is outside the angle limitation and crosses the plane of the [member forward_axis] and [member primary_rotation_axis].
+
+
+ The ease type of the time-based interpolation. See also [enum Tween.EaseType].
+
+
+ The forward axis of the bone. This [SkeletonModifier3D] modifies the bone so that this axis points toward the [member target_node].
+
+
+ If [member origin_from] is [constant ORIGIN_FROM_SPECIFIC_BONE], the bone global pose position specified for this is used as origin.
+
+
+ If [member origin_from] is [constant ORIGIN_FROM_EXTERNAL_NODE], the global position of the [Node3D] specified for this is used as origin.
+
+
+ This value determines from what origin is retrieved for use in the calculation of the forward vector.
+
+
+ The offset of the bone pose origin. Matching the origins by offset is useful for cases where multiple bones must always face the same direction, such as the eyes.
+ [b]Note:[/b] This value indicates the local position of the object set in [member origin_from].
+
+
+ If the target passes through too close to the origin than this value, time-based interpolation is used even if the target is within the angular limitations, to prevent the angular velocity from becoming too high.
+
+
+ The threshold to start damping for [member primary_limit_angle]. It provides non-linear (b-spline) interpolation, let it feel more resistance the more it rotate to the edge limit. This is useful for simulating the limits of human motion.
+ If [code]1.0[/code], no damping is performed. If [code]0.0[/code], damping is always performed.
+
+
+ The limit angle of the primary rotation when [member symmetry_limitation] is [code]true[/code].
+
+
+ The threshold to start damping for [member primary_negative_limit_angle].
+
+
+ The limit angle of negative side of the primary rotation when [member symmetry_limitation] is [code]false[/code].
+
+
+ The threshold to start damping for [member primary_positive_limit_angle].
+
+
+ The limit angle of positive side of the primary rotation when [member symmetry_limitation] is [code]false[/code].
+
+
+ The axis of the first rotation. This [SkeletonModifier3D] works by compositing the rotation by Euler angles to prevent to rotate the [member forward_axis].
+
+
+ The threshold to start damping for [member secondary_limit_angle].
+
+
+ The limit angle of the secondary rotation when [member symmetry_limitation] is [code]true[/code].
+
+
+ The threshold to start damping for [member secondary_negative_limit_angle].
+
+
+ The limit angle of negative side of the secondary rotation when [member symmetry_limitation] is [code]false[/code].
+
+
+ The threshold to start damping for [member secondary_positive_limit_angle].
+
+
+ The limit angle of positive side of the secondary rotation when [member symmetry_limitation] is [code]false[/code].
+
+
+ If [code]true[/code], the limitations are spread from the bone symmetrically.
+ If [code]false[/code], the limitation can be specified separately for each side of the bone rest.
+
+
+ The [NodePath] to the node that is the target for the look at modification. This node is what the modification will rotate the bone to.
+
+
+ The transition type of the time-based interpolation. See also [enum Tween.TransitionType].
+
+
+ If [code]true[/code], limits the degree of rotation. This helps prevent the character's neck from rotating 360 degrees.
+ [b]Note:[/b] As with [AnimationTree] blending, interpolation is provided that favors [method Skeleton3D.get_bone_rest]. This means that interpolation does not select the shortest path in some cases.
+ [b]Note:[/b] Some [member transition_type] may exceed the limitations (e.g. `Back`, `Elastic`, and `Spring`). If interpolation occurs while overshooting the limitations, the result might possibly not respect the bone rest.
+
+
+ If [code]true[/code], provides rotation by two axes.
+
+
+
+
+ Enumerated value for the +X axis.
+
+
+ Enumerated value for the -X axis.
+
+
+ Enumerated value for the +Y axis.
+
+
+ Enumerated value for the -Y axis.
+
+
+ Enumerated value for the +Z axis.
+
+
+ Enumerated value for the -Z axis.
+
+
+ The bone rest position of the bone specified in [member bone] is used as origin.
+
+
+ The bone global pose position of the bone specified in [member origin_bone] is used as origin.
+ [b]Note:[/b] It is recommended that you select only the parent bone unless you are familiar with the bone processing process. The specified bone pose at the time the [LookAtModifier3D] is processed is used as a reference. In other words, if you specify a child bone and the [LookAtModifier3D] causes the child bone to move, the rendered result and direction will not match.
+
+
+ The global position of the [Node3D] specified in [member origin_external_node] is used as origin.
+ [b]Note:[/b] Same as [constant ORIGIN_FROM_SPECIFIC_BONE], when specifying a [BoneAttachment3D] with a child bone assigned, the rendered result and direction will not match.
+
+
+
diff --git a/editor/icons/LookAtModifier3D.svg b/editor/icons/LookAtModifier3D.svg
new file mode 100644
index 00000000000..9315b297ef7
--- /dev/null
+++ b/editor/icons/LookAtModifier3D.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/scene/3d/look_at_modifier_3d.cpp b/scene/3d/look_at_modifier_3d.cpp
new file mode 100644
index 00000000000..ad33cd420a0
--- /dev/null
+++ b/scene/3d/look_at_modifier_3d.cpp
@@ -0,0 +1,781 @@
+/**************************************************************************/
+/* look_at_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 "look_at_modifier_3d.h"
+
+void LookAtModifier3D::_validate_property(PropertyInfo &p_property) const {
+ SkeletonModifier3D::_validate_property(p_property);
+
+ if (p_property.name == "bone" || p_property.name == "origin_bone") {
+ Skeleton3D *skeleton = get_skeleton();
+ if (skeleton) {
+ p_property.hint = PROPERTY_HINT_ENUM;
+ p_property.hint_string = skeleton->get_concatenated_bone_names();
+ } else {
+ p_property.hint = PROPERTY_HINT_NONE;
+ p_property.hint_string = "";
+ }
+ }
+
+ if (origin_from == ORIGIN_FROM_SPECIFIC_BONE) {
+ if (p_property.name == "origin_external_node") {
+ p_property.usage = PROPERTY_USAGE_NONE;
+ }
+ } else if (origin_from == ORIGIN_FROM_EXTERNAL_NODE) {
+ if (p_property.name == "origin_bone") {
+ p_property.usage = PROPERTY_USAGE_NONE;
+ }
+ } else {
+ if (p_property.name == "origin_external_node" || p_property.name == "origin_bone") {
+ p_property.usage = PROPERTY_USAGE_NONE;
+ }
+ }
+
+ if ((!use_angle_limitation &&
+ (p_property.name == "symmetry_limitation" || p_property.name.ends_with("limit_angle") || p_property.name.ends_with("damp_threshold"))) ||
+ (!use_secondary_rotation && p_property.name.begins_with("secondary_")) ||
+ (!symmetry_limitation && (p_property.name == "primary_limit_angle" || p_property.name == "primary_damp_threshold" || p_property.name == "secondary_limit_angle" || p_property.name == "secondary_damp_threshold")) ||
+ (symmetry_limitation && (p_property.name.begins_with("primary_positive") || p_property.name.begins_with("primary_negative") || p_property.name.begins_with("secondary_positive") || (p_property.name.begins_with("secondary_negative"))))) {
+ p_property.usage = PROPERTY_USAGE_NONE;
+ }
+}
+
+PackedStringArray LookAtModifier3D::get_configuration_warnings() const {
+ PackedStringArray warnings = SkeletonModifier3D::get_configuration_warnings();
+ if (get_axis_from_bone_axis(forward_axis) == primary_rotation_axis) {
+ warnings.push_back(RTR("Forward axis and primary rotation axis must not be parallel."));
+ }
+ return warnings;
+}
+
+void LookAtModifier3D::set_bone(int p_bone) {
+ bone = p_bone;
+}
+
+int LookAtModifier3D::get_bone() const {
+ return bone;
+}
+
+void LookAtModifier3D::set_forward_axis(BoneAxis p_axis) {
+ forward_axis = p_axis;
+ update_configuration_warnings();
+}
+
+LookAtModifier3D::BoneAxis LookAtModifier3D::get_forward_axis() const {
+ return forward_axis;
+}
+
+void LookAtModifier3D::set_primary_rotation_axis(Vector3::Axis p_axis) {
+ primary_rotation_axis = p_axis;
+ update_configuration_warnings();
+}
+
+Vector3::Axis LookAtModifier3D::get_primary_rotation_axis() const {
+ return primary_rotation_axis;
+}
+
+void LookAtModifier3D::set_use_secondary_rotation(bool p_enabled) {
+ use_secondary_rotation = p_enabled;
+ notify_property_list_changed();
+}
+
+bool LookAtModifier3D::is_using_secondary_rotation() const {
+ return use_secondary_rotation;
+}
+
+void LookAtModifier3D::set_target_node(const NodePath &p_target_node) {
+ if (target_node != p_target_node) {
+ init_transition();
+ }
+ target_node = p_target_node;
+}
+
+NodePath LookAtModifier3D::get_target_node() const {
+ return target_node;
+}
+
+// For origin settings.
+
+void LookAtModifier3D::set_origin_from(OriginFrom p_origin_from) {
+ origin_from = p_origin_from;
+ notify_property_list_changed();
+}
+
+LookAtModifier3D::OriginFrom LookAtModifier3D::get_origin_from() const {
+ return origin_from;
+}
+
+void LookAtModifier3D::set_origin_bone(int p_bone) {
+ origin_bone = p_bone;
+}
+
+int LookAtModifier3D::get_origin_bone() const {
+ return origin_bone;
+}
+
+void LookAtModifier3D::set_origin_external_node(const NodePath &p_external_node) {
+ origin_external_node = p_external_node;
+}
+
+NodePath LookAtModifier3D::get_origin_external_node() const {
+ return origin_external_node;
+}
+
+void LookAtModifier3D::set_origin_offset(const Vector3 &p_offset) {
+ origin_offset = p_offset;
+}
+
+Vector3 LookAtModifier3D::get_origin_offset() const {
+ return origin_offset;
+}
+
+void LookAtModifier3D::set_origin_safe_margin(float p_margin) {
+ origin_safe_margin = p_margin;
+}
+
+float LookAtModifier3D::get_origin_safe_margin() const {
+ return origin_safe_margin;
+}
+
+// For time-based interpolation.
+
+void LookAtModifier3D::set_duration(float p_duration) {
+ duration = p_duration;
+ if (Math::is_zero_approx(p_duration)) {
+ time_step = 0;
+ remaining = 0;
+ } else {
+ time_step = 1.0 / p_duration; // Cache to avoid division.
+ }
+}
+
+float LookAtModifier3D::get_duration() const {
+ return duration;
+}
+
+void LookAtModifier3D::set_transition_type(Tween::TransitionType p_transition_type) {
+ transition_type = p_transition_type;
+}
+
+Tween::TransitionType LookAtModifier3D::get_transition_type() const {
+ return transition_type;
+}
+
+void LookAtModifier3D::set_ease_type(Tween::EaseType p_ease_type) {
+ ease_type = p_ease_type;
+}
+
+Tween::EaseType LookAtModifier3D::get_ease_type() const {
+ return ease_type;
+}
+
+// For angle limitation.
+
+void LookAtModifier3D::set_use_angle_limitation(bool p_enabled) {
+ use_angle_limitation = p_enabled;
+ notify_property_list_changed();
+}
+
+bool LookAtModifier3D::is_using_angle_limitation() const {
+ return use_angle_limitation;
+}
+
+void LookAtModifier3D::set_symmetry_limitation(bool p_enabled) {
+ symmetry_limitation = p_enabled;
+ notify_property_list_changed();
+}
+
+bool LookAtModifier3D::is_limitation_symmetry() const {
+ return symmetry_limitation;
+}
+
+void LookAtModifier3D::set_primary_limit_angle(float p_angle) {
+ primary_limit_angle = p_angle;
+}
+
+float LookAtModifier3D::get_primary_limit_angle() const {
+ return primary_limit_angle;
+}
+
+void LookAtModifier3D::set_primary_damp_threshold(float p_power) {
+ primary_damp_threshold = p_power;
+}
+
+float LookAtModifier3D::get_primary_damp_threshold() const {
+ return primary_damp_threshold;
+}
+
+void LookAtModifier3D::set_primary_positive_limit_angle(float p_angle) {
+ primary_positive_limit_angle = p_angle;
+}
+
+float LookAtModifier3D::get_primary_positive_limit_angle() const {
+ return primary_positive_limit_angle;
+}
+
+void LookAtModifier3D::set_primary_positive_damp_threshold(float p_power) {
+ primary_positive_damp_threshold = p_power;
+}
+
+float LookAtModifier3D::get_primary_positive_damp_threshold() const {
+ return primary_positive_damp_threshold;
+}
+
+void LookAtModifier3D::set_primary_negative_limit_angle(float p_angle) {
+ primary_negative_limit_angle = p_angle;
+}
+
+float LookAtModifier3D::get_primary_negative_limit_angle() const {
+ return primary_negative_limit_angle;
+}
+
+void LookAtModifier3D::set_primary_negative_damp_threshold(float p_power) {
+ primary_negative_damp_threshold = p_power;
+}
+
+float LookAtModifier3D::get_primary_negative_damp_threshold() const {
+ return primary_negative_damp_threshold;
+}
+
+void LookAtModifier3D::set_secondary_limit_angle(float p_angle) {
+ secondary_limit_angle = p_angle;
+}
+
+float LookAtModifier3D::get_secondary_limit_angle() const {
+ return secondary_limit_angle;
+}
+
+void LookAtModifier3D::set_secondary_damp_threshold(float p_power) {
+ secondary_damp_threshold = p_power;
+}
+
+float LookAtModifier3D::get_secondary_damp_threshold() const {
+ return secondary_damp_threshold;
+}
+
+void LookAtModifier3D::set_secondary_positive_limit_angle(float p_angle) {
+ secondary_positive_limit_angle = p_angle;
+}
+
+float LookAtModifier3D::get_secondary_positive_limit_angle() const {
+ return secondary_positive_limit_angle;
+}
+
+void LookAtModifier3D::set_secondary_positive_damp_threshold(float p_power) {
+ secondary_positive_damp_threshold = p_power;
+}
+
+float LookAtModifier3D::get_secondary_positive_damp_threshold() const {
+ return secondary_positive_damp_threshold;
+}
+
+void LookAtModifier3D::set_secondary_negative_limit_angle(float p_angle) {
+ secondary_negative_limit_angle = p_angle;
+}
+
+float LookAtModifier3D::get_secondary_negative_limit_angle() const {
+ return secondary_negative_limit_angle;
+}
+
+void LookAtModifier3D::set_secondary_negative_damp_threshold(float p_power) {
+ secondary_negative_damp_threshold = p_power;
+}
+
+float LookAtModifier3D::get_secondary_negative_damp_threshold() const {
+ return secondary_negative_damp_threshold;
+}
+
+bool LookAtModifier3D::is_target_within_limitation() const {
+ return is_within_limitations;
+}
+
+float LookAtModifier3D::get_interpolation_remaining() const {
+ return remaining * duration;
+}
+
+bool LookAtModifier3D::is_interpolating() const {
+ return Math::is_zero_approx(remaining);
+}
+
+// General API.
+
+void LookAtModifier3D::_bind_methods() {
+ ClassDB::bind_method(D_METHOD("set_target_node", "target_node"), &LookAtModifier3D::set_target_node);
+ ClassDB::bind_method(D_METHOD("get_target_node"), &LookAtModifier3D::get_target_node);
+
+ ClassDB::bind_method(D_METHOD("set_bone", "bone"), &LookAtModifier3D::set_bone);
+ ClassDB::bind_method(D_METHOD("get_bone"), &LookAtModifier3D::get_bone);
+ ClassDB::bind_method(D_METHOD("set_forward_axis", "forward_axis"), &LookAtModifier3D::set_forward_axis);
+ ClassDB::bind_method(D_METHOD("get_forward_axis"), &LookAtModifier3D::get_forward_axis);
+ ClassDB::bind_method(D_METHOD("set_primary_rotation_axis", "axis"), &LookAtModifier3D::set_primary_rotation_axis);
+ ClassDB::bind_method(D_METHOD("get_primary_rotation_axis"), &LookAtModifier3D::get_primary_rotation_axis);
+ ClassDB::bind_method(D_METHOD("set_use_secondary_rotation", "enabled"), &LookAtModifier3D::set_use_secondary_rotation);
+ ClassDB::bind_method(D_METHOD("is_using_secondary_rotation"), &LookAtModifier3D::is_using_secondary_rotation);
+ ClassDB::bind_method(D_METHOD("set_origin_safe_margin", "margin"), &LookAtModifier3D::set_origin_safe_margin);
+ ClassDB::bind_method(D_METHOD("get_origin_safe_margin"), &LookAtModifier3D::get_origin_safe_margin);
+
+ ClassDB::bind_method(D_METHOD("set_origin_from", "origin_from"), &LookAtModifier3D::set_origin_from);
+ ClassDB::bind_method(D_METHOD("get_origin_from"), &LookAtModifier3D::get_origin_from);
+ ClassDB::bind_method(D_METHOD("set_origin_bone", "bone"), &LookAtModifier3D::set_origin_bone);
+ ClassDB::bind_method(D_METHOD("get_origin_bone"), &LookAtModifier3D::get_origin_bone);
+ ClassDB::bind_method(D_METHOD("set_origin_external_node", "external_node"), &LookAtModifier3D::set_origin_external_node);
+ ClassDB::bind_method(D_METHOD("get_origin_external_node"), &LookAtModifier3D::get_origin_external_node);
+
+ ClassDB::bind_method(D_METHOD("set_origin_offset", "offset"), &LookAtModifier3D::set_origin_offset);
+ ClassDB::bind_method(D_METHOD("get_origin_offset"), &LookAtModifier3D::get_origin_offset);
+
+ ClassDB::bind_method(D_METHOD("set_duration", "duration"), &LookAtModifier3D::set_duration);
+ ClassDB::bind_method(D_METHOD("get_duration"), &LookAtModifier3D::get_duration);
+ ClassDB::bind_method(D_METHOD("set_transition_type", "transition_type"), &LookAtModifier3D::set_transition_type);
+ ClassDB::bind_method(D_METHOD("get_transition_type"), &LookAtModifier3D::get_transition_type);
+ ClassDB::bind_method(D_METHOD("set_ease_type", "ease_type"), &LookAtModifier3D::set_ease_type);
+ ClassDB::bind_method(D_METHOD("get_ease_type"), &LookAtModifier3D::get_ease_type);
+
+ ClassDB::bind_method(D_METHOD("set_use_angle_limitation", "enabled"), &LookAtModifier3D::set_use_angle_limitation);
+ ClassDB::bind_method(D_METHOD("is_using_angle_limitation"), &LookAtModifier3D::is_using_angle_limitation);
+ ClassDB::bind_method(D_METHOD("set_symmetry_limitation", "enabled"), &LookAtModifier3D::set_symmetry_limitation);
+ ClassDB::bind_method(D_METHOD("is_limitation_symmetry"), &LookAtModifier3D::is_limitation_symmetry);
+
+ ClassDB::bind_method(D_METHOD("set_primary_limit_angle", "angle"), &LookAtModifier3D::set_primary_limit_angle);
+ ClassDB::bind_method(D_METHOD("get_primary_limit_angle"), &LookAtModifier3D::get_primary_limit_angle);
+ ClassDB::bind_method(D_METHOD("set_primary_damp_threshold", "power"), &LookAtModifier3D::set_primary_damp_threshold);
+ ClassDB::bind_method(D_METHOD("get_primary_damp_threshold"), &LookAtModifier3D::get_primary_damp_threshold);
+
+ ClassDB::bind_method(D_METHOD("set_primary_positive_limit_angle", "angle"), &LookAtModifier3D::set_primary_positive_limit_angle);
+ ClassDB::bind_method(D_METHOD("get_primary_positive_limit_angle"), &LookAtModifier3D::get_primary_positive_limit_angle);
+ ClassDB::bind_method(D_METHOD("set_primary_positive_damp_threshold", "power"), &LookAtModifier3D::set_primary_positive_damp_threshold);
+ ClassDB::bind_method(D_METHOD("get_primary_positive_damp_threshold"), &LookAtModifier3D::get_primary_positive_damp_threshold);
+ ClassDB::bind_method(D_METHOD("set_primary_negative_limit_angle", "angle"), &LookAtModifier3D::set_primary_negative_limit_angle);
+ ClassDB::bind_method(D_METHOD("get_primary_negative_limit_angle"), &LookAtModifier3D::get_primary_negative_limit_angle);
+ ClassDB::bind_method(D_METHOD("set_primary_negative_damp_threshold", "power"), &LookAtModifier3D::set_primary_negative_damp_threshold);
+ ClassDB::bind_method(D_METHOD("get_primary_negative_damp_threshold"), &LookAtModifier3D::get_primary_negative_damp_threshold);
+
+ ClassDB::bind_method(D_METHOD("set_secondary_limit_angle", "angle"), &LookAtModifier3D::set_secondary_limit_angle);
+ ClassDB::bind_method(D_METHOD("get_secondary_limit_angle"), &LookAtModifier3D::get_secondary_limit_angle);
+ ClassDB::bind_method(D_METHOD("set_secondary_damp_threshold", "power"), &LookAtModifier3D::set_secondary_damp_threshold);
+ ClassDB::bind_method(D_METHOD("get_secondary_damp_threshold"), &LookAtModifier3D::get_secondary_damp_threshold);
+
+ ClassDB::bind_method(D_METHOD("set_secondary_positive_limit_angle", "angle"), &LookAtModifier3D::set_secondary_positive_limit_angle);
+ ClassDB::bind_method(D_METHOD("get_secondary_positive_limit_angle"), &LookAtModifier3D::get_secondary_positive_limit_angle);
+ ClassDB::bind_method(D_METHOD("set_secondary_positive_damp_threshold", "power"), &LookAtModifier3D::set_secondary_positive_damp_threshold);
+ ClassDB::bind_method(D_METHOD("get_secondary_positive_damp_threshold"), &LookAtModifier3D::get_secondary_positive_damp_threshold);
+ ClassDB::bind_method(D_METHOD("set_secondary_negative_limit_angle", "angle"), &LookAtModifier3D::set_secondary_negative_limit_angle);
+ ClassDB::bind_method(D_METHOD("get_secondary_negative_limit_angle"), &LookAtModifier3D::get_secondary_negative_limit_angle);
+ ClassDB::bind_method(D_METHOD("set_secondary_negative_damp_threshold", "power"), &LookAtModifier3D::set_secondary_negative_damp_threshold);
+ ClassDB::bind_method(D_METHOD("get_secondary_negative_damp_threshold"), &LookAtModifier3D::get_secondary_negative_damp_threshold);
+
+ ClassDB::bind_method(D_METHOD("get_interpolation_remaining"), &LookAtModifier3D::get_interpolation_remaining);
+ ClassDB::bind_method(D_METHOD("is_interpolating"), &LookAtModifier3D::is_interpolating);
+ ClassDB::bind_method(D_METHOD("is_target_within_limitation"), &LookAtModifier3D::is_target_within_limitation);
+
+ ADD_PROPERTY(PropertyInfo(Variant::NODE_PATH, "target_node", PROPERTY_HINT_NODE_TYPE, "Node3D"), "set_target_node", "get_target_node");
+
+ ADD_PROPERTY(PropertyInfo(Variant::INT, "bone", PROPERTY_HINT_ENUM, ""), "set_bone", "get_bone");
+ ADD_PROPERTY(PropertyInfo(Variant::INT, "forward_axis", PROPERTY_HINT_ENUM, "+X,-X,+Y,-Y,+Z,-Z"), "set_forward_axis", "get_forward_axis");
+ ADD_PROPERTY(PropertyInfo(Variant::INT, "primary_rotation_axis", PROPERTY_HINT_ENUM, "X,Y,Z"), "set_primary_rotation_axis", "get_primary_rotation_axis");
+ ADD_PROPERTY(PropertyInfo(Variant::BOOL, "use_secondary_rotation"), "set_use_secondary_rotation", "is_using_secondary_rotation");
+
+ ADD_GROUP("Origin Settings", "origin_");
+ ADD_PROPERTY(PropertyInfo(Variant::INT, "origin_from", PROPERTY_HINT_ENUM, "Self,SpecificBone,ExternalNode"), "set_origin_from", "get_origin_from");
+ ADD_PROPERTY(PropertyInfo(Variant::INT, "origin_bone", PROPERTY_HINT_ENUM, ""), "set_origin_bone", "get_origin_bone");
+ ADD_PROPERTY(PropertyInfo(Variant::NODE_PATH, "origin_external_node", PROPERTY_HINT_NODE_TYPE, "Node3D"), "set_origin_external_node", "get_origin_external_node");
+ ADD_PROPERTY(PropertyInfo(Variant::VECTOR3, "origin_offset"), "set_origin_offset", "get_origin_offset");
+ ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "origin_safe_margin", PROPERTY_HINT_RANGE, "0,100,0.001,or_greater,suffix:m"), "set_origin_safe_margin", "get_origin_safe_margin");
+
+ ADD_GROUP("Time Based Interpolation", "");
+ ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "duration", PROPERTY_HINT_RANGE, "0,10,0.001,or_greater,suffix:s"), "set_duration", "get_duration");
+ ADD_PROPERTY(PropertyInfo(Variant::INT, "transition_type", PROPERTY_HINT_ENUM, "Linear,Sine,Quint,Quart,Quad,Expo,Elastic,Cubic,Circ,Bounce,Back,Spring"), "set_transition_type", "get_transition_type");
+ ADD_PROPERTY(PropertyInfo(Variant::INT, "ease_type", PROPERTY_HINT_ENUM, "In,Out,InOut,OutIn"), "set_ease_type", "get_ease_type");
+
+ ADD_GROUP("Angle Limitation", "");
+ ADD_PROPERTY(PropertyInfo(Variant::BOOL, "use_angle_limitation"), "set_use_angle_limitation", "is_using_angle_limitation");
+ ADD_PROPERTY(PropertyInfo(Variant::BOOL, "symmetry_limitation"), "set_symmetry_limitation", "is_limitation_symmetry");
+
+ ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "primary_limit_angle", PROPERTY_HINT_RANGE, "0,360,0.01,radians_as_degrees"), "set_primary_limit_angle", "get_primary_limit_angle");
+ ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "primary_damp_threshold", PROPERTY_HINT_RANGE, "0,1,0.01"), "set_primary_damp_threshold", "get_primary_damp_threshold");
+
+ ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "primary_positive_limit_angle", PROPERTY_HINT_RANGE, "0,180,0.01,radians_as_degrees"), "set_primary_positive_limit_angle", "get_primary_positive_limit_angle");
+ ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "primary_positive_damp_threshold", PROPERTY_HINT_RANGE, "0,1,0.01"), "set_primary_positive_damp_threshold", "get_primary_positive_damp_threshold");
+ ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "primary_negative_limit_angle", PROPERTY_HINT_RANGE, "0,180,0.01,radians_as_degrees"), "set_primary_negative_limit_angle", "get_primary_negative_limit_angle");
+ ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "primary_negative_damp_threshold", PROPERTY_HINT_RANGE, "0,1,0.01"), "set_primary_negative_damp_threshold", "get_primary_negative_damp_threshold");
+
+ ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "secondary_limit_angle", PROPERTY_HINT_RANGE, "0,360,0.01,radians_as_degrees"), "set_secondary_limit_angle", "get_secondary_limit_angle");
+ ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "secondary_damp_threshold", PROPERTY_HINT_RANGE, "0,1,0.01"), "set_secondary_damp_threshold", "get_secondary_damp_threshold");
+
+ ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "secondary_positive_limit_angle", PROPERTY_HINT_RANGE, "0,180,0.01,radians_as_degrees"), "set_secondary_positive_limit_angle", "get_secondary_positive_limit_angle");
+ ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "secondary_positive_damp_threshold", PROPERTY_HINT_RANGE, "0,1,0.01"), "set_secondary_positive_damp_threshold", "get_secondary_positive_damp_threshold");
+ ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "secondary_negative_limit_angle", PROPERTY_HINT_RANGE, "0,180,0.01,radians_as_degrees"), "set_secondary_negative_limit_angle", "get_secondary_negative_limit_angle");
+ ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "secondary_negative_damp_threshold", PROPERTY_HINT_RANGE, "0,1,0.01"), "set_secondary_negative_damp_threshold", "get_secondary_negative_damp_threshold");
+
+ BIND_ENUM_CONSTANT(BONE_AXIS_PLUS_X);
+ BIND_ENUM_CONSTANT(BONE_AXIS_MINUS_X);
+ BIND_ENUM_CONSTANT(BONE_AXIS_PLUS_Y);
+ BIND_ENUM_CONSTANT(BONE_AXIS_MINUS_Y);
+ BIND_ENUM_CONSTANT(BONE_AXIS_PLUS_Z);
+ BIND_ENUM_CONSTANT(BONE_AXIS_MINUS_Z);
+
+ BIND_ENUM_CONSTANT(ORIGIN_FROM_SELF);
+ BIND_ENUM_CONSTANT(ORIGIN_FROM_SPECIFIC_BONE);
+ BIND_ENUM_CONSTANT(ORIGIN_FROM_EXTERNAL_NODE);
+}
+
+void LookAtModifier3D::_process_modification() {
+ if (!is_inside_tree()) {
+ return;
+ }
+
+ Skeleton3D *skeleton = get_skeleton();
+ if (!skeleton || bone < 0 || bone >= skeleton->get_bone_count()) {
+ return;
+ }
+
+ // Calculate bone rest space in the world.
+ Transform3D bone_rest_space;
+ int parent_bone = skeleton->get_bone_parent(bone);
+ if (parent_bone < 0) {
+ bone_rest_space = skeleton->get_global_transform() * skeleton->get_bone_rest(bone);
+ } else {
+ bone_rest_space = skeleton->get_global_transform() * skeleton->get_bone_global_pose(parent_bone) * skeleton->get_bone_rest(bone);
+ }
+
+ // Calculate forward_vector and destination.
+ is_within_limitations = true;
+ Vector3 prev_forward_vector = forward_vector;
+ Quaternion destination;
+ Node3D *target = Object::cast_to(get_node_or_null(target_node));
+ if (!target) {
+ destination = skeleton->get_bone_pose_rotation(bone);
+ } else {
+ Transform3D origin_tr;
+ if (origin_from == ORIGIN_FROM_SPECIFIC_BONE && origin_bone < skeleton->get_bone_count()) {
+ origin_tr = skeleton->get_global_transform() * skeleton->get_bone_global_pose(origin_bone);
+ } else if (origin_from == ORIGIN_FROM_EXTERNAL_NODE) {
+ Node3D *origin_src = Object::cast_to(get_node_or_null(origin_external_node));
+ if (origin_src) {
+ origin_tr = origin_src->get_global_transform();
+ } else {
+ origin_tr = bone_rest_space;
+ }
+ } else {
+ origin_tr = bone_rest_space;
+ }
+ forward_vector = bone_rest_space.basis.xform_inv((target->get_global_position() - origin_tr.translated_local(origin_offset).origin));
+ forward_vector_nrm = forward_vector.normalized();
+ if (forward_vector_nrm.abs().is_equal_approx(get_vector_from_axis(primary_rotation_axis))) {
+ destination = skeleton->get_bone_pose_rotation(bone);
+ forward_vector = Vector3(0, 0, 0); // The zero-vector to be used for checking in the line immediately below to avoid animation glitch.
+ } else {
+ destination = look_at_with_axes(skeleton->get_bone_rest(bone)).basis.get_rotation_quaternion();
+ }
+ }
+
+ // Detect flipping.
+ Vector3::Axis current_forward_axis = get_axis_from_bone_axis(forward_axis);
+ if (is_intersecting_axis(prev_forward_vector, forward_vector, current_forward_axis, secondary_rotation_axis) ||
+ is_intersecting_axis(prev_forward_vector, forward_vector, primary_rotation_axis, primary_rotation_axis, true) ||
+ is_intersecting_axis(prev_forward_vector, forward_vector, secondary_rotation_axis, current_forward_axis) ||
+ (prev_forward_vector != Vector3(0, 0, 0) && forward_vector == Vector3(0, 0, 0)) ||
+ (prev_forward_vector == Vector3(0, 0, 0) && forward_vector != Vector3(0, 0, 0))) {
+ init_transition();
+ } else if (use_angle_limitation && signbit(prev_forward_vector[secondary_rotation_axis]) != signbit(forward_vector[secondary_rotation_axis])) {
+ // Flipping by angle_limitation can be detected by sign of secondary rotation axes during forward_vector is rotated more than 90 degree from forward_axis (means dot production is negative).
+ Vector3 prev_forward_vector_nrm = forward_vector.normalized();
+ Vector3 rest_forward_vector = get_vector_from_bone_axis(forward_axis);
+ if (symmetry_limitation) {
+ if (!Math::is_equal_approx(primary_limit_angle, (float)Math_TAU) && prev_forward_vector_nrm.dot(rest_forward_vector) < 0 && forward_vector_nrm.dot(rest_forward_vector) < 0) {
+ init_transition();
+ }
+ } else {
+ if (!Math::is_equal_approx(primary_positive_limit_angle + primary_negative_limit_angle, (float)Math_TAU) && prev_forward_vector_nrm.dot(rest_forward_vector) < 0 && forward_vector_nrm.dot(rest_forward_vector) < 0) {
+ init_transition();
+ }
+ }
+ }
+
+ // Do time-based interpolation.
+ if (remaining > 0) {
+ double delta = 0.0;
+ if (skeleton->get_modifier_callback_mode_process() == Skeleton3D::MODIFIER_CALLBACK_MODE_PROCESS_IDLE) {
+ delta = get_process_delta_time();
+ } else {
+ delta = get_physics_process_delta_time();
+ }
+ remaining = MAX(0, remaining - time_step * delta);
+ if (use_angle_limitation) {
+ // Interpolate through the rest same as AnimationTree blending for preventing to penetrate the bone into the body.
+ Quaternion rest = skeleton->get_bone_rest(bone).basis.get_rotation_quaternion();
+ float weight = Tween::run_equation(transition_type, ease_type, 1 - remaining, 0.0, 1.0, 1.0);
+ destination = rest * Quaternion().slerp(rest.inverse() * from_q, 1 - weight) * Quaternion().slerp(rest.inverse() * destination, weight);
+ } else {
+ destination = from_q.slerp(destination, Tween::run_equation(transition_type, ease_type, 1 - remaining, 0.0, 1.0, 1.0));
+ }
+ }
+
+ skeleton->set_bone_pose_rotation(bone, destination);
+ prev_q = destination;
+}
+
+bool LookAtModifier3D::is_intersecting_axis(const Vector3 &p_prev, const Vector3 &p_current, Vector3::Axis p_flipping_axis, Vector3::Axis p_check_axis, bool p_check_plane) const {
+ // Prevent that the angular velocity does not become too large.
+ // Check that is p_flipping_axis flipped nearby p_check_axis (close than origin_safe_margin) or not. If p_check_plane is true, check two axes of crossed plane.
+ if (p_check_plane) {
+ if (get_projection_vector(p_prev, p_check_axis).length() > origin_safe_margin && get_projection_vector(p_current, p_check_axis).length() > origin_safe_margin) {
+ return false;
+ }
+ } else if (Math::abs(p_prev[p_check_axis]) > origin_safe_margin && Math::abs(p_current[p_check_axis]) > origin_safe_margin) {
+ return false;
+ }
+
+ return signbit(p_prev[p_flipping_axis]) != signbit(p_current[p_flipping_axis]);
+}
+
+Vector3 LookAtModifier3D::get_basis_vector_from_bone_axis(const Basis &p_basis, LookAtModifier3D::BoneAxis p_axis) const {
+ Vector3 ret;
+ switch (p_axis) {
+ case BONE_AXIS_PLUS_X: {
+ ret = p_basis.get_column(0);
+ } break;
+ case BONE_AXIS_MINUS_X: {
+ ret = -p_basis.get_column(0);
+ } break;
+ case BONE_AXIS_PLUS_Y: {
+ ret = p_basis.get_column(1);
+ } break;
+ case BONE_AXIS_MINUS_Y: {
+ ret = -p_basis.get_column(1);
+ } break;
+ case BONE_AXIS_PLUS_Z: {
+ ret = p_basis.get_column(2);
+ } break;
+ case BONE_AXIS_MINUS_Z: {
+ ret = -p_basis.get_column(2);
+ } break;
+ }
+ return ret;
+}
+
+Vector3 LookAtModifier3D::get_vector_from_bone_axis(const LookAtModifier3D::BoneAxis &p_axis) const {
+ Vector3 ret;
+ switch (p_axis) {
+ case BONE_AXIS_PLUS_X: {
+ ret = Vector3(1, 0, 0);
+ } break;
+ case BONE_AXIS_MINUS_X: {
+ ret = Vector3(-1, 0, 0);
+ } break;
+ case BONE_AXIS_PLUS_Y: {
+ ret = Vector3(0, 1, 0);
+ } break;
+ case BONE_AXIS_MINUS_Y: {
+ ret = Vector3(0, -1, 0);
+ } break;
+ case BONE_AXIS_PLUS_Z: {
+ ret = Vector3(0, 0, 1);
+ } break;
+ case BONE_AXIS_MINUS_Z: {
+ ret = Vector3(0, 0, -1);
+ } break;
+ }
+ return ret;
+}
+
+Vector3 LookAtModifier3D::get_vector_from_axis(const Vector3::Axis &p_axis) const {
+ Vector3 ret;
+ switch (p_axis) {
+ case Vector3::AXIS_X: {
+ ret = Vector3(1, 0, 0);
+ } break;
+ case Vector3::AXIS_Y: {
+ ret = Vector3(0, 1, 0);
+ } break;
+ case Vector3::AXIS_Z: {
+ ret = Vector3(0, 0, 1);
+ } break;
+ }
+ return ret;
+}
+
+Vector3::Axis LookAtModifier3D::get_axis_from_bone_axis(BoneAxis p_axis) const {
+ Vector3::Axis ret = Vector3::AXIS_X;
+ switch (p_axis) {
+ case BONE_AXIS_PLUS_X:
+ case BONE_AXIS_MINUS_X: {
+ ret = Vector3::AXIS_X;
+ } break;
+ case BONE_AXIS_PLUS_Y:
+ case BONE_AXIS_MINUS_Y: {
+ ret = Vector3::AXIS_Y;
+ } break;
+ case BONE_AXIS_PLUS_Z:
+ case BONE_AXIS_MINUS_Z: {
+ ret = Vector3::AXIS_Z;
+ } break;
+ }
+ return ret;
+}
+
+Vector2 LookAtModifier3D::get_projection_vector(const Vector3 &p_vector, Vector3::Axis p_axis) const {
+ // NOTE: axis is swapped between 2D and 3D.
+ Vector2 ret;
+ switch (p_axis) {
+ case Vector3::AXIS_X: {
+ ret = Vector2(p_vector.z, p_vector.y);
+ } break;
+ case Vector3::AXIS_Y: {
+ ret = Vector2(p_vector.x, p_vector.z);
+ } break;
+ case Vector3::AXIS_Z: {
+ ret = Vector2(p_vector.y, p_vector.x);
+ } break;
+ }
+ return ret;
+}
+
+float LookAtModifier3D::remap_damped(float p_from, float p_to, float p_damp_threshold, float p_value) const {
+ float sign = signbit(p_value) ? -1.0f : 1.0f;
+ float abs_value = Math::abs(p_value);
+
+ if (Math::is_equal_approx(p_damp_threshold, 1.0f) || Math::is_zero_approx(p_to)) {
+ return sign * CLAMP(abs_value, p_from, p_to); // Avoid division by zero.
+ }
+
+ float value = Math::inverse_lerp(p_from, p_to, abs_value);
+
+ if (value <= p_damp_threshold) {
+ return sign * CLAMP(abs_value, p_from, p_to);
+ }
+
+ double limit = Math_PI;
+ double inv_to = 1.0 / p_to;
+ double end_x = limit * inv_to;
+ double position = abs_value * inv_to;
+ Vector2 start = Vector2(p_damp_threshold, p_damp_threshold);
+ Vector2 mid = Vector2(1.0, 1.0);
+ Vector2 end = Vector2(end_x, 1.0);
+ value = get_bspline_y(start, mid, end, position);
+
+ return sign * Math::lerp(p_from, p_to, value);
+}
+
+double LookAtModifier3D::get_bspline_y(const Vector2 &p_from, const Vector2 &p_control, const Vector2 &p_to, double p_x) const {
+ double a = p_from.x - 2.0 * p_control.x + p_to.x;
+ double b = -2.0 * p_from.x + 2.0 * p_control.x;
+ double c = p_from.x - p_x;
+ double t = 0.0;
+ if (Math::is_zero_approx(a)) {
+ t = -c / b; // Almost linear.
+ } else {
+ double discriminant = b * b - 4.0 * a * c;
+ double sqrt_discriminant = Math::sqrt(discriminant);
+ double e = 1.0 / (2.0 * a);
+ double t1 = (-b + sqrt_discriminant) * e;
+ t = (0.0 <= t1 && t1 <= 1.0) ? t1 : (-b - sqrt_discriminant) * e;
+ }
+ double u = 1.0 - t;
+ double y = u * u * p_from.y + 2.0 * u * t * p_control.y + t * t * p_to.y;
+ return y;
+}
+
+Transform3D LookAtModifier3D::look_at_with_axes(const Transform3D &p_rest) {
+ // Primary rotation by projection to 2D plane by xform_inv and picking elements.
+ Vector3 current_vector = get_basis_vector_from_bone_axis(p_rest.basis, forward_axis).normalized();
+ Vector2 src_vec2 = get_projection_vector(p_rest.basis.xform_inv(forward_vector_nrm), primary_rotation_axis).normalized();
+ Vector2 dst_vec2 = get_projection_vector(p_rest.basis.xform_inv(current_vector), primary_rotation_axis).normalized();
+ real_t calculated_angle = src_vec2.angle_to(dst_vec2);
+ Transform3D primary_result = p_rest.rotated_local(get_vector_from_axis(primary_rotation_axis), calculated_angle);
+ Transform3D current_result = primary_result; // primary_result will be used by calculation of secondary rotation, current_result is rotated by that.
+ float limit_angle = 0.0;
+ float damp_threshold = 0.0;
+
+ if (use_angle_limitation) {
+ if (symmetry_limitation) {
+ limit_angle = primary_limit_angle * 0.5f;
+ damp_threshold = primary_damp_threshold;
+ } else {
+ if (signbit(calculated_angle)) {
+ limit_angle = primary_negative_limit_angle;
+ damp_threshold = primary_negative_damp_threshold;
+ } else {
+ limit_angle = primary_positive_limit_angle;
+ damp_threshold = primary_positive_damp_threshold;
+ }
+ }
+ if (Math::abs(calculated_angle) > limit_angle) {
+ is_within_limitations = false;
+ }
+ calculated_angle = remap_damped(0, limit_angle, damp_threshold, calculated_angle);
+ current_result = p_rest.rotated_local(get_vector_from_axis(primary_rotation_axis), calculated_angle);
+ }
+
+ // Needs for detecting flipping even if use_secondary_rotation is false.
+ Vector3 secondary_plane = get_vector_from_bone_axis(forward_axis) + get_vector_from_axis(primary_rotation_axis);
+ secondary_rotation_axis = Math::is_zero_approx(secondary_plane.x) ? Vector3::AXIS_X : (Math::is_zero_approx(secondary_plane.y) ? Vector3::AXIS_Y : Vector3::AXIS_Z);
+
+ if (!use_secondary_rotation) {
+ return current_result;
+ }
+
+ // Secondary rotation by projection to 2D plane by xform_inv and picking elements.
+ current_vector = get_basis_vector_from_bone_axis(primary_result.basis, forward_axis).normalized();
+ src_vec2 = get_projection_vector(primary_result.basis.xform_inv(forward_vector_nrm), secondary_rotation_axis).normalized();
+ dst_vec2 = get_projection_vector(primary_result.basis.xform_inv(current_vector), secondary_rotation_axis).normalized();
+ calculated_angle = src_vec2.angle_to(dst_vec2);
+
+ if (use_angle_limitation) {
+ if (symmetry_limitation) {
+ limit_angle = secondary_limit_angle * 0.5f;
+ damp_threshold = secondary_damp_threshold;
+ } else {
+ if (signbit(calculated_angle)) {
+ limit_angle = secondary_negative_limit_angle;
+ damp_threshold = secondary_negative_damp_threshold;
+ } else {
+ limit_angle = secondary_positive_limit_angle;
+ damp_threshold = secondary_positive_damp_threshold;
+ }
+ }
+ if (Math::abs(calculated_angle) > limit_angle) {
+ is_within_limitations = false;
+ }
+ calculated_angle = remap_damped(0, limit_angle, damp_threshold, calculated_angle);
+ }
+
+ current_result = current_result.rotated_local(get_vector_from_axis(secondary_rotation_axis), calculated_angle);
+
+ return current_result;
+}
+
+void LookAtModifier3D::init_transition() {
+ if (Math::is_zero_approx(duration)) {
+ return;
+ }
+ from_q = prev_q;
+ remaining = 1.0;
+}
diff --git a/scene/3d/look_at_modifier_3d.h b/scene/3d/look_at_modifier_3d.h
new file mode 100644
index 00000000000..5f3c4e8b1c2
--- /dev/null
+++ b/scene/3d/look_at_modifier_3d.h
@@ -0,0 +1,199 @@
+/**************************************************************************/
+/* look_at_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. */
+/**************************************************************************/
+
+#ifndef LOOK_AT_MODIFIER_3D_H
+#define LOOK_AT_MODIFIER_3D_H
+
+#include "scene/3d/skeleton_modifier_3d.h"
+#include "scene/animation/tween.h"
+
+class LookAtModifier3D : public SkeletonModifier3D {
+ GDCLASS(LookAtModifier3D, SkeletonModifier3D);
+
+public:
+ enum BoneAxis {
+ BONE_AXIS_PLUS_X,
+ BONE_AXIS_MINUS_X,
+ BONE_AXIS_PLUS_Y,
+ BONE_AXIS_MINUS_Y,
+ BONE_AXIS_PLUS_Z,
+ BONE_AXIS_MINUS_Z,
+ };
+
+ enum OriginFrom {
+ ORIGIN_FROM_SELF,
+ ORIGIN_FROM_SPECIFIC_BONE,
+ ORIGIN_FROM_EXTERNAL_NODE,
+ };
+
+private:
+ int bone = 0;
+
+ Vector3 forward_vector;
+ Vector3 forward_vector_nrm;
+ BoneAxis forward_axis = BONE_AXIS_PLUS_Z;
+ Vector3::Axis primary_rotation_axis = Vector3::AXIS_Y;
+ Vector3::Axis secondary_rotation_axis = Vector3::AXIS_X;
+ bool use_secondary_rotation = true;
+
+ OriginFrom origin_from = ORIGIN_FROM_SELF;
+ int origin_bone = -1;
+ NodePath origin_external_node;
+
+ Vector3 origin_offset;
+ float origin_safe_margin = 0.1;
+
+ NodePath target_node;
+
+ float duration = 0;
+ Tween::TransitionType transition_type = Tween::TRANS_LINEAR;
+ Tween::EaseType ease_type = Tween::EASE_IN;
+
+ bool use_angle_limitation = false;
+ bool symmetry_limitation = true;
+
+ float primary_limit_angle = Math_TAU;
+ float primary_damp_threshold = 1.0f;
+ float primary_positive_limit_angle = Math_PI;
+ float primary_positive_damp_threshold = 1.0f;
+ float primary_negative_limit_angle = Math_PI;
+ float primary_negative_damp_threshold = 1.0f;
+
+ float secondary_limit_angle = Math_TAU;
+ float secondary_damp_threshold = 1.0f;
+ float secondary_positive_limit_angle = Math_PI;
+ float secondary_positive_damp_threshold = 1.0f;
+ float secondary_negative_limit_angle = Math_PI;
+ float secondary_negative_damp_threshold = 1.0f;
+
+ bool is_within_limitations = false;
+
+ // For time-based interpolation.
+ Quaternion from_q;
+ Quaternion prev_q;
+
+ float remaining = 0;
+ float time_step = 1.0;
+
+ Vector3 get_basis_vector_from_bone_axis(const Basis &p_basis, BoneAxis p_axis) const;
+ Vector3 get_vector_from_bone_axis(const BoneAxis &p_axis) const;
+ Vector3 get_vector_from_axis(const Vector3::Axis &p_axis) const;
+ Vector3::Axis get_axis_from_bone_axis(BoneAxis p_axis) const;
+ Vector2 get_projection_vector(const Vector3 &p_vector, Vector3::Axis p_axis) const;
+ float remap_damped(float p_from, float p_to, float p_damp_threshold, float p_value) const;
+ double get_bspline_y(const Vector2 &p_from, const Vector2 &p_control, const Vector2 &p_to, double p_x) const;
+ bool is_intersecting_axis(const Vector3 &p_prev, const Vector3 &p_current, Vector3::Axis p_flipping_axis, Vector3::Axis p_check_axis, bool p_check_plane = false) const;
+
+ Transform3D look_at_with_axes(const Transform3D &p_rest);
+ void init_transition();
+
+protected:
+ virtual PackedStringArray get_configuration_warnings() const override;
+ void _validate_property(PropertyInfo &p_property) const;
+
+ static void _bind_methods();
+
+ virtual void _process_modification() override;
+
+public:
+ void set_bone(int p_bone);
+ int get_bone() const;
+
+ void set_forward_axis(BoneAxis p_axis);
+ BoneAxis get_forward_axis() const;
+ void set_primary_rotation_axis(Vector3::Axis p_axis);
+ Vector3::Axis get_primary_rotation_axis() const;
+ void set_use_secondary_rotation(bool p_enabled);
+ bool is_using_secondary_rotation() const;
+
+ void set_origin_from(OriginFrom p_origin_from);
+ OriginFrom get_origin_from() const;
+ void set_origin_bone(int p_bone);
+ int get_origin_bone() const;
+ void set_origin_external_node(const NodePath &p_external_node);
+ NodePath get_origin_external_node() const;
+
+ void set_origin_offset(const Vector3 &p_offset);
+ Vector3 get_origin_offset() const;
+ void set_origin_safe_margin(float p_margin);
+ float get_origin_safe_margin() const;
+
+ void set_target_node(const NodePath &p_target_node);
+ NodePath get_target_node() const;
+
+ void set_duration(float p_duration);
+ float get_duration() const;
+ void set_transition_type(Tween::TransitionType p_transition_type);
+ Tween::TransitionType get_transition_type() const;
+ void set_ease_type(Tween::EaseType p_ease_type);
+ Tween::EaseType get_ease_type() const;
+
+ void set_use_angle_limitation(bool p_enabled);
+ bool is_using_angle_limitation() const;
+ void set_symmetry_limitation(bool p_enabled);
+ bool is_limitation_symmetry() const;
+
+ void set_primary_limit_angle(float p_angle);
+ float get_primary_limit_angle() const;
+ void set_primary_damp_threshold(float p_power);
+ float get_primary_damp_threshold() const;
+
+ void set_primary_positive_limit_angle(float p_angle);
+ float get_primary_positive_limit_angle() const;
+ void set_primary_positive_damp_threshold(float p_power);
+ float get_primary_positive_damp_threshold() const;
+ void set_primary_negative_limit_angle(float p_angle);
+ float get_primary_negative_limit_angle() const;
+ void set_primary_negative_damp_threshold(float p_power);
+ float get_primary_negative_damp_threshold() const;
+
+ void set_secondary_limit_angle(float p_angle);
+ float get_secondary_limit_angle() const;
+ void set_secondary_damp_threshold(float p_power);
+ float get_secondary_damp_threshold() const;
+
+ void set_secondary_positive_limit_angle(float p_angle);
+ float get_secondary_positive_limit_angle() const;
+ void set_secondary_positive_damp_threshold(float p_power);
+ float get_secondary_positive_damp_threshold() const;
+ void set_secondary_negative_limit_angle(float p_angle);
+ float get_secondary_negative_limit_angle() const;
+ void set_secondary_negative_damp_threshold(float p_power);
+ float get_secondary_negative_damp_threshold() const;
+
+ float get_interpolation_remaining() const;
+ bool is_interpolating() const;
+ bool is_target_within_limitation() const;
+};
+
+VARIANT_ENUM_CAST(LookAtModifier3D::BoneAxis);
+VARIANT_ENUM_CAST(LookAtModifier3D::OriginFrom);
+
+#endif // LOOK_AT_MODIFIER_3D_H
diff --git a/scene/register_scene_types.cpp b/scene/register_scene_types.cpp
index 6b1ce2b4ca7..287dcfdbb79 100644
--- a/scene/register_scene_types.cpp
+++ b/scene/register_scene_types.cpp
@@ -243,6 +243,7 @@
#include "scene/3d/light_3d.h"
#include "scene/3d/lightmap_gi.h"
#include "scene/3d/lightmap_probe.h"
+#include "scene/3d/look_at_modifier_3d.h"
#include "scene/3d/marker_3d.h"
#include "scene/3d/mesh_instance_3d.h"
#include "scene/3d/multimesh_instance_3d.h"
@@ -608,6 +609,7 @@ void register_scene_types() {
GDREGISTER_CLASS(SkeletonIK3D);
GDREGISTER_CLASS(BoneAttachment3D);
+ GDREGISTER_CLASS(LookAtModifier3D);
GDREGISTER_CLASS(VehicleBody3D);
GDREGISTER_CLASS(VehicleWheel3D);