the.source's picture

OpenTK Mathematics (TKM)

I have written a bunch of OpenGL programs using C++ in the past and one library that I have come to like a lot is OpenGL Mathematics (GLM). The idea is to provide an (as close as possible) implementation of the math functionality of GLSL in C++. That way, it feels like you are using the same math classes/functions all across your program. I really like this idea of having a close mapping of GLSL in your core programming language. This has become even more relevant now that all the fixed function stuff is getting deprecated in the OpenGL standard. Such a library is a great place to provide this functionality.

Now, obviously OpenTK already provides some great math functionality. However, I couldn't help but miss GLM, so I started working on a project that is heavily inspired by it. Unfortunately, the code of GLM does not translate into C# very well. It heavily relies on templates and other C++ specific features. So my project doesn't share much with GLM beyond the premise of providing GLSL math capabilities in another language. It is important to mention that there are limits and certain things that are possible in GLSL may not make sense in another language, so I think that it's important to allow a certain degree of freedom when it comes to the implementation. It is not trying to be 100% compliant with the GLSL specification.

Like I said, I had to find another way of generating classes/functions. Writing them by hand was not an option as it would be impossible to maintain and the risk of introducing errors would be too great. After a bit of research I found out about CodeDOM, which looked very promising. Working with it, I discovered that it has some rough edges, so I had to "hack" together some things using CodeSnippetExpressions, but overall it worked pretty well. Right now, the project is starting to shape up nicely, but I'm running into a new problem. As I kept adding functions I noticed that shear code size has started to explode. This may or may not be a problem for the library, since luckily memory is cheap nowadays. I don't think it should affect its performance either, as all of the methods should be resolved at compile time. However, it is starting to become a problem for Visual Studio. Several hundreds of overloads don't play along very well with code assist.

For example, lets look at the dot product. If I only generate classes with component type float, there are vec{2-4} and mat{2-4}x{2-4} (not considering higher tensor ranks or dimensions, which could theoretically also be generated). So, only considering float, we have 12 distinct types. Now, we can take the dot product between each two types that share one dimension, like Dot(vec2,vec2), Dot(vec2,mat2x{2-4}), Dot(mat{2-4}x2, vec2) and Dot(mat{2-4}x2, mat2x{2-4}) . That's already 3+3*3+3*3+3*3*3=48 overloads for the dot product. If we want to generate classes with components of type unit, int, float and double, we have to multiply this by 4, leaving us with 192 overloads. Finally, if we also provide overloads that take their arguments as ref, we arrive at 384.

This is pretty much what is being produced by my code generator right now. I've basically put all those methods into static classes, corresponding to how they are grouped in the GLSL specification. I'm experiencing considerable delay on my machine when I start typing something like "Geometric.Dot(" in Visual Studio. It is clear that I'll have to change something. Splitting the methods up into several static classes would be an option, but it would require more typing (i.e. "Geometric.ivec.Dot"). Another interesting option would be to make them extension methods, but it would deviate from GLSL and some may prefer "Dot(a,b)" to "a.Dot(b)".

Anyways, I'm curious to hear your thoughts on this, as I am relatively new to C# and there may be other options that I have failed to consider. Also, I would love to hear whether there is any interest for such a project at all.

I'll leave you with a small excerpt of what is possible with the code right now.

            var v1 = new vec2(1);                           // Set components to 1, 1
            var v2 = new vec3(v1, 2);                       // Set components to 1, 1, 2
            var v3 = new vec3(2, v1);                       // Set components to 2, 1, 1
 
            v2.zyx = v2.xzz;                                // Properties that mimic swizzle operations
 
            Console.WriteLine(v2);                          // Overloaded ToString prints vec3(2,2,1)
 
            var m1 = new mat3(1);                           // Set components on diagonal to 1 (identity matrix)
 
            var m2 = Tensor.OuterProduct(v2, v3);           // Compute the outer product of v2 and v3
 
            Console.WriteLine(m2);                          // Overloaded ToString prints mat3(4,4,2,2,2,1,2,2,1)
 
            v3[2] = 0;                                      // Set the 3rd element of v3 to 0
 
            m2[0] = v3;                                     // Set the 1st column of m2 to v3
            m2[1] = 2*(v3 + v3.zyx);                        // Set the 2nd column of m2 to 2*(v3 + v3.zyx)
 
            Console.WriteLine(Matrix.Determinant(m2));      // Print the determinant of m2
 
            var v4 = Geometric.Dot(v3, m2);                 // Calculate the dot product of v3 and m2
 
            Console.WriteLine(Geometric.Distance(v3, v4));  // Print the distance between v3 and v4
 
            Console.WriteLine(Relational.All(v3 == v4));    // Print whether all components of v3 and v4 are equal
 
            var v5 = Geometric.Cross(m1[0], m1[1]);         // Calculate the cross product between the 1st and 2nd column of m1
 
            var m3 = new mat2x3(v4, v5);                    // Set 1st column to v4 and 2nd column to v5
 
            Console.WriteLine(Geometric.Dot(m3, m2));       // Print the dot product of m3 and m2

Comments

Comment viewing options

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

