393 lines
16 KiB
C++
Raw Normal View History

2021-11-11 21:22:24 +01:00
//----------------------------------------------------------------------------//
// //
// ozz-animation is hosted at http://github.com/guillaumeblanc/ozz-animation //
// and distributed under the MIT License (MIT). //
// //
// Copyright (c) Guillaume Blanc //
// //
// 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 "ozz/animation/runtime/ik_two_bone_job.h"
#include <cassert>
#include "ozz/base/log.h"
#include "ozz/base/maths/math_ex.h"
#include "ozz/base/maths/simd_quaternion.h"
using namespace ozz::math;
namespace ozz {
namespace animation {
IKTwoBoneJob::IKTwoBoneJob()
: target(math::simd_float4::zero()),
mid_axis(math::simd_float4::z_axis()),
pole_vector(math::simd_float4::y_axis()),
twist_angle(0.f),
soften(1.f),
weight(1.f),
start_joint(nullptr),
mid_joint(nullptr),
end_joint(nullptr),
start_joint_correction(nullptr),
mid_joint_correction(nullptr),
reached(nullptr) {}
bool IKTwoBoneJob::Validate() const {
bool valid = true;
valid &= start_joint && mid_joint && end_joint;
valid &= start_joint_correction && mid_joint_correction;
valid &= ozz::math::AreAllTrue1(ozz::math::IsNormalizedEst3(mid_axis));
return valid;
}
namespace {
// Local data structure used to share constant data accross ik stages.
struct IKConstantSetup {
IKConstantSetup(const IKTwoBoneJob& _job) {
// Prepares constants
one = simd_float4::one();
mask_sign = simd_int4::mask_sign();
m_one = Xor(one, mask_sign);
// Computes inverse matrices required to change to start and mid spaces.
// If matrices aren't invertible, they'll be all 0 (ozz::math
// implementation), which will result in identity correction quaternions.
SimdInt4 invertible;
(void)invertible;
inv_start_joint = Invert(*_job.start_joint, &invertible);
const Float4x4 inv_mid_joint = Invert(*_job.mid_joint, &invertible);
// Transform some positions to mid joint space (_ms)
const SimdFloat4 start_ms =
TransformPoint(inv_mid_joint, _job.start_joint->cols[3]);
const SimdFloat4 end_ms =
TransformPoint(inv_mid_joint, _job.end_joint->cols[3]);
// Transform some positions to start joint space (_ss)
const SimdFloat4 mid_ss =
TransformPoint(inv_start_joint, _job.mid_joint->cols[3]);
const SimdFloat4 end_ss =
TransformPoint(inv_start_joint, _job.end_joint->cols[3]);
// Computes bones vectors and length in mid and start spaces.
// Start joint position will be treated as 0 because all joints are
// expressed in start joint space.
start_mid_ms = -start_ms;
mid_end_ms = end_ms;
start_mid_ss = mid_ss;
const SimdFloat4 mid_end_ss = end_ss - mid_ss;
const SimdFloat4 start_end_ss = end_ss;
start_mid_ss_len2 = Length3Sqr(start_mid_ss);
mid_end_ss_len2 = Length3Sqr(mid_end_ss);
start_end_ss_len2 = Length3Sqr(start_end_ss);
}
// Constants
SimdFloat4 one;
SimdFloat4 m_one;
SimdInt4 mask_sign;
// Inverse matrices
Float4x4 inv_start_joint;
// Bones vectors and length in mid and start spaces (_ms and _ss).
SimdFloat4 start_mid_ms;
SimdFloat4 mid_end_ms;
SimdFloat4 start_mid_ss;
SimdFloat4 start_mid_ss_len2;
SimdFloat4 mid_end_ss_len2;
SimdFloat4 start_end_ss_len2;
};
// Smoothen target position when it's further that a ratio of the joint chain
// length, and start to target length isn't 0.
// Inspired by http://www.softimageblog.com/archives/108
// and http://www.ryanjuckett.com/programming/analytic-two-bone-ik-in-2d/
bool SoftenTarget(const IKTwoBoneJob& _job, const IKConstantSetup& _setup,
SimdFloat4* _start_target_ss,
SimdFloat4* _start_target_ss_len2) {
// Hanlde position in start joint space (_ss)
const SimdFloat4 start_target_original_ss =
TransformPoint(_setup.inv_start_joint, _job.target);
const SimdFloat4 start_target_original_ss_len2 =
Length3Sqr(start_target_original_ss);
const SimdFloat4 lengths =
Sqrt(SetZ(SetY(_setup.start_mid_ss_len2, _setup.mid_end_ss_len2),
start_target_original_ss_len2));
const SimdFloat4 start_mid_ss_len = lengths;
const SimdFloat4 mid_end_ss_len = SplatY(lengths);
const SimdFloat4 start_target_original_ss_len = SplatZ(lengths);
const SimdFloat4 bone_len_diff_abs =
AndNot(start_mid_ss_len - mid_end_ss_len, _setup.mask_sign);
const SimdFloat4 bones_chain_len = start_mid_ss_len + mid_end_ss_len;
const SimdFloat4 da = // da.yzw needs to be 0
bones_chain_len *
Clamp(simd_float4::zero(), simd_float4::LoadX(_job.soften), _setup.one);
const SimdFloat4 ds = bones_chain_len - da;
// Sotftens target position if it is further than a ratio (_soften) of the
// whole bone chain length. Needs to check also that ds and
// start_target_original_ss_len2 are != 0, because they're used as a
// denominator.
// x = start_target_original_ss_len > da
// y = start_target_original_ss_len > 0
// z = start_target_original_ss_len > bone_len_diff_abs
// w = ds > 0
const SimdFloat4 left = SetW(start_target_original_ss_len, ds);
const SimdFloat4 right = SetZ(da, bone_len_diff_abs);
const SimdInt4 comp = CmpGt(left, right);
const int comp_mask = MoveMask(comp);
// xyw all 1, z is untested.
if ((comp_mask & 0xb) == 0xb) {
// Finds interpolation ratio (aka alpha).
const SimdFloat4 alpha = (start_target_original_ss_len - da) * RcpEstX(ds);
// Approximate an exponential function with : 1-(3^4)/(alpha+3)^4
// The derivative must be 1 for x = 0, and y must never exceeds 1.
// Negative x aren't used.
const SimdFloat4 three = simd_float4::Load1(3.f);
const SimdFloat4 op = SetY(three, alpha + three);
const SimdFloat4 op2 = op * op;
const SimdFloat4 op4 = op2 * op2;
const SimdFloat4 ratio = op4 * RcpEstX(SplatY(op4));
// Recomputes start_target_ss vector and length.
const SimdFloat4 start_target_ss_len = da + ds - ds * ratio;
*_start_target_ss_len2 = start_target_ss_len * start_target_ss_len;
*_start_target_ss =
start_target_original_ss *
SplatX(start_target_ss_len * RcpEstX(start_target_original_ss_len));
} else {
*_start_target_ss = start_target_original_ss;
*_start_target_ss_len2 = start_target_original_ss_len2;
}
// The maximum distance we can reach is the soften bone chain length: da
// (stored in !x). The minimum distance we can reach is the absolute value of
// the difference of the 2 bone lengths, |d1d2| (stored in z). x is 0 and z
// is 1, yw are untested.
return (comp_mask & 0x5) == 0x4;
}
SimdQuaternion ComputeMidJoint(const IKTwoBoneJob& _job,
const IKConstantSetup& _setup,
_SimdFloat4 _start_target_ss_len2) {
// Computes expected angle at mid_ss joint, using law of cosine (generalized
// Pythagorean).
// c^2 = a^2 + b^2 - 2ab cosC
// cosC = (a^2 + b^2 - c^2) / 2ab
// Computes both corrected and initial mid joint angles
// cosine within a single SimdFloat4 (corrected is x component, initial is y).
const SimdFloat4 start_mid_end_sum_ss_len2 =
_setup.start_mid_ss_len2 + _setup.mid_end_ss_len2;
const SimdFloat4 start_mid_end_ss_half_rlen =
SplatX(simd_float4::Load1(.5f) *
RSqrtEstXNR(_setup.start_mid_ss_len2 * _setup.mid_end_ss_len2));
// Cos value needs to be clamped, as it will exit expected range if
// start_target_ss_len2 is longer than the triangle can be (start_mid_ss +
// mid_end_ss).
const SimdFloat4 mid_cos_angles_unclamped =
(SplatX(start_mid_end_sum_ss_len2) -
SetY(_start_target_ss_len2, _setup.start_end_ss_len2)) *
start_mid_end_ss_half_rlen;
const SimdFloat4 mid_cos_angles =
Clamp(_setup.m_one, mid_cos_angles_unclamped, _setup.one);
// Computes corrected angle
const SimdFloat4 mid_corrected_angle = ACosX(mid_cos_angles);
// Computes initial angle.
// The sign of this angle needs to be decided. It's considered negative if
// mid-to-end joint is bent backward (mid_axis direction dictates valid
// bent direction).
const SimdFloat4 bent_side_ref = Cross3(_setup.start_mid_ms, _job.mid_axis);
const SimdInt4 bent_side_flip = SplatX(
CmpLt(Dot3(bent_side_ref, _setup.mid_end_ms), simd_float4::zero()));
const SimdFloat4 mid_initial_angle =
Xor(ACosX(SplatY(mid_cos_angles)), And(bent_side_flip, _setup.mask_sign));
// Finally deduces initial to corrected angle difference.
const SimdFloat4 mid_angles_diff = mid_corrected_angle - mid_initial_angle;
// Builds queternion.
return SimdQuaternion::FromAxisAngle(_job.mid_axis, mid_angles_diff);
}
SimdQuaternion ComputeStartJoint(const IKTwoBoneJob& _job,
const IKConstantSetup& _setup,
const SimdQuaternion& _mid_rot_ms,
_SimdFloat4 _start_target_ss,
_SimdFloat4 _start_target_ss_len2) {
// Pole vector in start joint space (_ss)
const SimdFloat4 pole_ss =
TransformVector(_setup.inv_start_joint, _job.pole_vector);
// start_mid_ss with quaternion mid_rot_ms applied.
const SimdFloat4 mid_end_ss_final = TransformVector(
_setup.inv_start_joint,
TransformVector(*_job.mid_joint,
TransformVector(_mid_rot_ms, _setup.mid_end_ms)));
const SimdFloat4 start_end_ss_final = _setup.start_mid_ss + mid_end_ss_final;
// Quaternion for rotating the effector onto the target
const SimdQuaternion end_to_target_rot_ss =
SimdQuaternion::FromVectors(start_end_ss_final, _start_target_ss);
// Calculates rotate_plane_ss quaternion which aligns joint chain plane to
// the reference plane (pole vector). This can only be computed if start
// target axis is valid (not 0 length)
// -------------------------------------------------
SimdQuaternion start_rot_ss = end_to_target_rot_ss;
if (AreAllTrue1(CmpGt(_start_target_ss_len2, simd_float4::zero()))) {
// Computes each plane normal.
const ozz::math::SimdFloat4 ref_plane_normal_ss =
Cross3(_start_target_ss, pole_ss);
const ozz::math::SimdFloat4 ref_plane_normal_ss_len2 =
ozz::math::Length3Sqr(ref_plane_normal_ss);
// Computes joint chain plane normal, which is the same as mid joint axis
// (same triangle).
const ozz::math::SimdFloat4 mid_axis_ss =
TransformVector(_setup.inv_start_joint,
TransformVector(*_job.mid_joint, _job.mid_axis));
const ozz::math::SimdFloat4 joint_plane_normal_ss =
TransformVector(end_to_target_rot_ss, mid_axis_ss);
const ozz::math::SimdFloat4 joint_plane_normal_ss_len2 =
ozz::math::Length3Sqr(joint_plane_normal_ss);
// Computes all reciprocal square roots at once.
const SimdFloat4 rsqrts =
RSqrtEstNR(SetZ(SetY(_start_target_ss_len2, ref_plane_normal_ss_len2),
joint_plane_normal_ss_len2));
// Computes angle cosine between the 2 normalized normals.
const SimdFloat4 rotate_plane_cos_angle =
ozz::math::Dot3(ref_plane_normal_ss * SplatY(rsqrts),
joint_plane_normal_ss * SplatZ(rsqrts));
// Computes rotation axis, which is either start_target_ss or
// -start_target_ss depending on rotation direction.
const SimdFloat4 rotate_plane_axis_ss = _start_target_ss * SplatX(rsqrts);
const SimdFloat4 start_axis_flip =
And(SplatX(Dot3(joint_plane_normal_ss, pole_ss)), _setup.mask_sign);
const SimdFloat4 rotate_plane_axis_flipped_ss =
Xor(rotate_plane_axis_ss, start_axis_flip);
// Builds quaternion along rotation axis.
const SimdQuaternion rotate_plane_ss = SimdQuaternion::FromAxisCosAngle(
rotate_plane_axis_flipped_ss,
Clamp(_setup.m_one, rotate_plane_cos_angle, _setup.one));
if (_job.twist_angle != 0.f) {
// If a twist angle is provided, rotation angle is rotated along
// rotation plane axis.
const SimdQuaternion twist_ss = SimdQuaternion::FromAxisAngle(
rotate_plane_axis_ss, simd_float4::Load1(_job.twist_angle));
start_rot_ss = twist_ss * rotate_plane_ss * end_to_target_rot_ss;
} else {
start_rot_ss = rotate_plane_ss * end_to_target_rot_ss;
}
}
return start_rot_ss;
}
void WeightOutput(const IKTwoBoneJob& _job, const IKConstantSetup& _setup,
const SimdQuaternion& _start_rot,
const SimdQuaternion& _mid_rot) {
const SimdFloat4 zero = simd_float4::zero();
// Fix up quaternions so w is always positive, which is required for NLerp
// (with identity quaternion) to lerp the shortest path.
const SimdFloat4 start_rot_fu =
Xor(_start_rot.xyzw,
And(_setup.mask_sign, CmpLt(SplatW(_start_rot.xyzw), zero)));
const SimdFloat4 mid_rot_fu = Xor(
_mid_rot.xyzw, And(_setup.mask_sign, CmpLt(SplatW(_mid_rot.xyzw), zero)));
if (_job.weight < 1.f) {
// NLerp start and mid joint rotations.
const SimdFloat4 identity = simd_float4::w_axis();
const SimdFloat4 simd_weight = Max(zero, simd_float4::Load1(_job.weight));
// Lerp
const SimdFloat4 start_lerp = Lerp(identity, start_rot_fu, simd_weight);
const SimdFloat4 mid_lerp = Lerp(identity, mid_rot_fu, simd_weight);
// Normalize
const SimdFloat4 rsqrts =
RSqrtEstNR(SetY(Length4Sqr(start_lerp), Length4Sqr(mid_lerp)));
_job.start_joint_correction->xyzw = start_lerp * SplatX(rsqrts);
_job.mid_joint_correction->xyzw = mid_lerp * SplatY(rsqrts);
} else {
// Quatenions don't need interpolation
_job.start_joint_correction->xyzw = start_rot_fu;
_job.mid_joint_correction->xyzw = mid_rot_fu;
}
}
} // namespace
bool IKTwoBoneJob::Run() const {
if (!Validate()) {
return false;
}
// Early out if weight is 0.
if (weight <= 0.f) {
// No correction.
*start_joint_correction = *mid_joint_correction =
SimdQuaternion::identity();
// Target isn't reached.
if (reached) {
*reached = false;
}
return true;
}
// Prepares constant ik data.
const IKConstantSetup setup(*this);
// Finds soften target position.
SimdFloat4 start_target_ss;
SimdFloat4 start_target_ss_len2;
const bool lreached =
SoftenTarget(*this, setup, &start_target_ss, &start_target_ss_len2);
if (reached) {
*reached = lreached && weight >= 1.f;
}
// Calculate mid_rot_local quaternion which solves for the mid_ss joint
// rotation.
const SimdQuaternion mid_rot_ms =
ComputeMidJoint(*this, setup, start_target_ss_len2);
// Calculates end_to_target_rot_ss quaternion which solves for effector
// rotating onto the target.
const SimdQuaternion start_rot_ss = ComputeStartJoint(
*this, setup, mid_rot_ms, start_target_ss, start_target_ss_len2);
// Finally apply weight and output quaternions.
WeightOutput(*this, setup, start_rot_ss, mid_rot_ms);
return true;
}
} // namespace animation
} // namespace ozz