ferreiradaselva

MathC - a C math library for 2D and 3D programming

Recommended Posts

I was in doubt whether to post this on Programming -> Math and Physics or in Your Announcements. Since it's a project, I will leave here. 

As I was writing my C game engine, I decided to decouple the math implementation and make its own library.

It has implementation for 2D vectors, 3D vectors, 4D vectors, quaternions and matrices.

It's mainly for game programming.

Github: https://github.com/ferreiradaselva/mathc

Feedback is appreciated, too. Especially on the math aspects (lacking functions, math errors), since I don't have a particularly professional background with math.

Edited by ferreiradaselva

Share this post


Link to post
Share on other sites

I havent had time to look through it thoroughly, but :

There's no point in declaring an input parameter const if it's being passed by value:

cvector2 vector2_add(const cvector2 a, const cvector2 b);

You should just either declare it without the const, or declare it as a const reference:

cvector2 vector2_add(cvector2 a, cvector2 b);

or

cvector2 vector2_add(const cvector2& a, const cvector2& b);

 

Share this post


Link to post
Share on other sites
2 minutes ago, ferreiradaselva said:

Is passing by reference a thing in C? I'm trying to keep it usable from C.

As for the const, that's a good point. Thanks!

Oh... in that case, then no.  I didnt realize you're trying to stick to pure C.

Is there a reason for that decision?  

Share this post


Link to post
Share on other sites
1 minute ago, ferreiradaselva said:

I just like it more. The whole engine is in C. I wouldn't like to have to use other compiler only because of one part of the library.

You wouldnt need to use two different compilers just because some parts use C++ features and others dont.

I'd suggest reconsidering not using C++.  Especially for math, and especially for math in games, this C style can make for some absolutely unreadable and tedious code.  There's many parts of a game like physics or AI where you'll be doing a lot of math and the more natural and readable the code the better... not just to write, but to read/debug/maintain later.

For example, here's just one line of physics code that I dug up:

// Pre-collision relative velocity at point of contact
v3 vr = (v1 + w1.cross(r1)) - (v2 + w2.cross(r2));

If I rewrite it with your math library it'd be like this:

cvector3 temp1 = vector3_cross(w1, r1);
cvector3 temp2 = vector3_cross(w2, r2);
temp1 = vector3_add(v1, temp1);
temp2 = vector3_add(v2, temp2);
cvector3 vr = vector3_sub(temp1, temp2);

And that's just one line, from a function that has around 60 lines of math code like that.

Share this post


Link to post
Share on other sites
30 minutes ago, 0r0d said:

You wouldnt need to use two different compilers just because some parts use C++ features and others dont.

With Visual Studio, probably I wouldn't, but I use gcc (for Linux) and mingw (for Windows).

30 minutes ago, 0r0d said:

I'd suggest reconsidering not using C++.  Especially for math, and especially for math in games, this C style can make for some absolutely unreadable and tedious code.  There's many parts of a game like physics or AI where you'll be doing a lot of math and the more natural and readable the code the better... not just to write, but to read/debug/maintain later.

I get it that some people prefer C++ and find it more readable, but I prefer C (and I know there are people there that prefer C, too). (before programming only in C, I programmed in C++, too) I don't want to make this post a C vs C++, bc there's already a bunch on that in this forum.

 

