Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions src/FixedMathSharp/Extensions/Fixed4x4.Extensions.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.CompilerServices;

namespace FixedMathSharp
{
Expand Down Expand Up @@ -41,6 +40,18 @@ public static Fixed4x4 SetGlobalScale(this ref Fixed4x4 matrix, Vector3d globalS
return matrix = Fixed4x4.SetGlobalScale(matrix, globalScale);
}

/// <inheritdoc cref="Fixed4x4.TransformPoint(Fixed4x4, Vector3d)" />
public static Vector3d TransformPoint(this Fixed4x4 matrix, Vector3d point)
{
return Fixed4x4.TransformPoint(matrix, point);
}

/// <inheritdoc cref="Fixed4x4.InverseTransformPoint(Fixed4x4, Vector3d)" />
public static Vector3d InverseTransformPoint(this Fixed4x4 matrix, Vector3d point)
{
return Fixed4x4.InverseTransformPoint(matrix, point);
}

#endregion
}
}
12 changes: 10 additions & 2 deletions src/FixedMathSharp/Extensions/FixedQuaternion.Extensions.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
using FixedMathSharp;
using System.Runtime.CompilerServices;
using System.Runtime.CompilerServices;

namespace FixedMathSharp
{
public static partial class FixedQuaternionExtensions
{
/// <inheritdoc cref="FixedQuaternion.ToAngularVelocity" />
public static Vector3d ToAngularVelocity(
this FixedQuaternion currentRotation,
FixedQuaternion previousRotation,
Fixed64 deltaTime)
{
return FixedQuaternion.ToAngularVelocity(currentRotation, previousRotation, deltaTime);
}

#region Equality

/// <summary>
Expand Down
242 changes: 180 additions & 62 deletions src/FixedMathSharp/Numerics/Fixed4x4.cs

Large diffs are not rendered by default.

1 change: 0 additions & 1 deletion src/FixedMathSharp/Numerics/FixedCurve.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
using System;
using System.Linq;


#if NET8_0_OR_GREATER
using System.Text.Json.Serialization;
#endif
Expand Down Expand Up @@ -95,14 +94,14 @@
return Fixed64.One; // Fallback (should never be hit)
}

public bool Equals(FixedCurve other)

Check warning on line 97 in src/FixedMathSharp/Numerics/FixedCurve.cs

View workflow job for this annotation

GitHub Actions / build-and-test-linux

Nullability of reference types in type of parameter 'other' of 'bool FixedCurve.Equals(FixedCurve other)' doesn't match implicitly implemented member 'bool IEquatable<FixedCurve>.Equals(FixedCurve? other)' (possibly because of nullability attributes).

Check warning on line 97 in src/FixedMathSharp/Numerics/FixedCurve.cs

View workflow job for this annotation

GitHub Actions / build-and-test-windows

Nullability of reference types in type of parameter 'other' of 'bool FixedCurve.Equals(FixedCurve other)' doesn't match implicitly implemented member 'bool IEquatable<FixedCurve>.Equals(FixedCurve? other)' (possibly because of nullability attributes).
{
if (other is null) return false;
if (ReferenceEquals(this, other)) return true;
return Mode == other.Mode && Keyframes.SequenceEqual(other.Keyframes);
}

public override bool Equals(object obj) => obj is FixedCurve other && Equals(other);

Check warning on line 104 in src/FixedMathSharp/Numerics/FixedCurve.cs

View workflow job for this annotation

GitHub Actions / build-and-test-linux

Nullability of type of parameter 'obj' doesn't match overridden member (possibly because of nullability attributes).

Check warning on line 104 in src/FixedMathSharp/Numerics/FixedCurve.cs

View workflow job for this annotation

GitHub Actions / build-and-test-windows

Nullability of type of parameter 'obj' doesn't match overridden member (possibly because of nullability attributes).

public override int GetHashCode()
{
Expand Down
57 changes: 57 additions & 0 deletions src/FixedMathSharp/Numerics/FixedQuaternion.cs
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,63 @@
return GetNormalized(new FixedQuaternion(x, y, z, w));
}

/// <summary>
/// Computes the logarithm of a quaternion, which represents the rotational displacement.
/// This is useful for interpolation and angular velocity calculations.
/// </summary>
/// <param name="q">The quaternion to compute the logarithm of.</param>
/// <returns>A Vector3d representing the logarithm of the quaternion (axis-angle representation).</returns>
/// <remarks>
/// The logarithm of a unit quaternion is given by:
/// log(q) = (θ * v̂), where:
/// - θ = 2 * acos(w) is the rotation angle.
/// - v̂ = (x, y, z) / ||(x, y, z)|| is the unit vector representing the axis of rotation.
/// If the quaternion is close to identity, the function returns a zero vector to avoid numerical instability.
/// </remarks>
public static Vector3d QuaternionLog(FixedQuaternion q)
{
// Ensure the quaternion is normalized
q = q.Normal;

// Extract vector part
Vector3d v = new Vector3d(q.x, q.y, q.z);
Fixed64 vLength = v.Magnitude;

// If rotation is very small, avoid division by zero
if (vLength < Fixed64.FromRaw(0x00001000L)) // Small epsilon
return Vector3d.Zero;

// Compute angle (theta = 2 * acos(w))
Fixed64 theta = Fixed64.Two * FixedMath.Acos(q.w);

// Convert to angular velocity
return (v / vLength) * theta;
}

/// <summary>
/// Computes the angular velocity required to move from `previousRotation` to `currentRotation` over a given time step.
/// </summary>
/// <param name="currentRotation">The current orientation as a quaternion.</param>
/// <param name="previousRotation">The previous orientation as a quaternion.</param>
/// <param name="deltaTime">The time step over which the rotation occurs.</param>
/// <returns>A Vector3d representing the angular velocity (in radians per second).</returns>
/// <remarks>
/// This function calculates the change in rotation over `deltaTime` and converts it into angular velocity.
/// - First, it computes the relative rotation: `rotationDelta = currentRotation * previousRotation.Inverse()`.
/// - Then, it applies `QuaternionLog(rotationDelta)` to extract the axis-angle representation.
/// - Finally, it divides by `deltaTime` to compute the angular velocity.
/// </remarks>
public static Vector3d ToAngularVelocity(
FixedQuaternion currentRotation,
FixedQuaternion previousRotation,
Fixed64 deltaTime)
{
FixedQuaternion rotationDelta = currentRotation * previousRotation.Inverse();
Vector3d angularDisplacement = QuaternionLog(rotationDelta);

return angularDisplacement / deltaTime; // Convert to angular velocity
}

/// <summary>
/// Performs a simple linear interpolation between the components of the input quaternions
/// </summary>
Expand Down Expand Up @@ -665,7 +722,7 @@
#region Equality and HashCode Overrides

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override bool Equals(object obj)

Check warning on line 725 in src/FixedMathSharp/Numerics/FixedQuaternion.cs

View workflow job for this annotation

GitHub Actions / build-and-test-linux

Nullability of type of parameter 'obj' doesn't match overridden member (possibly because of nullability attributes).

Check warning on line 725 in src/FixedMathSharp/Numerics/FixedQuaternion.cs

View workflow job for this annotation

GitHub Actions / build-and-test-windows

Nullability of type of parameter 'obj' doesn't match overridden member (possibly because of nullability attributes).
{
return obj is FixedQuaternion other && Equals(other);
}
Expand Down
22 changes: 18 additions & 4 deletions src/FixedMathSharp/Numerics/Vector3d.cs
Original file line number Diff line number Diff line change
Expand Up @@ -997,12 +997,26 @@ public static Vector3d InverseRotate(Vector3d source, Vector3d position, FixedQu
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Vector3d operator *(Fixed4x4 matrix, Vector3d vector)
public static Vector3d operator *(Fixed4x4 matrix, Vector3d point)
{
if(matrix.IsAffine)
{
return new Vector3d(
matrix.m00 * point.x + matrix.m01 * point.y + matrix.m02 * point.z + matrix.m03 + matrix.m30,
matrix.m10 * point.x + matrix.m11 * point.y + matrix.m12 * point.z + matrix.m13 + matrix.m31,
matrix.m20 * point.x + matrix.m21 * point.y + matrix.m22 * point.z + matrix.m23 + matrix.m32
);
}

// Full 4×4 transformation
Fixed64 w = matrix.m03 * point.x + matrix.m13 * point.y + matrix.m23 * point.z + matrix.m33;
if (w == Fixed64.Zero) w = Fixed64.One; // Prevent divide-by-zero

return new Vector3d(
matrix.m00 * vector.x + matrix.m01 * vector.y + matrix.m02 * vector.z + matrix.m03,
matrix.m10 * vector.x + matrix.m11 * vector.y + matrix.m12 * vector.z + matrix.m13,
matrix.m20 * vector.x + matrix.m21 * vector.y + matrix.m22 * vector.z + matrix.m23);
(matrix.m00 * point.x + matrix.m01 * point.y + matrix.m02 * point.z + matrix.m03 + matrix.m30) / w,
(matrix.m10 * point.x + matrix.m11 * point.y + matrix.m12 * point.z + matrix.m13 + matrix.m31) / w,
(matrix.m20 * point.x + matrix.m21 * point.y + matrix.m22 * point.z + matrix.m23 + matrix.m32) / w
);
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
Expand Down
10 changes: 5 additions & 5 deletions tests/FixedMathSharp.Tests/Fixed4x4.Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ public void FixedMatrix4x4_TRS_CreatesCorrectTransformationMatrix()
var rotation = FixedQuaternion.FromEulerAnglesInDegrees((Fixed64)30, (Fixed64)45, (Fixed64)60);
var scale = new Vector3d(2, 3, 4);

var trsMatrix = Fixed4x4.SRT(translation, rotation, scale);
var trsMatrix = Fixed4x4.ScaleRotateTranslate(translation, rotation, scale);

// Instead of direct equality, compare the decomposed components
Assert.True(Fixed4x4.Decompose(trsMatrix, out var decomposedScale, out var decomposedRotation, out var decomposedTranslation));
Expand Down Expand Up @@ -279,7 +279,7 @@ public void TransformPoint_WorldToLocal_ReturnsCorrectResult()
var rotation = FixedQuaternion.FromEulerAnglesInDegrees(-(Fixed64)20, (Fixed64)35, (Fixed64)50);
var scale = new Vector3d(1, 2, 1.5);

var transformMatrix = Fixed4x4.SRT(translation, rotation, scale);
var transformMatrix = Fixed4x4.ScaleRotateTranslate(translation, rotation, scale);

var worldPoint = new Vector3d(10, 15, -2);
var localPoint = Fixed4x4.InverseTransformPoint(transformMatrix, worldPoint);
Expand All @@ -296,7 +296,7 @@ public void InverseTransformPoint_LocalToWorld_ReturnsCorrectResult()
var rotation = FixedQuaternion.FromEulerAnglesInDegrees((Fixed64)45, -(Fixed64)30, (Fixed64)90);
var scale = new Vector3d(1.2, 0.8, 1.5);

var transformMatrix = Fixed4x4.SRT(translation, rotation, scale);
var transformMatrix = Fixed4x4.ScaleRotateTranslate(translation, rotation, scale);

var localPoint = new Vector3d(2, 3, -1);
var worldPoint = Fixed4x4.TransformPoint(transformMatrix, localPoint);
Expand All @@ -313,7 +313,7 @@ public void TransformPoint_InverseTransformPoint_RoundTripConsistency()
var rotation = FixedQuaternion.FromEulerAnglesInDegrees(-(Fixed64)45, (Fixed64)30, (Fixed64)90);
var scale = new Vector3d(1.5, 2.5, 3.0);

var transformMatrix = Fixed4x4.SRT(translation, rotation, scale);
var transformMatrix = Fixed4x4.ScaleRotateTranslate(translation, rotation, scale);

var originalPoint = new Vector3d(3, 5, 7);
var transformedPoint = Fixed4x4.TransformPoint(transformMatrix, originalPoint);
Expand All @@ -330,7 +330,7 @@ public void Fixed4x4_Serialization_RoundTripMaintainsData()
var rotation = FixedQuaternion.FromEulerAnglesInDegrees(Fixed64.Zero, FixedMath.PiOver2, Fixed64.Zero);
var scale = new Vector3d(1, 1, 1);

var original4x4 = Fixed4x4.SRT(translation, rotation, scale);
var original4x4 = Fixed4x4.ScaleRotateTranslate(translation, rotation, scale);

// Serialize the Fixed4x4 object
#if NET48_OR_GREATER
Expand Down
49 changes: 48 additions & 1 deletion tests/FixedMathSharp.Tests/FixedQuanternion.Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
#if NET8_0_OR_GREATER
using System.Text.Json;
using System.Text.Json.Serialization;

#endif

using Xunit;
Expand Down Expand Up @@ -197,6 +196,33 @@ public void FixedQuaternion_ToMatrix_WorksCorrectly()
Assert.True(result == expected, $"ToMatrix returned {result}, expected Identity matrix.");
}

[Fact]
public void FixedQuaternion_ToAngularVelocity_WorksCorrectly()
{
var prevRotation = FixedQuaternion.Identity;
var currentRotation = FixedQuaternion.FromAxisAngle(Vector3d.Up, FixedMath.PiOver4); // Rotated 45 degrees around Y-axis
var deltaTime = new Fixed64(2); // Assume 2 seconds elapsed

var angularVelocity = FixedQuaternion.ToAngularVelocity(currentRotation, prevRotation, deltaTime);

var expected = new Vector3d(Fixed64.Zero, FixedMath.PiOver4 / deltaTime, Fixed64.Zero); // Expect ω = θ / dt
Assert.True(angularVelocity.FuzzyEqual(expected, new Fixed64(0.0001)),
$"ToAngularVelocity returned {angularVelocity}, expected {expected}");
}

[Fact]
public void FixedQuaternion_ToAngularVelocity_ZeroForNoRotation()
{
var prevRotation = FixedQuaternion.Identity;
var currentRotation = FixedQuaternion.Identity;
var deltaTime = Fixed64.One;

var angularVelocity = FixedQuaternion.ToAngularVelocity(currentRotation, prevRotation, deltaTime);

Assert.True(angularVelocity.FuzzyEqual(Vector3d.Zero),
$"ToAngularVelocity should return zero for no rotation, but got {angularVelocity}");
}

[Fact]
public void FixedQuanternion_Serialization_RoundTripMaintainsData()
{
Expand Down Expand Up @@ -336,6 +362,27 @@ public void FixedQuaternion_LookRotation_WorksCorrectly()
Assert.True(result.FuzzyEqual(expected), $"Look rotation returned {result}, expected {expected}.");
}

[Fact]
public void FixedQuaternion_QuaternionLog_WorksCorrectly()
{
var quaternion = FixedQuaternion.FromAxisAngle(Vector3d.Up, FixedMath.PiOver4); // 45-degree rotation around Y-axis
var logResult = FixedQuaternion.QuaternionLog(quaternion);

var expected = new Vector3d(Fixed64.Zero, FixedMath.PiOver4, Fixed64.Zero); // Expect log(q) = θ * axis
Assert.True(logResult.FuzzyEqual(expected, new Fixed64(0.0001)),
$"QuaternionLog returned {logResult}, expected {expected}");
}

[Fact]
public void FixedQuaternion_QuaternionLog_ReturnsZeroForIdentity()
{
var identity = FixedQuaternion.Identity;
var logResult = FixedQuaternion.QuaternionLog(identity);

Assert.True(logResult.FuzzyEqual(Vector3d.Zero),
$"QuaternionLog of Identity should be (0,0,0), but got {logResult}");
}

#endregion

#region Test: Operators
Expand Down
Loading