george's picture

Math module?

On the source force page, it's mentioned that OpenTK contains a maths module. Is this accurate? I don't see it anywhere.


Comments

Comment viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.
Inertia's picture

Couldn't produce any compiler or runtime errors, also the Documentation is rather complete. Good job :)

Only minor things:

6) Quaternion.ToAxisAngle documentation does not explicitly state that it returns the angle in radians or degrees.

7) Quaternion.Slerp() blend Parameter would be nicer if named WeightQ2 or similar to make it more obvious which given Quaternion is weighted by the given float. Same applies for Vector3.Lerp() although the Documentation is more clear about it there.

8) It would be useful if "public static Vector4 Transform(Vector3 vec, Matrix4 mat)" in Vector3.cs had an overload that returns a Vector3 (if you know your Matrix doesn't contain any perspective, you can skip calculating 1 Dot-Product and 1 Conversion to Vector3)

8.b) Also nice would be overloading the * operator so you can write something like VectorResult = MatrixA * VectorB;

9) Is there any special reason why the structs don't use the IComparable Interface and don't contain any other helpers to compare their values?

9.b) Assuming comparisons will be added, the Matrix4 struct could contain 3 static boolean Methods (ContainsTranslation, ContainsRotation, ContainsPerspective) which return true if the respective values in the given Matrix differ from Matrix4.Identity.

Edit: 10) Would it be a problem to change the order of the Quaternion's Fields to this?

public float W;
public Vector3 XYZ;

This is afaik the more commonly used way to write a Quaternion.

the Fiddler.'s picture

Couldn't produce any compiler or runtime errors, also the documentation is rather complete. Good job :)
Thanks go to george who did all the hard work :)

8) Cannot overload on return type alone (stupid C++ legacy), but could be added as a different function.

9) Working on that.

9.b) Never thought of that before, interesting!

objarni's picture

I wrote a simple test-program to measure the performance of a struct-vector versus a class-vector:

int times = 100000000;
VClass v = new VClass(3, 1, 2);
VClass w = new VClass(0, 0, 0);
for (int i = 0; i < times; i++)
{
  w = w + v;
}

versus

int times = 100000000;
VStruct v = new VStruct(3, 1, 2);
VStruct w = new VStruct(0, 0, 0);
for (int i = 0; i < times; i++)
{
  VStruct.Add(ref v, ref w, out w);
}

This gave me approximately 2,0 seconds running time for the first loop and 0,7 seconds on the second. (Release build on a WinXp AMD Athlon 64 Dual Core 3800+ machine, experiment repeated like 5 times for each loop)

So there is quite a difference! Structs is the way to go, even though the syntax isn't that good-looking! :)

A third variation I tried was using operator + on the struct:

int times = 100000000;
VStruct v = new VStruct(3, 1, 2);
VStruct w = new VStruct(0, 0, 0);
for (int i = 0; i < times; i++)
{
  w = w + v;
}

This gave me a running time of 3,3 seconds! The most expensive alternative..

Inertia's picture

Thanks george :)

8) Well the * operator overload could behave like that. I can only speak for myself, but my Matrix operations are usually in world- or local-space and don't contain any perspective.

9.b) I haven't needed that besides for skeletal animation either. It makes sense for optimizations, because if you notice that your matrices only contain rotations OR translations you can use the Quaternion OR Vector methods instead of Matrix methods. (typically a bone's length will not change)

george's picture

I actually just added a Vector3.TransformPerspective function which behaves as you describe, ie, it divides through by w and returns a Vector3.
It's waiting to be commited afaik.
I didn't add the operator overload because it's not clear which transform it should do, ie does the Vector3 contain a position, direction or normal?

I agree, the ContainsTranslation/Scale etc functions would be useful and they're easy to add, be my guest :)

the Fiddler.'s picture

Oops yeah, haha! I was busy with the font stuff I´ll go through that patch today.

Inertia's picture

@objarni: Sorry I didn't see your reply earlier, this thread is getting kinda weird with those tree-like replies. The way I understood you, you asked for Vectors that after created by a constructor are read-only (==immutable) and my reply was based on that.

Not sure if P/Invoke the operations from C is actually faster than doing the implementation in C#, because of overhead. Have you actually tested if those 10% exist, or is it just a Pi*Thumb number?

