On Chapter 3 we covered the simplest way to display, rotate and resize sprites.
However there might be situations where more control on the sprites is needed: for example to apply custom transforms to the sprite's vertices.
On Chapter 3 we've drawn sprites only by specifying the bitmap ID, sprite position, and optionally scale, rotation and zoompoint.
Although the functions themselves are quite simple to use they do not alow for us to deform the sprite.
For this there are 2 overloads that allow for us to specify the vertices of the quad where the sprite will be mapped to. This vertices can be transformed as we wish.
static bool SRL::Scene2D::DrawSprite ( const uint16_t texture,
const SRL::Math::Types::Vector2D points[4],
const SRL::Math::Types::Fxp depth )
and
static bool SRL::Scene2D::DrawSprite ( const uint16_t texture,
SRL::CRAM::Palette * texturePalette,
const SRL::Math::Types::Vector2D points[4],
const SRL::Math::Types::Fxp depth )Note
Notice that these functions do not have rotation, scale or ZoomPoint. This is because, with these functions, you are expected to transform the sprite's edge coordinates yourself.
The use of SRL::CRAM::Palette will be covered at a late tutorial.
To start lets specify a simple quad. This Quad is where our sprite will be mapped into. Bear in mind that there are no UV coordinates: the whole sprite is fully mapped into the quad.
Vector2D points[4] = {Vector2D(0.0)};
points[0] = Vector2D(-50, -50);
points[1] = Vector2D(50, -50);
points[2] = Vector2D( 50, 50);
points[3] = Vector2D(-50, 50);
and use our loader function as specified on chapter 5:
int32_t loadTGA(char* filename)
{
SRL::Bitmap::TGA *tga = new SRL::Bitmap::TGA(filename); // Loads TGA file into main RAM
int32_t textureIndex = SRL::VDP1::TryLoadTexture(tga); // Loads TGA into VDP1
delete tga;
return textureIndex;
}Now the finished source code looks like this:
#include <srl.hpp>
// Using to shorten names for Vector and HighColor
using namespace SRL::Types;
using namespace SRL::Math::Types;
int32_t loadTGA(char* filename) //texture loading function
{
SRL::Bitmap::TGA *tga = new SRL::Bitmap::TGA(filename); // Loads TGA file into main RAM
int32_t textureIndex = SRL::VDP1::TryLoadTexture(tga); // Loads TGA into VDP1
delete tga;
return textureIndex;
}
int main()
{
// Initialize library
SRL::Core::Initialize(HighColor::Colors::Black);
SRL::Debug::Print(1,1, "06_Tutorial");
int32_t textureIndex = loadTGA("TEST.TGA"); // Loads TGA into VDP1
Vector2D points[4] = {Vector2D(0.0)};
points[0] = Vector2D(-50, -50);
points[1] = Vector2D(50, -50);
points[2] = Vector2D( 50, 50);
points[3] = Vector2D(-50, 50);
// Main program loop
while(1)
{
SRL::Scene2D::DrawSprite(textureIndex, points, 50.0 );
// Refresh screen
SRL::Core::Synchronize();
}
return 0;
}The Result:
From this, we can now start to experiment with the coordiates. Lets, for example, make the quad larger at the top:
points[0] = Vector2D(-75, -50);
points[1] = Vector2D(75, -50);
points[2] = Vector2D( 50, 50);
points[3] = Vector2D(-50, 50); The resulting quad:
We can scale, rotate and transform the points of our quad. For this, SRL provides matrices to help.
Since we are on the XY plane, we must rotate along the Z axis.
Therefore, using SRL we can first declare our matrix.
We can use the RotateZ method to apply a rotation matrix on the Z axis by the supplied angle.
We if we want the rotation matrix, we can use the CreateRotationZ method, also providing the angle we want to rotate by.
SRL also provides functions to create rotation in X and Y as well.
In SRL is really simple:
You can write
//Declare and initialize our 3x3 matrix
Matrix33 transform = Matrix33::Identity();
//Lets apply a rotation matrix to our Identity Matrix
transform.RotateZ(SRL::Math::Angle::FromDegrees(45.0));An equivalent way would be to write:
//Declare and initialize our 3x3 matrix
Matrix33 transform = Matrix33::Identity();
//Lets create a rotation matrix
transform = transform.CreateRotationZ(SRL::Math::Angle::FromDegrees(45.0));Tip
M.RotateZ is equivalent to write M = M * M.CreateRotationZ in our example, since M is an Identity Matrix, we can, in this case, omit the multiplication.
Our sprite point coordinates are in 2 component vectors. We must create a 3 component vectors in order to multiply the vertex position by the rotation matrix. Then we multiply each point and copy the X and Y values into the Vector2D array that will be provided to SRL::Scene2D::DrawSprite.
Vector3D vec3_points[4] = {Vector3D(0.0)};
vec3_points[0] = Vector3D(points[0], 1.0);
vec3_points[1] = Vector3D(points[1], 1.0);
vec3_points[2] = Vector3D(points[2], 1.0);
vec3_points[3] = Vector3D(points[3], 1.0);
for(int i = 0 ; i < 4 ; i++)
{
//multiply vector by our rotation matrix
vec3_points[i] = transform * vec3_points[i];
// get back to vector2D type that SRL::Scene2D::DrawSprite accepts
points[i].X = vec3_points[i].X;
points[i].Y = vec3_points[i].Y;
}The result:
Warning
A quick note on animations
One might be tempted to, for example, make an animation of a sprite rotating by multiplying the rotated points from a previous frame by the rotation matrix. This will compound the rounding errors due to precision loss. This can be seen on the GIF below.
The correct approach is to define the new position from the initial state instead of the previous frame.
We calculate the new points from the original points and a new rotation matrix with the new angle. This way we avoid the compound of errors due to precision loss. The code for the main loop is shown below:
// Main program loop
while(1)
{
transform = Matrix33::Identity();
transform = transform.CreateRotationZ(SRL::Math::Angle::FromDegrees(angle)); // new matrix with the new angle
points[0] = Vector2D(-50, -50);
points[1] = Vector2D( 50, -50);
points[2] = Vector2D( 50, 50);
points[3] = Vector2D(-50, 50);
for(int i = 0 ; i < 4 ; i++)
{
vec3_points[i] = Vector3D(points[i], 1.0); // copy the original points into Vector3D points
vec3_points[i] = transform * vec3_points[i]; //multiply by matrix
// get back to vector2D type that SRL::Scene2D::DrawSprite accepts
points[i].X = vec3_points[i].X;
points[i].Y = vec3_points[i].Y;
}
SRL::Scene2D::DrawSprite ( textureIndex, points, 50.0 );
// Refresh screen
SRL::Core::Synchronize();
angle = angle + 1.0;
}And now we have the rotation working properly:
If we rotate by X or Y, we can get a very interesting behavior:
Rotation by X:
Rotation by Y:
And of course, we can compound transforms by multiplying the matrices.
For example if we want to rotate by 45 degrees in Z, and then by Y we can define or matrices as follows:
transform = Matrix33::Identity();
transform = transform.CreateRotationZ(SRL::Math::Angle::FromDegrees(45.0)); // new matrix with the new angle
transform = transform * transform.CreateRotationY(SRL::Math::Angle::FromDegrees(angle)) ; // We multiply our rotation matrix by our previous transformOr you could write
transform = Matrix33::Identity();
transform.RotateZ(SRL::Math::Angle::FromDegrees(45.0));
transform.RotateY(SRL::Math::Angle::FromDegrees(angle));Warning
Matrix multiplication is NOT commutative! A x B != B x A. The multiplication order matters!
See Below examples
The scaling is made by calling the Scale method.
The method needs a Vector3D, where its X, Y, and Z represent the scaling factor on the respective axis.
For example, to scale the sprite by 50%:
transform = Matrix33::Identity();
transform.Scale(Vector3D(0.5)); The Resulting sprite:
For translation we can do in two ways:
- Sum a translation vector
- Create a Translation matrix
Its the more simple way. Just add the vector that represents the displacement to each point.
Vector2D translateV = Vector2D(15.0, 15.0);
points[0] = Vector2D(-50, -50);
points[1] = Vector2D( 50, -50);
points[2] = Vector2D( 50, 50);
points[3] = Vector2D(-50, 50);
for(int i = 0 ; i < 4 ; i++)
{
points[i] = points[i] + translateV;
}
SRL::Scene2D::DrawSprite ( textureIndex, points, 50.0 );
// Refresh screen
SRL::Core::Synchronize();Its simple, however if you want to combine several transforms, using this approach gets cumbersome.
Defining a translation matrix allows to apply the translation to a given vertex in the same way you would apply scale and rotation.
A Translation matrix, that translates a Vertex by (x, y) is defined as:
For example we can define a translation matrix for a translation by (15,15) in SRL:
Matrix33 transform = Matrix33::Identity();
Vector2D translateV = Vector2D(15.0, 15.0);
transform.Row0.Z = translateV.X;
transform.Row1.Z = translateV.Y;
and then we can apply the translation in the same way we did for rotation and scaling.
The resulting code for the main loop becomes:
while(1)
{
Matrix33 transform = Matrix33::Identity();
Vector2D translateV = Vector2D(15.0, 15.0);
transform.Row0.Z = translateV.X;
transform.Row1.Z = translateV.Y;
points[0] = Vector2D(-50, -50);
points[1] = Vector2D( 50, -50);
points[2] = Vector2D( 50, 50);
points[3] = Vector2D(-50, 50);
for(int i = 0 ; i < 4 ; i++)
{
vec3_points[i] = Vector3D(points[i], 1.0); // copy the original points into Vector3D points
vec3_points[i] = transform * vec3_points[i]; //multiply by matrix
//get back to vector2D type that SRL::Scene2D::DrawSprite accepts
points[i].X = vec3_points[i].X;
points[i].Y = vec3_points[i].Y;
}
SRL::Scene2D::DrawSprite ( textureIndex, points, 50.0 );
// Refresh screen
SRL::Core::Synchronize();
}
Notice that if all 3 transforms are represented by matrices we can use the same code to apply them to the vertices. And also we can combine transforms by multiplying the matrices.
However, we can define a function to return the translation matrix that we can re-use when we need a translation matrix:
Matrix33 translationM(Fxp x, Fxp y)
{
Matrix33 transform = Matrix33::Identity();
transform.Row0.Z = x;
transform.Row1.Z = y;
return transform;
}
Now lets combine all the above:
First let's draw a scaled sprite on the center of the screen and have 4 sprites orbiting it.
First we must define the Radius, the current angle, and the angle offset between sprites.
Fxp radius = 150.0;
Fxp curr_angle = 0.0;
Fxp sprite_offset = 90.0;For the center sprite:
//draw the center sprite
Matrix33 transform_s = Matrix33::Identity();
transform_s = transform_s.CreateScale(Vector3D(0.3)); // scale the points
for(int i = 0 ; i < 4 ; i++)
{
working_points[i] = center_sprite[i];
vec3_points[i] = Vector3D(working_points[i], 1.0); // copy the original points into Vector3D points
vec3_points[i] = transform_s * vec3_points[i]; //multiply by matrix
// get back to vector2D type that SRL::Scene2D::DrawSprite accepts
working_points[i].X = vec3_points[i].X;
working_points[i].Y = vec3_points[i].Y;
}
SRL::Scene2D::DrawSprite ( textureIndex, working_points, 50.0 );
Now we can start by drawing the orbiting sprites:
transform_s = transform_s.CreateScale(Vector3D(0.5)); // scale our sprites by 50%
//draw each sprite
for(int sprite_nr = 0 ; sprite_nr < 4 ; sprite_nr++)
{
Fxp final_angle = curr_angle + (sprite_offset * sprite_nr);
//Calculate coordinates of our orbital sprite
Fxp x_pos = radius * SRL::Math::Trigonometry::Cos(final_angle);
Fxp y_pos = radius * SRL::Math::Trigonometry::Sin(final_angle);
Matrix33 transform_t = translationM(x_pos, y_pos);
Matrix33 transform_r = Matrix33::Identity();
Matrix33 transforms = transform_r.CreateRotationZ(Angle::FromDegrees(final_angle)) * transform_t * transform_s;
for(int i = 0 ; i < 4 ; i++)
{
working_points[i] = center_sprite[i];
vec3_points[i] = Vector3D(working_points[i], 1.0); // copy the original points into Vector3D points
vec3_points[i] = transforms * vec3_points[i]; //multiply by matrix
// get back to vector2D type that SRL::Scene2D::DrawSprite accepts
working_points[i].X = vec3_points[i].X;
working_points[i].Y = vec3_points[i].Y;
}
SRL::Scene2D::DrawSprite ( textureIndex, working_points, 50.0 );
}
Notice that we can combine all the transforms into a single matrix, by multiplication of the transform matrices.
The result:
The final code for this example becomes:
#include <srl.hpp>
// Using to shorten names for Vector and HighColor
using namespace SRL::Types;
using namespace SRL::Math::Types;
int32_t loadTGA(char* filename) //texture loading function
{
SRL::Bitmap::TGA *tga = new SRL::Bitmap::TGA(filename); // Loads TGA file into main RAM
int32_t textureIndex = SRL::VDP1::TryLoadTexture(tga); // Loads TGA into VDP1
delete tga;
return textureIndex;
}
Matrix33 translationM(Fxp x, Fxp y)
{
Matrix33 transform = Matrix33::Identity();
transform.Row0.Z = x;
transform.Row1.Z = y;
return transform;
}
int main()
{
// Initialize library
SRL::Core::Initialize(HighColor::Colors::Black);
SRL::Debug::Print(1,1, "06_Tutorial");
int32_t textureIndex = loadTGA("TEST.TGA"); // Loads TGA into VDP1
Vector2D center_sprite[4] = {Vector2D(0.0)}; // holds center sprite position.
center_sprite[0] = Vector2D(-50, -50);
center_sprite[1] = Vector2D( 50, -50);
center_sprite[2] = Vector2D( 50, 50);
center_sprite[3] = Vector2D(-50, 50);
Vector2D working_points[4] = {Vector2D(0.0)}; //
//we cant multiply a 2 component vector by a 3x3 matrix....
Vector3D vec3_points[4] = {Vector3D(0.0)};
Fxp radius = 150.0;
Fxp curr_angle = 0.0;
Fxp sprite_offset = 90.0;
// Main program loop
while(1)
{
//draw the center sprite
Matrix33 transform_s = Matrix33::Identity();
transform_s = transform_s.CreateScale(Vector3D(0.3));
// Apply the transform for every point
for(int i = 0 ; i < 4 ; i++)
{
working_points[i] = center_sprite[i];
vec3_points[i] = Vector3D(working_points[i], 1.0); // copy the original points into Vector3D points
vec3_points[i] = transform_s * vec3_points[i]; //multiply by matrix
// get back to vector2D type that SRL::Scene2D::DrawSprite accepts
working_points[i].X = vec3_points[i].X;
working_points[i].Y = vec3_points[i].Y;
}
SRL::Scene2D::DrawSprite ( textureIndex, working_points, 50.0 );
transform_s = transform_s.CreateScale(Vector3D(0.5));
for(int sprite_nr = 0 ; sprite_nr < 4 ; sprite_nr++)
{
Fxp final_angle = curr_angle + (sprite_offset * sprite_nr);
//Calculate coordinates of our orbital sprite
Fxp x_pos = radius * SRL::Math::Trigonometry::Cos(final_angle);
Fxp y_pos = radius * SRL::Math::Trigonometry::Sin(final_angle);
Matrix33 transform_t = translationM(x_pos, y_pos);
Matrix33 transform_r = Matrix33::Identity();
Matrix33 transforms = transform_r.CreateRotationZ(Angle::FromDegrees(final_angle)) * transform_t * transform_s;
for(int i = 0 ; i < 4 ; i++)
{
working_points[i] = center_sprite[i];
vec3_points[i] = Vector3D(working_points[i], 1.0); // copy the original points into Vector3D points
vec3_points[i] = transforms * vec3_points[i]; //multiply by matrix
// get back to vector2D type that SRL::Scene2D::DrawSprite accepts
working_points[i].X = vec3_points[i].X;
working_points[i].Y = vec3_points[i].Y;
}
SRL::Scene2D::DrawSprite ( textureIndex, working_points, 50.0 );
}
// Refresh screen
SRL::Core::Synchronize();
curr_angle += 1.0 ;
}
return 0;
}