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);