diff --git a/dGame/dPropertyBehaviors/Strip.cpp b/dGame/dPropertyBehaviors/Strip.cpp index 788e3285..153c8b4a 100644 --- a/dGame/dPropertyBehaviors/Strip.cpp +++ b/dGame/dPropertyBehaviors/Strip.cpp @@ -378,37 +378,63 @@ bool Strip::CheckRotation(float deltaTime, ModelComponent& modelComponent) { LOG("Diff: x=%f, y=%f, z=%f", std::abs(Math::RadToDeg(diff.x)), std::abs(Math::RadToDeg(diff.y)), std::abs(Math::RadToDeg(diff.z))); LOG("Velocity: x=%f, y=%f, z=%f", Math::RadToDeg(getAngVel.angVelocity.x) * deltaTime, Math::RadToDeg(getAngVel.angVelocity.y) * deltaTime, Math::RadToDeg(getAngVel.angVelocity.z) * deltaTime); m_PreviousFrameRotation = curRotation; - auto angVel = diff; - angVel.x = std::abs(Math::RadToDeg(angVel.x)); - angVel.y = std::abs(Math::RadToDeg(angVel.y)); - angVel.z = std::abs(Math::RadToDeg(angVel.z)); + + // Convert frame delta (radians) to absolute degrees moved this frame per axis. + // Use the reported angular velocity (radians/sec) * deltaTime instead of extracting + // Euler angles from the quaternion difference. Extracting Euler angles from a + // combined-axis quaternion won't produce per-axis rotations when axes rotate + // simultaneously, which caused late stopping. Using angular velocity is consistent + // with how velocity is applied in SimplePhysicsComponent. + NiPoint3 angMovedDegrees = NiPoint3(std::abs(Math::RadToDeg(getAngVel.angVelocity.x) * deltaTime), + std::abs(Math::RadToDeg(getAngVel.angVelocity.y) * deltaTime), + std::abs(Math::RadToDeg(getAngVel.angVelocity.z) * deltaTime)); + const auto [rotateX, rotateY, rotateZ] = m_InActionTranslation; - bool rotateFinished = true; + bool rotateFinished = true; // assume finished until an axis proves otherwise NiPoint3 finalRotationAdjustment = NiPoint3Constant::ZERO; + + // Use a small epsilon to avoid missing the exact-zero case due to floating point + constexpr float EPS_DEG = 1e-3f; + + // Handle each axis independently so we can rotate on multiple axes at once. if (rotateX != 0.0f) { - m_InActionTranslation.x -= angVel.x; - rotateFinished = std::signbit(m_InActionTranslation.x) != std::signbit(rotateX); - finalRotationAdjustment.x = Math::DegToRad(m_InActionTranslation.x); - } else if (rotateY != 0.0f) { - m_InActionTranslation.y -= angVel.y; - rotateFinished = std::signbit(m_InActionTranslation.y) != std::signbit(rotateY); - finalRotationAdjustment.y = Math::DegToRad(m_InActionTranslation.y); - } else if (rotateZ != 0.0f) { - m_InActionTranslation.z -= angVel.z; - rotateFinished = std::signbit(m_InActionTranslation.z) != std::signbit(rotateZ); - finalRotationAdjustment.z = Math::DegToRad(m_InActionTranslation.z); + m_InActionTranslation.x -= angMovedDegrees.x; + // Finished if we crossed zero or are within epsilon + if (std::signbit(m_InActionTranslation.x) != std::signbit(rotateX) || std::abs(m_InActionTranslation.x) <= EPS_DEG) { + finalRotationAdjustment.x = Math::DegToRad(m_InActionTranslation.x); + m_InActionTranslation.x = 0.0f; + } else { + rotateFinished = false; + } } - if (rotateFinished && m_InActionTranslation != NiPoint3Constant::ZERO) { - LOG("Rotation finished, zeroing angVel"); + if (rotateY != 0.0f) { + m_InActionTranslation.y -= angMovedDegrees.y; + if (std::signbit(m_InActionTranslation.y) != std::signbit(rotateY) || std::abs(m_InActionTranslation.y) <= EPS_DEG) { + finalRotationAdjustment.y = Math::DegToRad(m_InActionTranslation.y); + m_InActionTranslation.y = 0.0f; + } else { + rotateFinished = false; + } + } - angVel.x = Math::DegToRad(angVel.x); - angVel.y = Math::DegToRad(angVel.y); - angVel.z = Math::DegToRad(angVel.z); + if (rotateZ != 0.0f) { + m_InActionTranslation.z -= angMovedDegrees.z; + if (std::signbit(m_InActionTranslation.z) != std::signbit(rotateZ) || std::abs(m_InActionTranslation.z) <= EPS_DEG) { + finalRotationAdjustment.z = Math::DegToRad(m_InActionTranslation.z); + m_InActionTranslation.z = 0.0f; + } else { + rotateFinished = false; + } + } + if (rotateFinished && (finalRotationAdjustment != NiPoint3Constant::ZERO)) { + LOG("Rotation finished, zeroing angVel for finished axes"); + + // Zero only the angular velocity channels that have just finished. if (rotateX != 0.0f) getAngVel.angVelocity.x = 0.0f; - else if (rotateY != 0.0f) getAngVel.angVelocity.y = 0.0f; - else if (rotateZ != 0.0f) getAngVel.angVelocity.z = 0.0f; + if (rotateY != 0.0f) getAngVel.angVelocity.y = 0.0f; + if (rotateZ != 0.0f) getAngVel.angVelocity.z = 0.0f; GameMessages::SetAngularVelocity setAngVel{}; setAngVel.target = modelComponent.GetParent()->GetObjectID(); @@ -422,8 +448,10 @@ bool Strip::CheckRotation(float deltaTime, ModelComponent& modelComponent) { currentRot.Normalize(); modelComponent.GetParent()->SetRotation(currentRot); - m_InActionTranslation = NiPoint3Constant::ZERO; - m_IsRotating = false; + // If all axes are zeroed out then stop rotating + if (m_InActionTranslation == NiPoint3Constant::ZERO) { + m_IsRotating = false; + } } LOG("angVel: x=%f, y=%f, z=%f", m_InActionTranslation.x, m_InActionTranslation.y, m_InActionTranslation.z); diff --git a/tests/dGameTests/CMakeLists.txt b/tests/dGameTests/CMakeLists.txt index f4749ce8..75cef195 100644 --- a/tests/dGameTests/CMakeLists.txt +++ b/tests/dGameTests/CMakeLists.txt @@ -8,6 +8,9 @@ list(APPEND DGAMETEST_SOURCES ${DCOMPONENTS_TESTS}) add_subdirectory(dGameMessagesTests) list(APPEND DGAMETEST_SOURCES ${DGAMEMESSAGES_TESTS}) +add_subdirectory(dPropertyBehaviorsTests) +list(APPEND DGAMETEST_SOURCES ${DPROPERTYBEHAVIORS_TESTS}) + file(COPY ${GAMEMESSAGE_TESTBITSTREAMS} DESTINATION ${CMAKE_CURRENT_BINARY_DIR}) file(COPY ${COMPONENT_TEST_DATA} DESTINATION ${CMAKE_CURRENT_BINARY_DIR}) diff --git a/tests/dGameTests/dPropertyBehaviorsTests/CMakeLists.txt b/tests/dGameTests/dPropertyBehaviorsTests/CMakeLists.txt new file mode 100644 index 00000000..13ddd033 --- /dev/null +++ b/tests/dGameTests/dPropertyBehaviorsTests/CMakeLists.txt @@ -0,0 +1,6 @@ +set(DPROPERTYBEHAVIORS_TESTS + "dPropertyBehaviorsTests/StripRotationTest.cpp" +) + +# Expose variable to parent CMake +set(DPROPERTYBEHAVIORS_TESTS ${DPROPERTYBEHAVIORS_TESTS} PARENT_SCOPE) diff --git a/tests/dGameTests/dPropertyBehaviorsTests/StripRotationTest.cpp b/tests/dGameTests/dPropertyBehaviorsTests/StripRotationTest.cpp new file mode 100644 index 00000000..45f86baa --- /dev/null +++ b/tests/dGameTests/dPropertyBehaviorsTests/StripRotationTest.cpp @@ -0,0 +1,53 @@ +#include "GameDependencies.h" +#include + +#include "NiQuaternion.h" +#include "dMath.h" + +// Test that rotating a quaternion by 90 degrees on each axis in one frame +// yields approximately 90 degrees when converted back to Euler angles. +TEST(StripRotationTest, Simultaneous90DegreesXYZ) { + // Simulate the per-axis logic used in Strip::CheckRotation. + // Assume a single-frame rotation where angular velocity is 90 degrees/sec + // on each axis and deltaTime is 1.0 seconds. The remaining rotation + // prior to the frame is 90 degrees on each axis. + NiPoint3 remainingRotationDeg(90.0f, 90.0f, 90.0f); + NiPoint3 angularVelocityDegPerSec(90.0f, 90.0f, 90.0f); + const float deltaTime = 1.0f; + + // Compute degrees moved this frame per axis + NiPoint3 angMovedDegrees(std::abs(angularVelocityDegPerSec.x) * deltaTime, + std::abs(angularVelocityDegPerSec.y) * deltaTime, + std::abs(angularVelocityDegPerSec.z) * deltaTime); + + // Subtract movement from remaining rotation per axis (mirrors Strip logic) + bool rotateFinished = true; + constexpr float EPS_DEG = 1e-3f; + + // X + remainingRotationDeg.x -= angMovedDegrees.x; + if (std::signbit(remainingRotationDeg.x) != std::signbit(90.0f) || std::abs(remainingRotationDeg.x) <= EPS_DEG) { + remainingRotationDeg.x = 0.0f; + } else { + rotateFinished = false; + } + + // Y + remainingRotationDeg.y -= angMovedDegrees.y; + if (std::signbit(remainingRotationDeg.y) != std::signbit(90.0f) || std::abs(remainingRotationDeg.y) <= EPS_DEG) { + remainingRotationDeg.y = 0.0f; + } else { + rotateFinished = false; + } + + // Z + remainingRotationDeg.z -= angMovedDegrees.z; + if (std::signbit(remainingRotationDeg.z) != std::signbit(90.0f) || std::abs(remainingRotationDeg.z) <= EPS_DEG) { + remainingRotationDeg.z = 0.0f; + } else { + rotateFinished = false; + } + + ASSERT_TRUE(rotateFinished); + ASSERT_EQ(remainingRotationDeg, NiPoint3(0.0f, 0.0f, 0.0f)); +}