diff --git a/doc/classes/LookAtModifier3D.xml b/doc/classes/LookAtModifier3D.xml
new file mode 100644
index 000000000000..074049a889e9
--- /dev/null
+++ b/doc/classes/LookAtModifier3D.xml
@@ -0,0 +1,75 @@
+
+
+
+
+
+ This [SkeletonModifier3D] rotates a bone to look a target. This is extremely 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.
+
+
+
+
+
+ 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
+
+
+ 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].
+
+
+ The threshold to start damping for [member primary_limit_angle]. It provides non-linear(logarithmic) 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 performed always.
+
+
+ The limit angle of the secondary rotation.
+
+
+ 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.
+
+
+ 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.
+
+
+ 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.
+
+
+
diff --git a/editor/icons/LookAtModifier3D.svg b/editor/icons/LookAtModifier3D.svg
new file mode 100644
index 000000000000..9315b297ef74
--- /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 000000000000..d1415a4bc544
--- /dev/null
+++ b/scene/3d/look_at_modifier_3d.cpp
@@ -0,0 +1,441 @@
+/**************************************************************************/
+/* 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") {
+ 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 ((!use_angle_limitation &&
+ (p_property.name.ends_with("limit_angle") || p_property.name.ends_with("damp_threshold"))) ||
+ (p_property.name.begins_with("secondary_") && !use_secondary_rotation)) {
+ p_property.usage = PROPERTY_USAGE_NONE;
+ }
+}
+
+void LookAtModifier3D::set_bone(int p_bone) {
+ bone = p_bone;
+}
+
+int LookAtModifier3D::get_bone() const {
+ return bone;
+}
+
+void LookAtModifier3D::set_forward_axis(LookAtModifier3D::BoneAxis p_axis) {
+ // TODO: Make warning "Forward axis and primary rotation axis must not be parallel.".
+ forward_axis = p_axis;
+}
+
+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;
+}
+
+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(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 time-based interpolation.
+
+void LookAtModifier3D::set_duration(float p_duration) {
+ duration = p_duration;
+ if (Math::is_zero_approx(p_duration)) {
+ time_step = 0;
+ remain = 0;
+ } else {
+ time_step = 1.0 / p_duration;
+ }
+}
+
+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_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_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;
+}
+
+// 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_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_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_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);
+
+ 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("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::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, "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");
+
+ 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);
+}
+
+void LookAtModifier3D::_process_modification() {
+ if (!is_inside_tree()) {
+ return;
+ }
+
+ Skeleton3D *skeleton = get_skeleton();
+ if (!skeleton || bone < 0 || bone > skeleton->get_bone_count()) {
+ return;
+ }
+
+ Node3D *target = cast_to(get_node_or_null(target_node));
+ Quaternion destination;
+ is_within_limitations = true;
+
+ Transform3D bone_rest_space = skeleton->get_global_transform() * skeleton->get_bone_global_rest(bone);
+
+ Vector2 prev_angles = current_angles;
+ if (!target) {
+ destination = skeleton->get_bone_pose_rotation(bone);
+ } else {
+ forward_vector = bone_rest_space.basis.xform_inv((target->get_global_transform().origin - bone_rest_space.origin).normalized());
+ destination = look_at_with_axes(skeleton->get_bone_rest(bone)).basis.get_rotation_quaternion();
+ }
+
+ // Flipping is detected on 180 degree from the rest.
+ if (use_angle_limitation && !is_within_limitations && prev_angles.sign() != current_angles.sign()) {
+ init_transition();
+ }
+
+ if (remain > 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();
+ }
+ remain = MAX(0, remain - 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 - remain, 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 - remain, 0.0, 1.0, 1.0));
+ }
+ }
+
+ skeleton->set_bone_pose_rotation(bone, destination);
+ prev_q = destination;
+}
+
+Vector3 LookAtModifier3D::get_basis_vector_from_bone_axis(Basis p_basis, LookAtModifier3D::BoneAxis p_axis) {
+ Vector3 ret;
+ switch (p_axis) {
+ case BONE_AXIS_PLUS_X: {
+ ret = p_basis.get_column(0).normalized();
+ } break;
+ case BONE_AXIS_MINUS_X: {
+ ret = -p_basis.get_column(0).normalized();
+ } break;
+ case BONE_AXIS_PLUS_Y: {
+ ret = p_basis.get_column(1).normalized();
+ } break;
+ case BONE_AXIS_MINUS_Y: {
+ ret = -p_basis.get_column(1).normalized();
+ } break;
+ case BONE_AXIS_PLUS_Z: {
+ ret = p_basis.get_column(2).normalized();
+ } break;
+ case BONE_AXIS_MINUS_Z: {
+ ret = -p_basis.get_column(2).normalized();
+ } break;
+ }
+ return ret;
+}
+
+Vector3 LookAtModifier3D::get_vector_from_bone_axis(LookAtModifier3D::BoneAxis p_axis) {
+ 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(Vector3::Axis p_axis) {
+ 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.normalized();
+}
+
+Vector2 LookAtModifier3D::get_projection_vector(Vector3 p_vector, Vector3::Axis p_axis) {
+ // 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.normalized();
+}
+
+float LookAtModifier3D::remap_powered(float p_from, float p_to, float p_damp_threshold, float p_value) {
+ float sign = signbit(p_value) ? -1.0f : 1.0f;
+
+ if (Math::is_equal_approx(p_damp_threshold, 1.0f)) {
+ return sign * CLAMP(Math::abs(p_value), p_from, p_to); // Avoid zero division.
+ }
+
+ float value = Math::abs(p_value);
+ value = Math::inverse_lerp(p_from, p_to, value);
+
+ if (value <= p_damp_threshold) {
+ return sign * CLAMP(Math::abs(p_value), p_from, p_to);
+ }
+
+ float threshold_inv = 1.0f - p_damp_threshold;
+ float pw = 1.0f / threshold_inv;
+ value = -threshold_inv * Math::pow((float)Math_E, -pw * value + pw - 1.0f) + 1.0f;
+
+ return sign * Math::lerp(p_from, p_to, value);
+}
+
+Transform3D LookAtModifier3D::look_at_with_axes(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);
+ Vector2 src_vec2 = get_projection_vector(p_rest.basis.xform_inv(forward_vector), primary_rotation_axis);
+ Vector2 dst_vec2 = get_projection_vector(p_rest.basis.xform_inv(current_vector), primary_rotation_axis);
+ 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 = primary_limit_angle * 0.5;
+
+ if (use_angle_limitation) {
+ if (Math::abs(calculated_angle) > limit_angle) {
+ is_within_limitations = false;
+ }
+ calculated_angle = remap_powered(0, limit_angle, primary_damp_threshold, calculated_angle);
+ current_result = p_rest.rotated_local(get_vector_from_axis(primary_rotation_axis), calculated_angle);
+ current_angles.x = calculated_angle;
+ }
+
+ if (!use_secondary_rotation) {
+ return current_result;
+ }
+
+ Vector3 secondary_plane = get_vector_from_bone_axis(forward_axis) + get_vector_from_axis(primary_rotation_axis);
+ Vector3::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);
+
+ // 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);
+ src_vec2 = get_projection_vector(primary_result.basis.xform_inv(forward_vector), secondary_rotation_axis);
+ dst_vec2 = get_projection_vector(primary_result.basis.xform_inv(current_vector), secondary_rotation_axis);
+ calculated_angle = src_vec2.angle_to(dst_vec2);
+ limit_angle = secondary_limit_angle * 0.5;
+
+ if (use_angle_limitation) {
+ if (Math::abs(calculated_angle) > limit_angle) {
+ is_within_limitations = false;
+ }
+ calculated_angle = remap_powered(0, limit_angle, secondary_damp_threshold, calculated_angle);
+ current_angles.y = 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;
+ remain = 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 000000000000..03af91997f88
--- /dev/null
+++ b/scene/3d/look_at_modifier_3d.h
@@ -0,0 +1,128 @@
+/**************************************************************************/
+/* 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,
+ };
+
+private:
+ int bone = 0;
+
+ Vector3 forward_vector;
+ BoneAxis forward_axis = BONE_AXIS_PLUS_Z;
+ Vector3::Axis primary_rotation_axis = Vector3::AXIS_Y;
+ bool use_secondary_rotation = true;
+
+ 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 is_within_limitations = false;
+ float primary_limit_angle = 360;
+ float primary_damp_threshold = 1.0;
+ float secondary_limit_angle = 360;
+ float secondary_damp_threshold = 1.0;
+
+ Quaternion from_q; // For time-based interpolation.
+ Quaternion prev_q;
+ Vector2 current_angles;
+
+ float remain = 0;
+ float time_step = 1.0;
+
+ Vector3 get_basis_vector_from_bone_axis(Basis p_basis, BoneAxis p_axis);
+ Vector3 get_vector_from_bone_axis(BoneAxis p_axis);
+ Vector3 get_vector_from_axis(Vector3::Axis p_axis);
+ Vector2 get_projection_vector(Vector3 p_vector, Vector3::Axis p_axis);
+ Transform3D look_at_with_axes(Transform3D p_rest);
+ float remap_powered(float p_from, float p_to, float p_damp_threshold, float p_value);
+ void init_transition();
+
+protected:
+ 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_target_node(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_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_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;
+};
+
+VARIANT_ENUM_CAST(LookAtModifier3D::BoneAxis);
+
+#endif // LOOK_AT_MODIFIER_3D_H
diff --git a/scene/register_scene_types.cpp b/scene/register_scene_types.cpp
index 6b1ce2b4ca7f..287dcfdbb799 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);