The constant M_PI_4 is added to GLSL to ensure it works there too. Differential Revision: https://developer.blender.org/D14288
427 lines
15 KiB
C++
427 lines
15 KiB
C++
/* SPDX-License-Identifier: GPL-2.0-or-later
|
|
* Copyright 2020 Blender Foundation. All rights reserved. */
|
|
|
|
#include "BKE_armature.hh"
|
|
|
|
#include "BLI_listbase.h"
|
|
#include "BLI_math.h"
|
|
|
|
#include "DNA_armature_types.h"
|
|
|
|
#include "testing/testing.h"
|
|
|
|
namespace blender::bke::tests {
|
|
|
|
static const float FLOAT_EPSILON = 1.2e-7;
|
|
|
|
static const float SCALE_EPSILON = 3.71e-5;
|
|
static const float ORTHO_EPSILON = 5e-5;
|
|
|
|
/** Test that the matrix is orthogonal, i.e. has no scale or shear within acceptable precision. */
|
|
static double EXPECT_M3_ORTHOGONAL(const float mat[3][3],
|
|
double epsilon_scale,
|
|
double epsilon_ortho)
|
|
{
|
|
/* Do the checks in double precision to avoid precision issues in the checks themselves. */
|
|
double dmat[3][3];
|
|
copy_m3d_m3(dmat, mat);
|
|
|
|
/* Check individual axis scaling. */
|
|
EXPECT_NEAR(len_v3_db(dmat[0]), 1.0, epsilon_scale);
|
|
EXPECT_NEAR(len_v3_db(dmat[1]), 1.0, epsilon_scale);
|
|
EXPECT_NEAR(len_v3_db(dmat[2]), 1.0, epsilon_scale);
|
|
|
|
/* Check orthogonality. */
|
|
EXPECT_NEAR(dot_v3v3_db(dmat[0], dmat[1]), 0.0, epsilon_ortho);
|
|
EXPECT_NEAR(dot_v3v3_db(dmat[0], dmat[2]), 0.0, epsilon_ortho);
|
|
EXPECT_NEAR(dot_v3v3_db(dmat[1], dmat[2]), 0.0, epsilon_ortho);
|
|
|
|
/* Check determinant to detect flipping and as a secondary volume change check. */
|
|
double determinant = determinant_m3_array_db(dmat);
|
|
|
|
EXPECT_NEAR(determinant, 1.0, epsilon_ortho);
|
|
|
|
return determinant;
|
|
}
|
|
|
|
TEST(mat3_vec_to_roll, UnitMatrix)
|
|
{
|
|
float unit_matrix[3][3];
|
|
float roll;
|
|
|
|
unit_m3(unit_matrix);
|
|
|
|
/* Any vector with a unit matrix should return zero roll. */
|
|
mat3_vec_to_roll(unit_matrix, unit_matrix[0], &roll);
|
|
EXPECT_FLOAT_EQ(0.0f, roll);
|
|
|
|
mat3_vec_to_roll(unit_matrix, unit_matrix[1], &roll);
|
|
EXPECT_FLOAT_EQ(0.0f, roll);
|
|
|
|
mat3_vec_to_roll(unit_matrix, unit_matrix[2], &roll);
|
|
EXPECT_FLOAT_EQ(0.0f, roll);
|
|
|
|
{
|
|
/* Non-unit vector. */
|
|
float vector[3] = {1.0f, 1.0f, 1.0f};
|
|
mat3_vec_to_roll(unit_matrix, vector, &roll);
|
|
EXPECT_NEAR(0.0f, roll, FLOAT_EPSILON);
|
|
|
|
/* Normalized version of the above vector. */
|
|
normalize_v3(vector);
|
|
mat3_vec_to_roll(unit_matrix, vector, &roll);
|
|
EXPECT_NEAR(0.0f, roll, FLOAT_EPSILON);
|
|
}
|
|
}
|
|
|
|
TEST(mat3_vec_to_roll, Rotationmatrix)
|
|
{
|
|
float rotation_matrix[3][3];
|
|
float roll;
|
|
|
|
const float rot_around_x[3] = {1.234f, 0.0f, 0.0f};
|
|
eul_to_mat3(rotation_matrix, rot_around_x);
|
|
|
|
{
|
|
const float unit_axis_x[3] = {1.0f, 0.0f, 0.0f};
|
|
mat3_vec_to_roll(rotation_matrix, unit_axis_x, &roll);
|
|
EXPECT_NEAR(1.234f, roll, FLOAT_EPSILON);
|
|
}
|
|
|
|
{
|
|
const float unit_axis_y[3] = {0.0f, 1.0f, 0.0f};
|
|
mat3_vec_to_roll(rotation_matrix, unit_axis_y, &roll);
|
|
EXPECT_NEAR(0, roll, FLOAT_EPSILON);
|
|
}
|
|
|
|
{
|
|
const float unit_axis_z[3] = {0.0f, 0.0f, 1.0f};
|
|
mat3_vec_to_roll(rotation_matrix, unit_axis_z, &roll);
|
|
EXPECT_NEAR(0, roll, FLOAT_EPSILON);
|
|
}
|
|
|
|
{
|
|
const float between_x_and_y[3] = {1.0f, 1.0f, 0.0f};
|
|
mat3_vec_to_roll(rotation_matrix, between_x_and_y, &roll);
|
|
EXPECT_NEAR(0.57158958f, roll, FLOAT_EPSILON);
|
|
}
|
|
}
|
|
|
|
/** Generic function to test vec_roll_to_mat3_normalized. */
|
|
static double test_vec_roll_to_mat3_normalized(const float input[3],
|
|
float roll,
|
|
const float expected_roll_mat[3][3],
|
|
bool normalize = true)
|
|
{
|
|
float input_normalized[3];
|
|
float roll_mat[3][3];
|
|
|
|
if (normalize) {
|
|
/* The vector is re-normalized to replicate the actual usage. */
|
|
normalize_v3_v3(input_normalized, input);
|
|
}
|
|
else {
|
|
copy_v3_v3(input_normalized, input);
|
|
}
|
|
|
|
vec_roll_to_mat3_normalized(input_normalized, roll, roll_mat);
|
|
|
|
EXPECT_V3_NEAR(roll_mat[1], input_normalized, FLT_EPSILON);
|
|
|
|
if (expected_roll_mat) {
|
|
EXPECT_M3_NEAR(roll_mat, expected_roll_mat, FLT_EPSILON);
|
|
}
|
|
|
|
return EXPECT_M3_ORTHOGONAL(roll_mat, SCALE_EPSILON, ORTHO_EPSILON);
|
|
}
|
|
|
|
/** Binary search to test where the code switches to the most degenerate special case. */
|
|
static double find_flip_boundary(double x, double z)
|
|
{
|
|
/* Irrational scale factor to ensure values aren't 'nice', have a lot of rounding errors,
|
|
* and can't accidentally produce the exact result returned by the special case. */
|
|
const double scale = M_1_PI / 10;
|
|
double theta = x * x + z * z;
|
|
double minv = 0, maxv = 1e-2;
|
|
|
|
while (maxv - minv > FLT_EPSILON * 1e-3) {
|
|
double mid = (minv + maxv) / 2;
|
|
|
|
float roll_mat[3][3];
|
|
float input[3] = {float(x * mid * scale),
|
|
-float(sqrt(1 - theta * mid * mid) * scale),
|
|
float(z * mid * scale)};
|
|
|
|
normalize_v3(input);
|
|
vec_roll_to_mat3_normalized(input, 0, roll_mat);
|
|
|
|
/* The special case assigns exact constants rather than computing. */
|
|
if (roll_mat[0][0] == -1 && roll_mat[0][1] == 0 && roll_mat[2][1] == 0) {
|
|
minv = mid;
|
|
}
|
|
else {
|
|
maxv = mid;
|
|
}
|
|
}
|
|
|
|
return sqrt(theta) * (minv + maxv) * 0.5;
|
|
}
|
|
|
|
TEST(vec_roll_to_mat3_normalized, FlippedBoundary1)
|
|
{
|
|
EXPECT_NEAR(find_flip_boundary(0, 1), 2.50e-4, 0.01e-4);
|
|
}
|
|
|
|
TEST(vec_roll_to_mat3_normalized, FlippedBoundary2)
|
|
{
|
|
EXPECT_NEAR(find_flip_boundary(1, 1), 2.50e-4, 0.01e-4);
|
|
}
|
|
|
|
/* Test cases close to the -Y axis. */
|
|
TEST(vec_roll_to_mat3_normalized, Flipped1)
|
|
{
|
|
/* If normalized_vector is -Y, simple symmetry by Z axis. */
|
|
const float input[3] = {0.0f, -1.0f, 0.0f};
|
|
const float expected_roll_mat[3][3] = {
|
|
{-1.0f, 0.0f, 0.0f}, {0.0f, -1.0f, 0.0f}, {0.0f, 0.0f, 1.0f}};
|
|
test_vec_roll_to_mat3_normalized(input, 0.0f, expected_roll_mat, false);
|
|
}
|
|
|
|
TEST(vec_roll_to_mat3_normalized, Flipped2)
|
|
{
|
|
/* If normalized_vector is close to -Y and
|
|
* it has X and Z values below a threshold,
|
|
* simple symmetry by Z axis. */
|
|
const float input[3] = {1e-24, -0.999999881, 0};
|
|
const float expected_roll_mat[3][3] = {
|
|
{-1.0f, 0.0f, 0.0f}, {0.0f, -1.0f, 0.0f}, {0.0f, 0.0f, 1.0f}};
|
|
test_vec_roll_to_mat3_normalized(input, 0.0f, expected_roll_mat, false);
|
|
}
|
|
|
|
TEST(vec_roll_to_mat3_normalized, Flipped3)
|
|
{
|
|
/* If normalized_vector is in a critical range close to -Y, apply the special case. */
|
|
const float input[3] = {2.5e-4f, -0.999999881f, 2.5e-4f}; /* Corner Case. */
|
|
const float expected_roll_mat[3][3] = {{0.000000f, -2.5e-4f, -1.000000f},
|
|
{2.5e-4f, -0.999999881f, 2.5e-4f},
|
|
{-1.000000f, -2.5e-4f, 0.000000f}};
|
|
test_vec_roll_to_mat3_normalized(input, 0.0f, expected_roll_mat, false);
|
|
}
|
|
|
|
/* Test 90 degree rotations. */
|
|
TEST(vec_roll_to_mat3_normalized, Rotate90_Z_CW)
|
|
{
|
|
/* Rotate 90 around Z. */
|
|
const float input[3] = {1, 0, 0};
|
|
const float expected_roll_mat[3][3] = {{0, -1, 0}, {1, 0, 0}, {0, 0, 1}};
|
|
test_vec_roll_to_mat3_normalized(input, 0.0f, expected_roll_mat);
|
|
}
|
|
|
|
TEST(vec_roll_to_mat3_normalized, Rotate90_Z_CCW)
|
|
{
|
|
/* Rotate 90 around Z. */
|
|
const float input[3] = {-1, 0, 0};
|
|
const float expected_roll_mat[3][3] = {{0, 1, 0}, {-1, 0, 0}, {0, 0, 1}};
|
|
test_vec_roll_to_mat3_normalized(input, 0.0f, expected_roll_mat);
|
|
}
|
|
|
|
TEST(vec_roll_to_mat3_normalized, Rotate90_X_CW)
|
|
{
|
|
/* Rotate 90 around X. */
|
|
const float input[3] = {0, 0, -1};
|
|
const float expected_roll_mat[3][3] = {{1, 0, 0}, {0, 0, -1}, {0, 1, 0}};
|
|
test_vec_roll_to_mat3_normalized(input, 0.0f, expected_roll_mat);
|
|
}
|
|
|
|
TEST(vec_roll_to_mat3_normalized, Rotate90_X_CCW)
|
|
{
|
|
/* Rotate 90 around X. */
|
|
const float input[3] = {0, 0, 1};
|
|
const float expected_roll_mat[3][3] = {{1, 0, 0}, {0, 0, 1}, {0, -1, 0}};
|
|
test_vec_roll_to_mat3_normalized(input, 0.0f, expected_roll_mat);
|
|
}
|
|
|
|
/* Test the general case when the vector is far enough from -Y. */
|
|
TEST(vec_roll_to_mat3_normalized, Generic1)
|
|
{
|
|
const float input[3] = {1.0f, 1.0f, 1.0f}; /* Arbitrary Value. */
|
|
const float expected_roll_mat[3][3] = {{0.788675129f, -0.577350259f, -0.211324856f},
|
|
{0.577350259f, 0.577350259f, 0.577350259f},
|
|
{-0.211324856f, -0.577350259f, 0.788675129f}};
|
|
test_vec_roll_to_mat3_normalized(input, 0.0f, expected_roll_mat);
|
|
}
|
|
|
|
TEST(vec_roll_to_mat3_normalized, Generic2)
|
|
{
|
|
const float input[3] = {1.0f, -1.0f, 1.0f}; /* Arbitrary Value. */
|
|
const float expected_roll_mat[3][3] = {{0.211324856f, -0.577350259f, -0.788675129f},
|
|
{0.577350259f, -0.577350259f, 0.577350259f},
|
|
{-0.788675129f, -0.577350259f, 0.211324856f}};
|
|
test_vec_roll_to_mat3_normalized(input, 0.0f, expected_roll_mat);
|
|
}
|
|
|
|
TEST(vec_roll_to_mat3_normalized, Generic3)
|
|
{
|
|
const float input[3] = {-1.0f, -1.0f, 1.0f}; /* Arbitrary Value. */
|
|
const float expected_roll_mat[3][3] = {{0.211324856f, 0.577350259f, 0.788675129f},
|
|
{-0.577350259f, -0.577350259f, 0.577350259f},
|
|
{0.788675129f, -0.577350259f, 0.211324856f}};
|
|
test_vec_roll_to_mat3_normalized(input, 0.0f, expected_roll_mat);
|
|
}
|
|
|
|
TEST(vec_roll_to_mat3_normalized, Generic4)
|
|
{
|
|
const float input[3] = {-1.0f, -1.0f, -1.0f}; /* Arbitrary Value. */
|
|
const float expected_roll_mat[3][3] = {{0.211324856f, 0.577350259f, -0.788675129f},
|
|
{-0.577350259f, -0.577350259f, -0.577350259f},
|
|
{-0.788675129f, 0.577350259f, 0.211324856f}};
|
|
test_vec_roll_to_mat3_normalized(input, 0.0f, expected_roll_mat);
|
|
}
|
|
|
|
/* Test roll. */
|
|
TEST(vec_roll_to_mat3_normalized, Roll1)
|
|
{
|
|
const float input[3] = {1.0f, 1.0f, 1.0f}; /* Arbitrary Value. */
|
|
const float expected_roll_mat[3][3] = {{0.211324856f, 0.577350259f, -0.788675129f},
|
|
{0.577350259f, 0.577350259f, 0.577350259f},
|
|
{0.788675129f, -0.577350259f, -0.211324856f}};
|
|
test_vec_roll_to_mat3_normalized(input, float(M_PI_2), expected_roll_mat);
|
|
}
|
|
|
|
/** Test that the matrix is orthogonal for an input close to -Y. */
|
|
static double test_vec_roll_to_mat3_orthogonal(double s, double x, double z)
|
|
{
|
|
const float input[3] = {float(x), float(s * sqrt(1 - x * x - z * z)), float(z)};
|
|
|
|
return test_vec_roll_to_mat3_normalized(input, 0.0f, nullptr);
|
|
}
|
|
|
|
/** Test that the matrix is orthogonal for a range of inputs close to -Y. */
|
|
static void test_vec_roll_to_mat3_orthogonal(double s, double x1, double x2, double y1, double y2)
|
|
{
|
|
const int count = 5000;
|
|
double delta = 0;
|
|
double tmax = 0;
|
|
|
|
for (int i = 0; i <= count; i++) {
|
|
double t = double(i) / count;
|
|
double det = test_vec_roll_to_mat3_orthogonal(s, interpd(x2, x1, t), interpd(y2, y1, t));
|
|
|
|
/* Find and report maximum error in the matrix determinant. */
|
|
double curdelta = abs(det - 1);
|
|
if (curdelta > delta) {
|
|
delta = curdelta;
|
|
tmax = t;
|
|
}
|
|
}
|
|
|
|
printf(" Max determinant deviation %.10f at %f.\n", delta, tmax);
|
|
}
|
|
|
|
#define TEST_VEC_ROLL_TO_MAT3_ORTHOGONAL(name, s, x1, x2, y1, y2) \
|
|
TEST(vec_roll_to_mat3_normalized, name) \
|
|
{ \
|
|
test_vec_roll_to_mat3_orthogonal(s, x1, x2, y1, y2); \
|
|
}
|
|
|
|
/* Moving from -Y towards X. */
|
|
TEST_VEC_ROLL_TO_MAT3_ORTHOGONAL(OrthoN_000_005, -1, 0, 0, 3e-4, 0.005)
|
|
TEST_VEC_ROLL_TO_MAT3_ORTHOGONAL(OrthoN_000_010, -1, 0, 0, 0.005, 0.010)
|
|
TEST_VEC_ROLL_TO_MAT3_ORTHOGONAL(OrthoN_000_050, -1, 0, 0, 0.010, 0.050)
|
|
TEST_VEC_ROLL_TO_MAT3_ORTHOGONAL(OrthoN_000_100, -1, 0, 0, 0.050, 0.100)
|
|
TEST_VEC_ROLL_TO_MAT3_ORTHOGONAL(OrthoN_000_200, -1, 0, 0, 0.100, 0.200)
|
|
TEST_VEC_ROLL_TO_MAT3_ORTHOGONAL(OrthoN_000_300, -1, 0, 0, 0.200, 0.300)
|
|
|
|
/* Moving from -Y towards X and Y. */
|
|
TEST_VEC_ROLL_TO_MAT3_ORTHOGONAL(OrthoN_005_005, -1, 3e-4, 0.005, 3e-4, 0.005)
|
|
TEST_VEC_ROLL_TO_MAT3_ORTHOGONAL(OrthoN_010_010, -1, 0.005, 0.010, 0.005, 0.010)
|
|
TEST_VEC_ROLL_TO_MAT3_ORTHOGONAL(OrthoN_050_050, -1, 0.010, 0.050, 0.010, 0.050)
|
|
TEST_VEC_ROLL_TO_MAT3_ORTHOGONAL(OrthoN_100_100, -1, 0.050, 0.100, 0.050, 0.100)
|
|
TEST_VEC_ROLL_TO_MAT3_ORTHOGONAL(OrthoN_200_200, -1, 0.100, 0.200, 0.100, 0.200)
|
|
|
|
/* Moving from +Y towards X. */
|
|
TEST_VEC_ROLL_TO_MAT3_ORTHOGONAL(OrthoP_000_005, 1, 0, 0, 0, 0.005)
|
|
TEST_VEC_ROLL_TO_MAT3_ORTHOGONAL(OrthoP_000_100, 1, 0, 0, 0.005, 0.100)
|
|
|
|
/* Moving from +Y towards X and Y. */
|
|
TEST_VEC_ROLL_TO_MAT3_ORTHOGONAL(OrthoP_005_005, 1, 0, 0.005, 0, 0.005)
|
|
TEST_VEC_ROLL_TO_MAT3_ORTHOGONAL(OrthoP_100_100, 1, 0.005, 0.100, 0.005, 0.100)
|
|
|
|
class BKE_armature_find_selected_bones_test : public testing::Test {
|
|
protected:
|
|
bArmature arm;
|
|
Bone bone1, bone2, bone3;
|
|
|
|
void SetUp() override
|
|
{
|
|
strcpy(bone1.name, "bone1");
|
|
strcpy(bone2.name, "bone2");
|
|
strcpy(bone3.name, "bone3");
|
|
|
|
arm.bonebase = {nullptr, nullptr};
|
|
bone1.childbase = {nullptr, nullptr};
|
|
bone2.childbase = {nullptr, nullptr};
|
|
bone3.childbase = {nullptr, nullptr};
|
|
|
|
BLI_addtail(&arm.bonebase, &bone1); /* bone1 is root bone. */
|
|
BLI_addtail(&arm.bonebase, &bone2); /* bone2 is root bone. */
|
|
BLI_addtail(&bone2.childbase, &bone3); /* bone3 has bone2 as parent. */
|
|
|
|
/* Make sure the armature & its bones are visible, to make them selectable. */
|
|
arm.layer = bone1.layer = bone2.layer = bone3.layer = 1;
|
|
}
|
|
};
|
|
|
|
TEST_F(BKE_armature_find_selected_bones_test, some_bones_selected)
|
|
{
|
|
bone1.flag = BONE_SELECTED;
|
|
bone2.flag = 0;
|
|
bone3.flag = BONE_SELECTED;
|
|
|
|
std::vector<Bone *> seen_bones;
|
|
auto callback = [&](Bone *bone) { seen_bones.push_back(bone); };
|
|
|
|
SelectedBonesResult result = BKE_armature_find_selected_bones(&arm, callback);
|
|
|
|
ASSERT_EQ(seen_bones.size(), 2) << "Expected 2 selected bones, got " << seen_bones.size();
|
|
EXPECT_EQ(seen_bones[0], &bone1);
|
|
EXPECT_EQ(seen_bones[1], &bone3);
|
|
|
|
EXPECT_FALSE(result.all_bones_selected); /* Bone 2 was not selected. */
|
|
EXPECT_FALSE(result.no_bones_selected); /* Bones 1 and 3 were selected. */
|
|
}
|
|
|
|
TEST_F(BKE_armature_find_selected_bones_test, no_bones_selected)
|
|
{
|
|
bone1.flag = bone2.flag = bone3.flag = 0;
|
|
|
|
std::vector<Bone *> seen_bones;
|
|
auto callback = [&](Bone *bone) { seen_bones.push_back(bone); };
|
|
|
|
SelectedBonesResult result = BKE_armature_find_selected_bones(&arm, callback);
|
|
|
|
EXPECT_TRUE(seen_bones.empty()) << "Expected no selected bones, got " << seen_bones.size();
|
|
EXPECT_FALSE(result.all_bones_selected);
|
|
EXPECT_TRUE(result.no_bones_selected);
|
|
}
|
|
|
|
TEST_F(BKE_armature_find_selected_bones_test, all_bones_selected)
|
|
{
|
|
bone1.flag = bone2.flag = bone3.flag = BONE_SELECTED;
|
|
|
|
std::vector<Bone *> seen_bones;
|
|
auto callback = [&](Bone *bone) { seen_bones.push_back(bone); };
|
|
|
|
SelectedBonesResult result = BKE_armature_find_selected_bones(&arm, callback);
|
|
|
|
ASSERT_EQ(seen_bones.size(), 3) << "Expected 3 selected bones, got " << seen_bones.size();
|
|
EXPECT_EQ(seen_bones[0], &bone1);
|
|
EXPECT_EQ(seen_bones[1], &bone2);
|
|
EXPECT_EQ(seen_bones[2], &bone3);
|
|
|
|
EXPECT_TRUE(result.all_bones_selected);
|
|
EXPECT_FALSE(result.no_bones_selected);
|
|
}
|
|
|
|
} // namespace blender::bke::tests
|