From 9b6cbdf489dde9c9495c54436011b027e5d0b3d9 Mon Sep 17 00:00:00 2001 From: fleroviux Date: Wed, 29 May 2024 00:02:39 +0200 Subject: [PATCH] Zephyr: Scene: simplify rotation system --- app/next/src/main_window.cpp | 10 +- zephyr/math/include/zephyr/math/rotation.hpp | 248 +++++------------- .../scene/include/zephyr/scene/transform.hpp | 10 +- zephyr/scene/src/transform.cpp | 2 +- 4 files changed, 76 insertions(+), 194 deletions(-) diff --git a/app/next/src/main_window.cpp b/app/next/src/main_window.cpp index 53b4d33..b146f9c 100644 --- a/app/next/src/main_window.cpp +++ b/app/next/src/main_window.cpp @@ -88,16 +88,16 @@ namespace zephyr { if(key_state[SDL_SCANCODE_UP]) euler_x += delta_r; if(key_state[SDL_SCANCODE_DOWN]) euler_x -= delta_r; camera_transform.SetPosition(camera_position); - camera_transform.GetRotation().SetFromEuler(euler_x, euler_y, 0.0f); + camera_transform.SetRotation(extrinsic_xyz_angles_to_quaternion({euler_x, euler_y, 0.0f})); for(SceneNode* cube : m_dynamic_cubes) { Vector3 position = cube->GetTransform().GetPosition(); position.X() += 0.01; cube->GetTransform().SetPosition(position); - Quaternion rotation = cube->GetTransform().GetRotation().GetAsQuaternion(); + Quaternion rotation = cube->GetTransform().GetRotation(); rotation = Quaternion::FromAxisAngle({0, 1, 0}, 0.01f) * rotation; - cube->GetTransform().GetRotation().SetFromQuaternion(rotation); + cube->GetTransform().SetRotation(rotation); } RenderFrame(); @@ -182,7 +182,7 @@ namespace zephyr { GLTFLoader gltf_loader{}; std::shared_ptr gltf_scene_1 = gltf_loader.Parse("models/DamagedHelmet/DamagedHelmet.gltf"); gltf_scene_1->GetTransform().SetPosition({1.0f, 0.0f, -5.0f}); - gltf_scene_1->GetTransform().GetRotation().SetFromEuler(1.5f, 0.0f, 0.0f); + gltf_scene_1->GetTransform().SetRotation(extrinsic_xyz_angles_to_quaternion({1.5f, 0.0f, 0.0f})); m_scene_graph->GetRoot()->Add(std::move(gltf_scene_1)); m_scene_graph->GetRoot()->Add(gltf_loader.Parse("models/triangleWithoutIndices/TriangleWithoutIndices.gltf")); @@ -190,7 +190,7 @@ namespace zephyr { m_behemoth_scene = gltf_loader.Parse("models/Behemoth/scene.gltf"); m_behemoth_scene->GetTransform().SetPosition({-1.0f, 0.0f, -5.0f}); - m_behemoth_scene->GetTransform().GetRotation().SetFromEuler(-M_PI * 0.5, M_PI, 0.0f); + m_behemoth_scene->GetTransform().SetRotation(extrinsic_xyz_angles_to_quaternion({-M_PI * 0.5, M_PI, 0.0f})); m_behemoth_scene->GetTransform().SetScale({0.5f, 0.5f, 0.5f}); m_scene_graph->GetRoot()->Add(m_behemoth_scene); } diff --git a/zephyr/math/include/zephyr/math/rotation.hpp b/zephyr/math/include/zephyr/math/rotation.hpp index 9324bd3..046326c 100644 --- a/zephyr/math/include/zephyr/math/rotation.hpp +++ b/zephyr/math/include/zephyr/math/rotation.hpp @@ -1,195 +1,77 @@ #pragma once -#include #include -#include -#include #include namespace zephyr { /** - * Represents a rotation around an arbitrary axis in 3D space. - * Underlying to the rotation is a quaternion encoding the current axis and angle. + * Convert a unit quaternion to extrinsic x-y-z (intrinsic z-y'-x'') Tait-Bryan angles. + * @param quaternion The unit quaternion + * @returns a {@link #Vector3} storing the X, Y and Z angles. */ - class Rotation { - public: - /** - * Default constructor. By default the Rotation is initialized to not rotate. - */ - Rotation() { - SetFromQuaternion(1.0f, 0.0f, 0.0f, 0.0f); - } - - /** - * Construct a Rotation from extrinsic x-y-z (intrinsic z-y'-x'') Tait-Bryan angles. - * @param x X angle - * @param y Y angle - * @param z Z angle - */ - Rotation(f32 x, f32 y, f32 z) { - SetFromEuler(x, y, z); - } - - /** - * Construct a Rotation from a quaternion. - * @param quaternion the Quaternion - */ - explicit Rotation(const Quaternion& quaternion) { - SetFromQuaternion(quaternion); - } - - /// @returns a reference to an event that is emitted whenever the rotation changes. - [[nodiscard]] VoidEvent& OnChange() const { - return m_event_on_change; - } - - /// @returns the rotation in quaternion form - [[nodiscard]] const Quaternion& GetAsQuaternion() const { - return m_quaternion; - } - - /** - * Set the rotation from a quaternion. - * @param quaternion the Quaternion - */ - void SetFromQuaternion(const Quaternion& quaternion) { - m_quaternion = quaternion; - MarkQuaternionAsChanged(); - } - - /** - * Set the rotation from a quaternion. - * @param w the w-component of the quaternion - * @param x the x-component of the quaternion - * @param y the y-component of the quaternion - * @param z the z-component of the quaternion - */ - void SetFromQuaternion(f32 w, f32 x, f32 y, f32 z) { - SetFromQuaternion({w, x, y, z}); - } - - /// @returns the rotation in 4x4 matrix form - [[nodiscard]] const Matrix4& GetAsMatrix4() const { - UpdateMatrixFromQuaternion(); - return m_matrix; - } - - /** - * Set the rotation from a 4x4 matrix - * @param matrix the 4x4 matrix - */ - void SetFromMatrix4(const Matrix4& matrix) { - m_quaternion = Quaternion::FromRotationMatrix(matrix); - // The 4x4 matrix is likely to be read and copying it now is faster than reconstructing it later. - m_matrix = matrix; - m_needs_euler_refresh = true; - m_event_on_change.Emit(); - } - - /** - * Returns the rotation in extrinsic x-y-z (intrinsic z-y'-x'') Tait-Bryan angles form. - * This is an expensive operation because the angles may need to be reconstructed. - * Due to the reconstruction, there also is no guarantee that calling - * {@link #GetAsEuler} after {@link #SetFromEuler} will return the original angles. - * @returns the Tait-Bryan angles - */ - const Vector3& GetAsEuler() { - UpdateEulerFromQuaternion(); - return m_euler; - } - - /** - * Set the rotation from extrinsic x-y-z (intrinsic z-y'-x'') Tait-Bryan angles. - * @param x X angle - * @param y Y angle - * @param z Z angle - */ - void SetFromEuler(f32 x, f32 y, f32 z) { - const f32 half_x = x * 0.5f; - const f32 cos_x = std::cos(half_x); - const f32 sin_x = std::sin(half_x); - - const f32 half_y = y * 0.5f; - const f32 cos_y = std::cos(half_y); - const f32 sin_y = std::sin(half_y); - - const f32 half_z = z * 0.5f; - const f32 cos_z = std::cos(half_z); - const f32 sin_z = std::sin(half_z); - - const f32 cos_z_cos_y = cos_z * cos_y; - const f32 sin_z_cos_y = sin_z * cos_y; - const f32 sin_z_sin_y = sin_z * sin_y; - const f32 cos_z_sin_y = cos_z * sin_y; - - SetFromQuaternion( - cos_z_cos_y * cos_x + sin_z_sin_y * sin_x, - cos_z_cos_y * sin_x - sin_z_sin_y * cos_x, - sin_z_cos_y * sin_x + cos_z_sin_y * cos_x, - sin_z_cos_y * cos_x - cos_z_sin_y * sin_x - ); - } - - /** - * Set the rotation from extrinsic x-y-z (intrinsic z-y'-x'') Tait-Bryan angles. - * @param euler the Tait-Bryan euler angles - */ - void SetFromEuler(Vector3 euler) { - SetFromEuler(euler.X(), euler.Y(), euler.Z()); - } - - private: - /// Signal internally that the quaternion has changed - void MarkQuaternionAsChanged() { - m_needs_matrix_refresh = true; - m_needs_euler_refresh = true; - m_event_on_change.Emit(); - } - - /// Update the 4x4 matrix from the quaternion. - void UpdateMatrixFromQuaternion() const { - if(m_needs_matrix_refresh) { - m_matrix = m_quaternion.ToRotationMatrix(); - m_needs_matrix_refresh = false; - } - } - - /// Update the euler angles from the quaternion. - void UpdateEulerFromQuaternion() const { - if(!m_needs_euler_refresh) { - return; - } - - // @todo: reconstruct the angles from the quaternion instead of the 4x4 matrix (which possibly needs to be updated first). - static constexpr f32 k_cos0_threshold = 1.0f - 1e-6f; - - UpdateMatrixFromQuaternion(); - - const f32 sin_y = -m_matrix[0][2]; - - m_euler.Y() = std::asin(std::clamp(sin_y, -1.0f, +1.0f)); - - // Guard against gimbal lock when Y=-90°/+90° (X and Z rotate around the same axis). - if(std::abs(sin_y) <= k_cos0_threshold) { - m_euler.X() = std::atan2(m_matrix[1][2], m_matrix[2][2]); - m_euler.Z() = std::atan2(m_matrix[0][1], m_matrix[0][0]); - } else { - m_euler.X() = std::atan2(m_matrix[1][0], m_matrix[2][0]); - m_euler.Z() = 0.0f; - } - - m_needs_euler_refresh = false; - } - - Quaternion m_quaternion{}; ///< the underlying rotation encoding the current axis and angle - - mutable Matrix4 m_matrix{}; ///< a 4x4 rotation matrix which is updated from the quaternion on demand. - mutable Vector3 m_euler{}; ///< a vector of euler angles which is updated from the quaternion on demand. - mutable bool m_needs_matrix_refresh{true}; ///< true when the 4x4 matrix (#{@link m_matrix}) is outdated and false otherwise. - mutable bool m_needs_euler_refresh{true}; ///< true whe euler angles (#{@link m_euler}) are outdated and false otherwise. - mutable VoidEvent m_event_on_change{}; ///< An event that is emitted when the rotation has changed. - }; + inline Vector3 quaternion_to_extrinsic_xyz_angles(const Quaternion& quaternion) { + Vector3 euler; + + const f32 wy = quaternion.W() * quaternion.Y(); + const f32 wz = quaternion.W() * quaternion.Z(); + const f32 xz = quaternion.X() * quaternion.Z(); + const f32 xy = quaternion.X() * quaternion.Y(); + const f32 yy = quaternion.Y() * quaternion.Y(); + const f32 zz = quaternion.Z() * quaternion.Z(); + + const f32 m00 = 1 - 2 * (zz + yy); + const f32 m01 = 2 * (xy + wz); + const bool gimbal_lock = std::sqrt(m00 * m00 + m01 * m01) < 1e-6; + + euler.Y() = std::asin(std::clamp(-2.0f * (xz - wy), -1.0f, +1.0f)); + + // Guard against gimbal lock when Y=-90°/+90° (X and Z rotate around the same axis). + if(!gimbal_lock) { + const f32 wx = quaternion.W() * quaternion.X(); + const f32 xx = quaternion.X() * quaternion.X(); + const f32 yz = quaternion.Y() * quaternion.Z(); + + euler.X() = std::atan2(yz + wx, 0.5f - (xx + yy)); + euler.Z() = std::atan2(xy + wz, 0.5f - (zz + yy)); + } else { + euler.X() = std::atan2(xy - wz, xz + wy); + euler.Z() = 0.0f; + } + + return euler; + } + + /** + * Convert extrinsic x-y-z (intrinsic z-y'-x'') Tait-Bryan angles into an unit quaternion. + * @param euler A {@link #Vector3} storing the extrinsic x-y-z Tait-Bryan angles + * @returns a {@link #Quaternion} + */ + inline Quaternion extrinsic_xyz_angles_to_quaternion(const Vector3& euler) { + const f32 half_x = euler.X() * 0.5f; + const f32 cos_x = std::cos(half_x); + const f32 sin_x = std::sin(half_x); + + const f32 half_y = euler.Y() * 0.5f; + const f32 cos_y = std::cos(half_y); + const f32 sin_y = std::sin(half_y); + + const f32 half_z = euler.Z() * 0.5f; + const f32 cos_z = std::cos(half_z); + const f32 sin_z = std::sin(half_z); + + const f32 cos_z_cos_y = cos_z * cos_y; + const f32 sin_z_cos_y = sin_z * cos_y; + const f32 sin_z_sin_y = sin_z * sin_y; + const f32 cos_z_sin_y = cos_z * sin_y; + + return { + cos_z_cos_y * cos_x + sin_z_sin_y * sin_x, + cos_z_cos_y * sin_x - sin_z_sin_y * cos_x, + sin_z_cos_y * sin_x + cos_z_sin_y * cos_x, + sin_z_cos_y * cos_x - cos_z_sin_y * sin_x + }; + } } // namespace zephyr diff --git a/zephyr/scene/include/zephyr/scene/transform.hpp b/zephyr/scene/include/zephyr/scene/transform.hpp index 3f910e7..920cca6 100644 --- a/zephyr/scene/include/zephyr/scene/transform.hpp +++ b/zephyr/scene/include/zephyr/scene/transform.hpp @@ -14,7 +14,6 @@ namespace zephyr { explicit Transform3D(SceneNode* node) : m_node{node} { UpdateLocal(); UpdateWorld(); - m_rotation.OnChange().Subscribe([this]() { SignalNodeTransformChanged(); }); } [[nodiscard]] const Vector3& GetPosition() const { @@ -35,12 +34,13 @@ namespace zephyr { SignalNodeTransformChanged(); } - [[nodiscard]] const Rotation& GetRotation() const { + [[nodiscard]] const Quaternion& GetRotation() const { return m_rotation; } - [[nodiscard]] Rotation& GetRotation() { - return m_rotation; + void SetRotation(const Quaternion& rotation) { + m_rotation = rotation; + SignalNodeTransformChanged(); } [[nodiscard]] const Matrix4& GetLocal() const { @@ -60,7 +60,7 @@ namespace zephyr { SceneNode* m_node; Vector3 m_position{}; Vector3 m_scale{1.0f, 1.0f, 1.0f}; - Rotation m_rotation{}; + Quaternion m_rotation{}; Matrix4 m_local_matrix{}; Matrix4 m_world_matrix{}; }; diff --git a/zephyr/scene/src/transform.cpp b/zephyr/scene/src/transform.cpp index 95c89c8..893f288 100644 --- a/zephyr/scene/src/transform.cpp +++ b/zephyr/scene/src/transform.cpp @@ -5,7 +5,7 @@ namespace zephyr { void Transform3D::UpdateLocal() { - m_local_matrix = m_rotation.GetAsMatrix4(); + m_local_matrix = m_rotation.ToRotationMatrix(); m_local_matrix.X() *= m_scale.X(); m_local_matrix.Y() *= m_scale.Y(); m_local_matrix.Z() *= m_scale.Z();