Extension methods might help, since a.Dot(b) should filter the number of overloads according to the type of a (note that he can still access extensions as static methods, i.e. Geometric.Dot(a, b), so there's nothing to lose).

Another solution would be to report the performance issue to CodeAssist and/or Visual Studio and hope for a fix in a future version (OpenTK used to run into performance problems all the time in the beginning. Many of those where fixed in VS2005 SP1).

I like the codegen approach very much, indeed. It's the only sane solution to cope with the amount of overloads. AFAIK Boo might have some built-in facilities to simplify this (IIRC, it has expanded generic support that get compiled into the necessary overloads statically), which might be worth a look. In the future, it might be useful to hook Mono.SIMD into the generator for some extra math performance.

the.source's picture

Thank you for your feedback! I wasn't aware that you could still use extension methods that way. It sure looks like they are a promising option. The only downside is that accessing them via "Geometric.Dot(a, b)" would probably still be slow.

For now, I have chosen a solution that required less changes. I have grouped the functions into separate classes (bmath, imath, umath, math, dmath) according to the element type of the arguments. It has reduced the number of overloads and therefore it has solved my problems with code assist in Visual Studio. However, I'm not entirely happy from a usability perspective. The problem is that it introduces a potential source of errors, because the user has to explicitly specify the element type. I would really prefer to have the compiler pick the correct overload.

The previous code now looks like this (note how it is necessary to use "bmath.All" rather than "math.All", because "All" receives a bvec/bmat as its argument):

            var v1 = new vec2(1);                           // Set components to 1, 1
            var v2 = new vec3(v1, 2);                       // Set components to 1, 1, 2
            var v3 = new vec3(2, v1);                       // Set components to 2, 1, 1
 
            v2.zyx = v2.xzz;                                // Properties that mimic swizzle operations
 
            Console.WriteLine(v2);                          // Overloaded ToString prints vec3(2,2,1)
 
            var m1 = new mat3(1);                           // Set components on diagonal to 1 (identity matrix)
 
            var m2 = math.OuterProduct(v2, v3);             // Compute the outer product of v2 and v3
 
            Console.WriteLine(m2);                          // Overloaded ToString prints mat3(4,4,2,2,2,1,2,2,1)
 
            v3[2] = 0;                                      // Set the 3rd element of v3 to 0
 
            m2[0] = v3;                                     // Set the 1st column of m2 to v3
            m2[1] = 2*(v3 + v3.zyx);                        // Set the 2nd column of m2 to 2*(v3 + v3.zyx)
 
            Console.WriteLine(math.Determinant(m2));        // Print the determinant of m2
 
            var v4 = v3 * m2;                               // Calculate the dot product of v3 and m2
 
            Console.WriteLine(math.Distance(v3, v4));       // Print the distance between v3 and v4
 
            Console.WriteLine(bmath.All(v3 == v4));         // Print whether all components of v3 and v4 are equal
 
            var v5 = math.Cross(m1[0], m1[1]);              // Calculate the cross product between the 1st and 2nd column of m1
 
            var m3 = new mat2x3(v4, v5);                    // Set 1st column to v4 and 2nd column to v5
 
            Console.WriteLine(m3 * m2);                     // Print the dot product of m3 and m2

Also, thanks for pointing out Mono.SIMD, I was wondering if it would be possible to incorporate SIMD. I'll definitely look into that.

If anybody would like to give it a try, here is a very early build (including sources) of the generated library: OpenTK Mathematics (TKM).

hannesh's picture

An idea I have is to have your Dot() function accept arguments only as float arrays.
For example float[] Dot(float[] a, float[] b).
Writing the Dot() function would be relatively trivial.
Then create implicit casts to convert your vectors to an array of floats.

You will get exactly the same as what you have there with minimal overloads.

I can imagine the GLSL compilers doing it the same way, they wouldn't write out overloads for every case.

the.source's picture

Thanks for the suggestion. That would certainly be a possibility. In fact, one could design a vector/matrix/tensor library that relies entirely on storing the elements in arrays. However, to my knowledge, its not possible to do so without impacting its performance, because the sizes would only be known at run-time. There would be no way to unroll the loops in the various functions as they are now.

martinsm's picture
hannesh wrote:

An idea I have is to have your Dot() function accept arguments only as float arrays.
For example float[] Dot(float[] a, float[] b).

This would be really awful solution in terms of performance. It would create a lot of pressure for GC during math operation.

Much better would be using unsafe code:
unsafe float Dot(int n, float* a, float* b);
You could pass to it all kind of vectors if they use [StructLayout(LayoutKind.Sequential)] attribute for structs.

the.source's picture

Over the last couple of days I've rewritten a bunch of stuff on the generator side. I put the methods back into static classes regardless of their argument types and I tried the approach using extension methods. It worked really well, but I was disappointed to find out that unlike instance methods the this parameter is passed by value, which means there is some additional copying involved for value types. Unfortunately there is no way to specify that the this parameter should be passed by reference. As a result, I have made it possible to specify whether extension methods should be generated or not, so I can easily switch between the two different approaches.

Besides adding more functions another big todo on my list is to provide wrappers for the glUniform/glUniformMatrix methods. This is where I've run into a problem, because I was planning on using OpenTKs bindings for that. However, OpenTK only exposes those methods for float arguments. Is there a reason why the integer versions are not exposed? They exist in GLDelegates.cs, but they aren't accessible.

zahirtezcan's picture

we can use text templating for these too. by declaring structs partial, management of various features can be divided. i have attached some .tt files and generated codes. if you are interested i can help templating other features too

note: actual extension for template files are .tt but site attachment system did not allowed. then i changed the extension to txt

AttachmentSize
VectorsXYZW.cs178.34 KB
VectorsXYZW.txt1.92 KB
Vectors.cs6.44 KB
Vectors.txt1.77 KB
the.source's picture

Using T4 for code generation is certainly another interesting approach. I chose CodeDOM because it seemed more intuitive for me (probably because of my familiarity with HTML DOM). Working with it, I have experienced some of its cons (CodeDom vs T4 does a good job identifying them), but overall I'm pretty happy with it. I'm not convinced that switching to T4 would be worth the effort. Currently, I'm testing the library in a small application of mine.