diff --git a/doc/classes/LookAtModifier3D.xml b/doc/classes/LookAtModifier3D.xml new file mode 100644 index 000000000000..c62f0c178f3b --- /dev/null +++ b/doc/classes/LookAtModifier3D.xml @@ -0,0 +1,149 @@ + + + + + + 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. + + + + + + + + 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_remain] 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]. + + + 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 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 during overshooting the limitations, the result may not respect the bone rest possibly. + + + 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. + + + The global position of the [Node3D] specified in [member origin_external_node] is used as origin. + + + 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..f2b5a5abae87 --- /dev/null +++ b/scene/3d/look_at_modifier_3d.cpp @@ -0,0 +1,687 @@ +/**************************************************************************/ +/* 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; + } +} + +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_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(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(Vector3 p_offset) { + origin_offset = p_offset; +} + +Vector3 LookAtModifier3D::get_origin_offset() const { + return origin_offset; +} + +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_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_remain() const { + return remain * duration; +} + +bool LookAtModifier3D::is_interpolating() const { + return Math::is_zero_approx(remain); +} + +// 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_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_remain"), &LookAtModifier3D::get_interpolation_remain); + 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 Setting", "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_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; + } + + Node3D *target = cast_to(get_node_or_null(target_node)); + Quaternion destination; + is_within_limitations = true; + + 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); + } + + Vector3 prev_forward_vector = forward_vector; + 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 = 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).normalized()); + destination = look_at_with_axes(skeleton->get_bone_rest(bone)).basis.get_rotation_quaternion(); + } + + if (Math::is_equal_approx((float)prev_forward_vector.dot(forward_vector), -1.0f)) { + init_transition(); // When dot production is -1.0, it is flipping. + } 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 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.dot(rest_forward_vector) < 0 && forward_vector.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.dot(rest_forward_vector) < 0 && forward_vector.dot(rest_forward_vector) < 0) { + 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; + float abs_value = Math::abs(p_value); + + if (Math::is_equal_approx(p_damp_threshold, 1.0f)) { + return sign * CLAMP(abs_value, p_from, p_to); // Avoid zero division. + } + + 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); + } + + float threshold_inv = 1.0f - p_damp_threshold; + float pw = 1.0f / threshold_inv; + value = -threshold_inv * Math::pow((float)Math_E, -pw * (value - p_damp_threshold)) + 1.0f; // Scaled & Shifted -e^-x. + + 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 = 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_powered(0, limit_angle, damp_threshold, calculated_angle); + current_result = p_rest.rotated_local(get_vector_from_axis(primary_rotation_axis), calculated_angle); + } + + // Needs for retecting 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); + 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); + + 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_powered(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; + 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..bfe19d2e8121 --- /dev/null +++ b/scene/3d/look_at_modifier_3d.h @@ -0,0 +1,190 @@ +/**************************************************************************/ +/* 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; + + 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; + NodePath origin_external_node; + + Vector3 origin_offset; + + 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; + + Quaternion from_q; // For time-based interpolation. + Quaternion prev_q; + + 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_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(NodePath p_external_node); + NodePath get_origin_external_node() const; + + void set_origin_offset(Vector3 p_offset); + Vector3 get_origin_offset() 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_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_remain() 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 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);