I see your concern regarding very complex projects, but I think that falls into the Programmers responsibility to communicate/document code properly and not into the math libraries' responsibility to work around that issue. The benefit of speed outweights possible user errors imho.

If you do v += u; I don't see how you could get rid of the copy operation :)
Not sure if immutablitity is the future for all types (it sure is for strings). One could get rid of the copy with X.Add(Y), assuming X is mutable.

premature optimization is the root of all evil
That's something you should imho never take as the absolute truth, until you have evaluated how premature is defined in the respective case. For abstract solution for an abstract problem that rule surely applies, but the scope of this library is very clear defined.

Don't get this wrong, I respect your opinion and the issues you reported regarding your projects sure are worth consideration.

@george:
It's only syntactic sugar anyways.

I agree, the ContainsTranslation/Scale etc functions would be useful and they're easy to add, be my guest :)
Edit:

Functions.cs

private static float _FloatComparisonThreshold = 0.000001f;
        /// <summary>
        /// Sets or Gets the Threshold used when comparing single-precision float with <see cref="Functions.IsNearlyEqual"/>
        /// </summary>
        public static float FloatComparisonThreshold
        {
            get { return _FloatComparisonThreshold; }
            set { _FloatComparisonThreshold = value; }
        }
 
        /// <summary>
        /// Compares two single-precision floating point numbers for being almost equal. The Threshold used can be set by <see cref="Functions.FloatComparisonThreshold"/>
        /// </summary>
        /// <param name="f1">The first float value to examine.</param>
        /// <param name="f2">The second float value to examine.</param>
        /// <returns>true if the parameters difference is smaller than <see cref="Functions.FloatComparisonThreshold"/></returns>
        public static bool IsNearlyEqual( float f1, float f2 )
        {
            return System.Math.Abs( f1 - f2 ) <= _FloatComparisonThreshold;
        }

Basically just the refactored&documented version of what i posted earlier.

and Matrix4.cs

/// <summary>
        /// Examines the gives Matrix4 whether it contains Translation or not.
        /// </summary>
        /// <param name="mat">The Matrix4 to examine.</param>
        /// <returns>true if Translation is present, false if not.</returns>
        public static bool ContainsTranslation( ref Matrix4 mat )
        {
            return !Functions.IsNearlyEqual( mat.Row3.X, 0f ) ||
                   !Functions.IsNearlyEqual( mat.Row3.Y, 0f ) ||
                   !Functions.IsNearlyEqual( mat.Row3.Z, 0f );
        }
 
        /// <summary>
        /// Examines the gives Matrix4 whether it contains Projection or not.
        /// </summary>
        /// <param name="mat">The Matrix4 to examine.</param>
        /// <returns>true if Projection is present, false if not.</returns>
        public static bool ContainsProjection( ref Matrix4 mat )
        {
            return !Functions.IsNearlyEqual( mat.Row0.W, 0f ) ||
                   !Functions.IsNearlyEqual( mat.Row1.W, 0f ) ||
                   !Functions.IsNearlyEqual( mat.Row2.W, 0f ) ||
                   !Functions.IsNearlyEqual( mat.Row3.W, 1f );
        }
 
        /// <summary>
        /// Examines the gives Matrix4 whether it contains Rotation or not.
        /// </summary>
        /// <param name="mat">The Matrix4 to examine.</param>
        /// <returns>true if Rotation is present, false if not.</returns>
        public static bool ContainsRotation( ref Matrix4 mat )
        {
            return !Functions.IsNearlyEqual( mat.Row0.X, 1f ) ||
                   !Functions.IsNearlyEqual( mat.Row0.Y, 0f ) ||
                   !Functions.IsNearlyEqual( mat.Row0.Z, 0f ) ||
                   !Functions.IsNearlyEqual( mat.Row1.X, 0f ) ||
                   !Functions.IsNearlyEqual( mat.Row1.Y, 1f ) ||
                   !Functions.IsNearlyEqual( mat.Row1.Z, 0f ) ||
                   !Functions.IsNearlyEqual( mat.Row2.X, 0f ) ||
                   !Functions.IsNearlyEqual( mat.Row2.Y, 0f ) ||
                   !Functions.IsNearlyEqual( mat.Row2.Z, 1f );
        }

It would be wise to check if i didn't mix anything up. I didn't build documentation, hope this is correct.

objarni's picture

