Recursive AI

Port from System.Numerics

All onemath recipes

Move matrix code from System.Numerics to OneMath without silently transposing your transforms: the conventions are opposite, and the conversions handle it.

C#
using RecursiveAI.OneMath;
 
float angle = MathF.PI / 2;
 
// System.Numerics (row vectors): T * R translates first, then rotates.
var sn = System.Numerics.Matrix4x4.CreateTranslation(10, 0, 0) * System.Numerics.Matrix4x4.CreateRotationY(angle);
// OneMath (column vectors): the same transform is R * T.
var m = Matrix4x4F.CreateRotationY(angle) * Matrix4x4F.CreateTranslation(new Vector3F(10, 0, 0));
 
var p = new Vector3F(1, 0, 0);
Console.WriteLine($"System.Numerics: {System.Numerics.Vector3.Transform(p.ToSystemNumerics(), sn)}");
Console.WriteLine($"OneMath:         {m.TransformPoint(p)}");
 
// Where the translation lives.
Console.WriteLine($"System.Numerics translation M41..M43: {sn.M41}, {sn.M42}, {sn.M43}");
Console.WriteLine($"OneMath translation M14, M24, M34:    {m.M14}, {m.M24}, {m.M34}");
 
// Convert, don't copy fields: both directions transpose.
Matrix4x4F same = Matrix4x4F.FromSystemNumerics(sn);
Console.WriteLine($"FromSystemNumerics(sn) == m: {same == m}");
Console.WriteLine($"Round trip equal: {Matrix4x4F.FromSystemNumerics(m.ToSystemNumerics()) == m}");
 
// Copying fields one to one gives the transpose: a different transform.
var copied = new Matrix4x4F(sn.M11, sn.M12, sn.M13, sn.M14, sn.M21, sn.M22, sn.M23, sn.M24,
                            sn.M31, sn.M32, sn.M33, sn.M34, sn.M41, sn.M42, sn.M43, sn.M44);
Console.WriteLine($"Field copy:      {copied.TransformPoint(p)}");
Text
System.Numerics: <-4.808253E-07, 0, -11>
OneMath:         (-4.808253E-07, 0, -11)
System.Numerics translation M41..M43: -4.371139E-07, 0, -10
OneMath translation M14, M24, M34:    -4.371139E-07, 0, -10
FromSystemNumerics(sn) == m: True
Round trip equal: True
Field copy:      (-4.371139E-08, 0, 1)

The rule

  • OneMath uses column vectors: m * v transforms the column v, a 4x4’s translation sits in column 4 (M14, M24, M34), and a * b applies b first.
  • System.Numerics uses row vectors: translation in M41…M43, and a * b applies a first. The field names Mrc are the same, which is what makes a field-by-field copy look right and transform wrongly.
  • So reverse the operand order of every product, and convert with FromSystemNumerics and ToSystemNumerics, which transpose. Both directions round-trip exactly.

Vectors and quaternions convert with the same pair of methods; quaternions keep their components and their Hamilton product. Converting from double to float is only the explicit ToSystemNumerics() and rounds to nearest even.

Good to know

Both libraries give -11 here. The difference is the promise: OneMath’s results are the same bits on every OS, CPU and runtime, and System.Numerics makes no such promise. Keep System.Numerics for rendering if you like, and use OneMath for whatever the simulation, a replay or a lockstep peer has to agree on.