Ok, I ported the monogame math API (matrices, quaternions, vectors) to my engine. But, I was wondering, what's the best way to represent the object position/rotation/scale (that works for both 2D and 3D)?
Would this suffice?struct ctransform {
cvector3 pos;
cvector3 scale;
cquaternion rotation;
};
Then, in the rendering, I would make 3 matrices from those and multiply the end result.
Close. It will work. If you want improvements this is probably the best:
struct ctransform {
cvector3 pos;
cmatrix3 rotation;
};
Generally scale is not that important for physics/pathfinding, and only really important as one-off things in rendering. For instance in physics scale will be implied by the shapes in local space. For pathfinding and the like, usually there are polygons already in world space (or that just need to be translated/rotated).
Non-uniform scaling can break down nice and clean matrix hierarchies since you can get non-linear transformations. Losing linearity can be a big performance problem. To deal with this a lot of systems just tack on a uniform scaling (or sometimes non-uniform scaling) as a final processing step. This scale factor is not apart of a lower level math API, and is specific to the system using it.
The above transform I showed is an affine transformation. They can be combined in stacks to form clean linear transformations. This is the most useful abstraction for general 3D math in games, in my experience.
Don't store a quaternion since most rendering using rotation matrices (like the GPU). Storing a and converting a quaternion is super annoying. The most common use case of a rotation matrix is to do a matrix vector product, something that is kind of awkward to perform with a raw quaternion.
Finally, write some Mul functions.
ctransform Mul( ctransform tx, cvector3 v );
ctransform Mul( cvector3 v, ctransform tx );
ctransform MulT( ctransform tx, cvector3 v );
ctransform MulT( cvector3 v, ctransform tx );
The reason Mul functions like these are nice is they can internally respect a consistent product orientation (column/row major notation), but allow the user to perform math however they are most comfortable. In the end it has no performance penalty. The T stands for transpose and can perform an optimized lower-level primitive to do Matrix^T * Vector transformation without performing an explicit transpose operation.
These kinds of functions play very nicely with SIMD APIs in both 2D and 3D, since new kinds of primitives can be added as needed. Make some more overloads for Mul that can transform planes, triangles, quads, OBBs, or other implicitly defined shapes (like spheres/capsules). Since scale is not included generic Mul functions can transform all shapes in a consistent matter (due to preserving linearity).
The nicest thing about this style is any other new primitives can be added as needed, on any platform that can run C/C++. If documented well people can come along and add in new features when needed. Like new shape types, or new kinds of functions.
Here's an example use case not possible with your struct, but possible when scale is removed:
ctransform a = GetA( );
ctransform b = GetB( );
ctransform b_in_a = MulT( a, b );
cvector3 v_in_b = GetVertexInB( );
cvector3 v_in_a = Mul( b_in_a, v_in_b );
bool hit = HitTestInA( GetAGeometry( ), v_in_a );
if ( hit ) { do stuff ... }
The code gets the reference frame of two objects A and B. A vertex from B is transformed into A and tested on the geometry defined local to A. It is important to do this test local to A, since converting a single point from space B to A is much cheaper than transforming all of A's geometry into a new space.
GetA( ) can look like this inside:
ctransform GetA( )
{
return Mul( a, Mul( b, Mul( c, d ) ) );
}
Since linearity is preserved, stacks of matrices can be joined to represent new reference frames arbitrarily. In practice GetA would probably just returned a cached matrix as the result of all those Mul functions.
Anyways, hope that helps! I'm actually implementing a bunch of this stuff at work right now since the code base lacks a consistent and well formed API.
Edit: Oh and don't make 3 unique matrices and multiply them. You should know enough about transformations to create your 4x4 matrix on the spot in one go. The upper left 3x3 sub-matrix is a rotation matrix. For scaling, scale the x, y and z columns for x y and z scaling factors. Translation goes in the upper right column, and a 1 goes in the bottom right corner. The bottom left 3 elements (the bottom row) are zeroes.
Place all the things in the right spot. Don't take an unnecessary performance hit by constructing 3 individual matrices and then multiplying them.