@objarni: Sorry I didn't see your reply earlier, this thread is getting kinda weird with those tree-like replies.

Yeah it's kinda hard to see what is new and what is old! Would be nice with some visual que on new messages..


Not sure if P/Invoke the operations from C is actually faster than doing the implementation in C#, because of overhead. Have you actually tested if those 10% exist, or is it just a Pi*Thumb number?

Well what I meant with using C was that if you really are so concerd with writing the absolutely-fastest-possible-code, you really should do your whole project in C/Assembler. And I'm quite sure P/Invoke would be slower than having the vectors directly in the mathlib.


I see your concern regarding very complex projects, but I think that falls into the Programmers responsibility to communicate/document code properly and not into the math libraries' responsibility to work around that issue. The benefit of speed outweights possible user errors imho.

My "mental model" of a vector is that of a mathematical entity, just as numbers in general. And since numbers don't "change", neither should vectors. That is why I'd like them to be immutable. As a bonus, they behave better in big projects!

My state of mind when I code in C# is "think of the algorithmics more than the low-level-tricks". That is, develop smart algorithms, don't rely on facts that were true in hand-optimized assembler code during the 80's or 90's. So I try to have an open mind and performance test things if I want to fine tune algorithms.

In any case, I did do a performance test and posted the result somewhere in the big thread ;). Mutable structs with reference parameters is the fastest add-operation, on second place (immutable) class-vectors (which are reference parameters automatically) and slowest was structs without reference parameters.

Happy coding,

Inertia's picture

@objarni:
...your whole project in C/Assembler
True. I didn't really see this as an option, because I really like that .NET/Mono doesn't care what OS it runs on. Imho you must be either masochist or guru-level programmer (preferably both) to develop in C, not to mention that all the low-level hacking costs time you could have spent developing other useful things.

"mental model"
Mhmm that explains alot, basically like writing them down on a sheet of paper and having no eraser around. I like to think of them as a collection of variables that describe a target in a coordinate system (well, that is the best i can do to describe it with words).

I've seen those comparisons, but none of them included a simple X.Add(Y) or X.Add(ref Y) as something to compare against. Although the tests were trying to measure the cost of creating a new instance, it would be nice to have that as a base value.

@george/Fiddler:
I realize you are busy getting the new build done this weekend, still some feedback if the functions need any change would be useful. The Threshold could be set by default to float.Epsilon ofcourse (kinda expected someone mentioning that), but I've found the current value more usable, because of the precision errors when parsing floats from strings. The scaling you are working with matters here ofcourse, so maybe the threshold should be set to a lower value in case someone wants to represent 1km with 1.0f. Not so sure about this though, the threshold i set works well for me.

the Fiddler.'s picture

I've seen those comparisons, but none of them included a simple X.Add(Y) or X.Add(ref Y) as something to compare against. Although the tests were trying to measure the cost of creating a new instance, it would be nice to have that as a base value.
Vector[234].Add(ref, ref, out) is (almost) equivalent to to X.Add(ref), unless there is some hidden cost with parameter passing/instance calling that I'm not aware of. In the current implementation, no function allocates memory. The performance delta comes from parameter passing/data copying.

We (george and me) were debating whether instance methods would be useful and concluded that they would only clatter the namespace (two ways of doing the same thing). There won't be any instance methods in this release, but we can always add them if there is demand.

I realize you are busy getting the new build done this weekend, still some feedback if the functions need any change would be useful. The Threshold could be set by default to float.Epsilon ofcourse (kinda expected someone mentioning that), but I've found the current value more usable, because of the precision errors when parsing floats from strings. The scaling you are working with matters here ofcourse, so maybe the threshold should be set to a lower value in case someone wants to represent 1km with 1.0f. Not so sure about this though, the threshold i set works well for me.
AFAIK Float.Epsilon (or was it Single.Epsilon?) represents the smallest possible delta between two consequtive floats. Problem is, the actual epsilon grows when you move closer to 0 (sub-normals) or towards bigger numbers, so using a constant for comparison deltas breaks down in these cases. A well-chose constant will work in most cases and provide the best performance, but in some cases (like the one you mentioned, 1.0f = 1km) we'll have to use the slower but more accurate integer comparison.

I have yet to implement either of these methods (or apply the latest math patch by george - too busy with assignments), but I lean towards implementing the more accurate/general method and leave the other to the user as a possible optimization. What do you think?