The math part is kinda the one I'm more concerned to improve, tbh, as I don't have a great math background (I should've added that in the first post). I made this library checking codes from MonoGame and linmath. Linmath is in C, too, but missing some features that I added there (especially on quaternions and matrices).

Edited by ferreiradaselva

Share this post


Link to post
Share on other sites
2 minutes ago, ferreiradaselva said:

With Visual Studio, probably I wouldn't, but I use gcc (for Linux) and mingw (for Windows).

28 minutes ago, 0r0d said:

GCC can do this to.

2 minutes ago, ferreiradaselva said:

I get it that some people prefer C++ and find it more readable, but I prefer C

One thing to consider is offering optional extra headers that wrap the C stuff you've written in operator overloads that let C++ users avoid some of the more onerous aspects of having to type out "vector3_add" all the time. This is generally a reasonable compromise between keeping the core ideology of your library ("it's for C") and getting more users who might be able to provide you useful feedback on the inner workings of the library (or find bugs). Generally the surface area involved in C++ overload shims is small relative to the rest of the library -- although in your case, less so, since the library itself is still pretty small -- so you don't pay much more continued upkeep for it and most of the cost is in the initial implementation.

5 minutes ago, ferreiradaselva said:

The math part is kinda the one I'm more concerned to improve, tbh, as I don't have a great math background. I made this library checking codes from MonoGame and linmath. Linmath is in C, too, but missing some features that I added there (especially on quaternions and matrices).

Consider adding tests. This also works towards making your library more approachable (people can see there is a verification framework), and it tells you when you make a change that accidentally breaks stuff somehow. It also serves as a kind of documentation (although again, your library is still small and straightforward enough you don't really need that).

And it serves as a way to prove to yourself you're doing it right, especially when you're adding functionality for orthogonality (that is, because you think it's a function you should have since you have similar ones, not because you're actively needing it now yourself): you can verify your tests are correct by running them against another library that you know to be correct, if you're not comfortable working out and verifying the math by hand. Or you're too lazy to, like I am.

More generally, you might benefit from either providing some one-line comments above certain functions, or making their names slightly longer to make them more explicit in their purpose. There's a handful of them (like vectorX_angle, vectorX_min, vectorX_max, some rotation stuff) where the semantics aren't immediately obvious, or where absent any other information I can guess that they might do one or two potentially different things.

You may also want to consider taking the appropriate steps to ensure any particular alignment or padding guarantees, or at least document that you expect somebody else to do that via the appropriate magics before including your header.

Share this post


Link to post
Share on other sites
2 minutes ago, jpetrie said:

One thing to consider is offering optional extra headers that wrap the C stuff you've written in operator overloads that let C++ users avoid some of the more onerous aspects of having to type out "vector3_add" all the time. This is generally a reasonable compromise between keeping the core ideology of your library ("it's for C") and getting more users who might be able to provide you useful feedback on the inner workings of the library (or find bugs). Generally the surface area involved in C++ overload shims is small relative to the rest of the library -- although in your case, less so, since the library itself is still pretty small -- so you don't pay much more continued upkeep for it and most of the cost is in the initial implementation.

I can get behind that :thumbsup:

4 minutes ago, jpetrie said:

Consider adding tests. This also works towards making your library more approachable (people can see there is a verification framework), and it tells you when you make a change that accidentally breaks stuff somehow. It also serves as a kind of documentation (although again, your library is still small and straightforward enough you don't really need that).

And it serves as a way to prove to yourself you're doing it right, especially when you're adding functionality for orthogonality (that is, because you think it's a function you should have since you have similar ones, not because you're actively needing it now yourself): you can verify your tests are correct by running them against another library that you know to be correct, if you're not comfortable working out and verifying the math by hand. Or you're too lazy to, like I am.

Those are good ideas! I have some code already with my own game, it's a matter of copy-paste.

5 minutes ago, jpetrie said:

More generally, you might benefit from either providing some one-line comments above certain functions, or making their names slightly longer to make them more explicit in their purpose. There's a handful of them (like vectorX_angle, vectorX_min, vectorX_max, some rotation stuff) where the semantics aren't immediately obvious, or where absent any other information I can guess that they might do one or two potentially different things.

Good points. Adding some prepositions (as "from") will probably make the name of some functions more explicit.

7 minutes ago, jpetrie said:

You may also want to consider taking the appropriate steps to ensure any particular alignment or padding guarantees, or at least document that you expect somebody else to do that via the appropriate magics before including your header.

Hmm, that's a good feature. I suppose the main use would be being able to use the structs as if they were arrays? Like this:

cmatrix m = matrix_ortho(...);
float *v = &m;

/* Can edit as an array */
v[0] = 0.5f;

/* Or send to the GPU */
glUniform4fv(view_loc, 16, v);

 

 

Share this post


Link to post
Share on other sites
1 hour ago, ferreiradaselva said:

With Visual Studio, probably I wouldn't, but I use gcc (for Linux) and mingw (for Windows).

I get it that some people prefer C++ and find it more readable, but I prefer C (and I know there are people there that prefer C, too). (before programming only in C, I programmed in C++, too) I don't want to make this post a C vs C++, bc there's already a bunch on that in this forum.

 

The math part is kinda the one I'm more concerned to improve, tbh, as I don't have a great math background (I should've added that in the first post). I made this library checking codes from MonoGame and linmath. Linmath is in C, too, but missing some features that I added there (especially on quaternions and matrices).

Well, I totally get if you prefer the C style of coding over some of the C++ way of doing things.  So, if that's what you want then go for it.  But, my comments were not about C++ over C, they were about writing math code in math-heavy applications such as physics, or AI, or animation.  Having had to write a lot of that code, what I told you is the best advice I can give you.   You can have small code that closely resembles the mathematics your encoding, or you can have something that doesnt.  Considering you're already using a compiler that will compile both C and C++ (gcc compiles both very well), the only obstacle is really your personal preference.  And like I said, that's fine... especially if you're the only one who's every going to look at or work with that code.  In that situation, whatever makes you happy is the most important thing. 

On the other hand, if anyone else is ever going to work with your code, then you need to consider other things than your current personal preferences.  And I can guarantee you, anyone who has to come in and do any kind of math heavy lifting with this math library will not be a happy camper, unless they enjoy this type of code as much as you do.  I really doubt many people would consider 5 lines of code more readable than 1 line, especially when that 1 line closely matches the underlying mathematical equation that the code is executing.  And I doubt you will either in a few years if you have to do a lot of math coding.  

Share this post


Link to post
Share on other sites

I have just looked over the api and it seem pretty reasonable, but here are some tips:

- The naming of the structs are too long (cvector2? cvec2 or even vec2 are much better)

- If you want to convince people to use your library or given proper feedback you better add C++ operator overloading and namespace support. Through ifdef you can exclude that features from native C if needed
- There are no swizzling, array, or union support (This so much more usable):

- There are no vec2i (Its not always float, sometimes there is even a double vec4d or mat4d)

- No SIMD (SSE/SSE2) implementation (This improves performance a lot, especially for mat4 operations)

 

Example of a better struct definition:

union vec2i {
	struct {
		int x, y;
	};
	struct {
		int w, h;
	};
	int m[2];
};

union vec2f {
	struct {
		float x, y;
	};
	struct {
		float w, h;
	};
	float m[2];
};

union vec4f {
	struct {
		union {
			vec3f xyz;
			struct {
				float x, y, z;
			};
		};
		float w;
	};
	struct {
		union {
			vec3f rgb;
			struct {
				float r, g, b;
			};
		};
		float a;
	};
	struct {
		vec3f xyz;
		float w;
	};
	struct {
		vec2f xy;
		float ignored0;
		float ignored1;
	};
	struct {
		float ignored2;
		vec2f yz;
		float ignored3;
	};
	struct {
		float ignored4;
		float ignored5;
		vec2f zw;
	};
	float m[4];
};

union mat4f {
	struct {
		vec4f col1;
		vec4f col2;
		vec4f col3;
		vec4f col4;
	};
	float m[16];
};

 

Edited by Finalspace

Share this post


Link to post
Share on other sites

Few seldom things (that might have already been said above):

Consider passing parameters by pointers for most of your operations involving vectors, matrices or quaternions. This will be far more fast. Make them const if necessary.

Consider using an array for your structures. Well, at least if in C (which I'm not sure about), alignment is not mandatory for structures members.

Consider splitting your file. If you create something for 2D and 3D games, then one that would like to make 2D games only does not care about vector3 for example.

 

5 hours ago, ferreiradaselva said:

Especially on the math aspects (lacking functions, math errors)

For lacking functions, depending on what you want, many may be lacking :) I suggest you to add them as you need them, or as your potential users will require them.

Share this post


Link to post
Share on other sites
7 hours ago, Finalspace said:

- The naming of the structs are too long (cvector2? cvec2 or even vec2 are much better)

Not sure if the "tor" add too much to it. But I will keep an open mind about it.

7 hours ago, Finalspace said:

- If you want to convince people to use your library or given proper feedback you better add C++ operator overloading and namespace support. Through ifdef you can exclude that features from native C if needed

I will make a wrap for c++, probably on a different header.

7 hours ago, Finalspace said:

- There are no swizzling, array, or union support (This so much more usable):

That's a very nice change, and I will work on it soon.

7 hours ago, Finalspace said:

- There are no vec2i (Its not always float, sometimes there is even a double vec4d or mat4d)

I will add the integer implementations. Not sure about the double, though.

7 hours ago, Finalspace said:

- No SIMD (SSE/SSE2) implementation (This improves performance a lot, especially for mat4 operations)

I was aware about SIMD, but it's still something I never worked with, so that one will probably take some time to implement. Still on the list, tho.

6 hours ago, _Silence_ said:

Consider passing parameters by pointers for most of your operations involving vectors, matrices or quaternions. This will be far more fast. Make them const if necessary.

This is a good thing (and was requested on other forum). I will add functions that take the parameters as pointers. Probably adding a `p` prefix will suffice to determine they are for pointers.

I want to keep the functions that take them as value (affect performance, but quick to work with).

6 hours ago, _Silence_ said:

Consider using an array for your structures. Well, at least if in C (which I'm not sure about), alignment is not mandatory for structures members.

This is similar with the union mentioned above. I will work on it.

6 hours ago, _Silence_ said:

Consider splitting your file. If you create something for 2D and 3D games, then one that would like to make 2D games only does not care about vector3 for example.

I will probably make add an ifdef/endif with macros like VEC2_IMPL, VEC3_IMPL, etc. Just to keep it only two files (easier to add to a project).

 

Everyone, thanks for the feedback! :D

Edited by ferreiradaselva

Share this post


Link to post
Share on other sites

My lib was trending on github yesterday (on the C category, at least)! :D

I decided to not to make a C++ binding. It doesn't make sense. There's already GLM for that.

I have added functions that take pointers, and still giving support to the functions that take the structures as pointers:

void pvector3_multiply(cvector3 *a, cvector3 *b, cvector3 *result)
{
	result->x = a->x * b->x;
	result->y = a->y * b->y;
	result->z = a->z * b->z;
}

MATHC_EXTERN_INLINE cvector3 vector3_multiply(cvector3 a, cvector3 b)
{
	cvector3 result;
	pvector3_multiply(&a, &b, &result);
	return result;
}

All the math happens in the functions that take pointers, that way I don't need to rewrite two versions if something needs to change.

Also, I made a quaternion slerp function today. I still need to compare with other engines, but it gives the same results with Blender, but not with Godot. Since the Godot implementation works (according with some youtube videos), I imagine the slerp function is different to satisfy something with the other Godot math functions. 

Share this post


Link to post
Share on other sites
On 10/5/2017 at 3:39 AM, 0r0d said:

There's no point in declaring an input parameter const if it's being passed by value:

Technically, you can re-assign the passed parameters to a function in C/C++.

I wonder if one could actually obtain a performance benefit by adding the "const" since in 99.99% of the cases you don't modify the passed parameters?

Share this post


Link to post
Share on other sites
5 hours ago, matt77hias said:

Technically, you can re-assign the passed parameters to a function in C/C++.

I wonder if one could actually obtain a performance benefit by adding the "const" since in 99.99% of the cases you don't modify the passed parameters?

Those were the reason I used 'const', BUT apparently there's not performance benefit from using 'const'. I couldn't find any difference in the assembly output between the version with 'const' and the version without it.

Share this post


Link to post
Share on other sites
1 minute ago, ferreiradaselva said:

Those were the reason I used 'const', BUT apparently there's not performance benefit from using 'const'. I couldn't find any difference in the assembly output between the version with 'const' and the version without it.

Thanks for letting me know! (If someone claimed the opposite, I would become crazy changing all my code bases) :)

Share this post


Link to post
Share on other sites

So to get to your library:

You have no different vector types, you have just one "vec" structure which is 32 bytes long, but you have functions which operates on 2D and 3D and 4D data only. So this will waste memory when you use 2D operations only.

Really there should be a vec2, vec3 and a vec4 and even quaternion should be a seperate quaternion structure. For matrices its the same, mat2, mat3 and mat4. Access them by an array and wrap them to union makes that possible without problems.

 

Now to const:

Const or const reference has no difference for generated code vs pointers - so no difference in performance.

But it makes the documentation / api of your functions more clear, which is input and which is output.

From what i have read about consts and seen in practice, it will just produce compiler errors when you try to modify something which is marked as const. And this gets really persistent in class methods. It forces you to use const in all places.

The best explanation about const i found so far (..."Its kind of a fake keyword"...):

 

Share this post


Link to post
Share on other sites
1 hour ago, Finalspace said:

 

From what i have read about consts and seen in practice, it will just produce compiler errors when you try to modify something which is marked as const. And this gets really persistent in class methods. It forces you to use const in all places.

 

Well, when you specify something as "const", as with function input parameters, you're defining your contract with the user of that function.  If you find yourself trying to modify something that's const, you either screwed up or the person who defined that API screwed up.  Getting rid of the const is not the answer because that just hides the fact that there's some lazy programming going on.  Now, it's true that if the laziness is pervasive throughout the codebase, then you will find yourself having to go around modifying a bunch of stuff... and some might be difficult because of said laziness in the coding.   So, of course, it's up to you on how to proceed.  My personal approach is to make sure the entire codebase is const-correct as I write it and the problem never arises.  It's not hard, it's just a matter of thinking about what a function/variable does when you write it, and then declare it appropriately.  If some assumption changes, then I change the API and everyone is happy.  This pays off later in better code, fewer bugs or bugs caught earlier, and less headaches.  You also give the compiler the option to generate more optimal code.  It might not in most cases, but it could in certain specific situations.

More information on const correctness:

https://isocpp.org/wiki/faq/const-correctness

Share this post


Link to post
Share on other sites
1 hour ago, 0r0d said:

Well, when you specify something as "const", as with function input parameters, you're defining your contract with the user of that function.  If you find yourself trying to modify something that's const, you either screwed up or the person who defined that API screwed up.  Getting rid of the const is not the answer because that just hides the fact that there's some lazy programming going on.  Now, it's true that if the laziness is pervasive throughout the codebase, then you will find yourself having to go around modifying a bunch of stuff... and some might be difficult because of said laziness in the coding.   So, of course, it's up to you on how to proceed.  My personal approach is to make sure the entire codebase is const-correct as I write it and the problem never arises.  It's not hard, it's just a matter of thinking about what a function/variable does when you write it, and then declare it appropriately.  If some assumption changes, then I change the API and everyone is happy.  This pays off later in better code, fewer bugs or bugs caught earlier, and less headaches.  You also give the compiler the option to generate more optimal code.  It might not in most cases, but it could in certain specific situations.

More information on const correctness:

https://isocpp.org/wiki/faq/const-correctness

I never got in a situation where const helped me to make better code. I know what i want to modify and what i dont want to modify. In 20 years of programming i never got a single case, where the compiler helped me just because i tried to modify unmutable data.

But i had million errors using someones api which are based on const-correctness, getting it to work with my code base - creating a ton of useless friction.

I see const just as a way to differentiate between input and output parameters, but nothing more. Everything else is useless friction for me.

But i dont doubt that it may catch some bugs for lazy programmers and i agree that it makes api´s more clean.

Edited by Finalspace

Share this post


Link to post
Share on other sites
7 hours ago, Finalspace said:

So to get to your library:

You have no different vector types, you have just one "vec" structure which is 32 bytes long, but you have functions which operates on 2D and 3D and 4D data only. So this will waste memory when you use 2D operations only.

Really there should be a vec2, vec3 and a vec4 and even quaternion should be a seperate quaternion structure. For matrices its the same, mat2, mat3 and mat4. Access them by an array and wrap them to union makes that possible without problems.

At first, there was a vector structure for each type (2d, 3d and 4d). But, in the practice the only wasted space is for a single `float`, the W component. Z vector is still very useful for 2D rendering with OpenGL, anyway.

7 hours ago, Finalspace said:

Const or const reference has no difference for generated code vs pointers - so no difference in performance.

But it makes the documentation / api of your functions more clear, which is input and which is output.

5 hours ago, 0r0d said:

Well, when you specify something as "const", as with function input parameters, you're defining your contract with the user of that function.

As for the const, I agree that it makes more obvious what is modified and what is not for the user. But, just like @Finalspace, I've had problems with integrating libraries from other people that had const (also no optimization, which led me to remove it):

4 hours ago, Finalspace said:

But i had million errors using someones api which are based on const-correctness, getting it to work with my code base - creating a ton of useless friction.

 

Share this post


Link to post
Share on other sites
8 hours ago, ferreiradaselva said:

As for the const, I agree that it makes more obvious what is modified and what is not for the user. But, just like @Finalspace, I've had problems with integrating libraries from other people that had const (also no optimization, which led me to remove it):

 

Then the problem was either that the library was using const inappropriately or you were.  Like I said, the keyword is there partly to help enforce the contract that the API defines between itself and the user.  Of course if you're combining const-correct code with code that isnt, it will cause all sorts of problems.  This is expected.  The solution is to modify the non-const-correct code to be const-correct.  From experience, when you start doing this you realize that each of those compile errors is almost certainly pointing you to some design issue with your code.  Once your code is const-correct it makes things much easier going forward.  It's easier to look at functions and be able to tell what it can or cant do with those inputs.  It helps with variables not accidentally being modified later when you actually intended them to be immutable.  And, it helps you find design/architectural issues early because the compiler tells you exactly when you're about to do something you shouldnt do.

The way I think of const-correctness is like having a clean organized house, rather than a really messy one.  Sure, you can live in a messy house for 20 years and be happy with it, thinking that you've never seen a situation where being organized would have helped you.  But, since you were never organized in the first place, you dont really know.  You've just adapted to a mess and you work around all the problems without even realizing that they're problems.  And, trying to organize at first just seems to create more problems for no real gain.  However, if you have an organized house then the benefits are apparent and you would not want to go back to a huge mess.

Edited by 0r0d

Share this post


Link to post
Share on other sites
7 hours ago, 0r0d said:

Then the problem was either that the library was using const inappropriately or you were.  Like I said, the keyword is there partly to help enforce the contract that the API defines between itself and the user.  Of course if you're combining const-correct code with code that isnt, it will cause all sorts of problems.  This is expected.  The solution is to modify the non-const-correct code to be const-correct.  From experience, when you start doing this you realize that each of those compile errors is almost certainly pointing you to some design issue with your code.  Once your code is const-correct it makes things much easier going forward.  It's easier to look at functions and be able to tell what it can or cant do with those inputs.  It helps with variables not accidentally being modified later when you actually intended them to be immutable.  And, it helps you find design/architectural issues early because the compiler tells you exactly when you're about to do something you shouldnt do.

The way I think of const-correctness is like having a clean organized house, rather than a really messy one.  Sure, you can live in a messy house for 20 years and be happy with it, thinking that you've never seen a situation where being organized would have helped you.  But, since you were never organized in the first place, you dont really know.  You've just adapted to a mess and you work around all the problems without even realizing that they're problems.  And, trying to organize at first just seems to create more problems for no real gain.  However, if you have an organized house then the benefits are apparent and you would not want to go back to a huge mess.

You can write messy code with const correctness as well - but well we should not discuss this any further, since it have nothing todo with the MathC library. But like i already sayd const are useful, but its not a magic wand for finding all your bugs.

7 hours ago, 0r0d said:

 

Not again, this is get overused so much... -.-

Edited by Finalspace

Share this post


Link to post
Share on other sites