In this tutorial we will look at selecting a specific region from a bitmap (e.g. a specific sprite from a spritesheet) and displaying it to the screen. We will also look at scaling this sprite horizontally and vertically.
Before we can think about cutting out a sprite from a larger bitmap image, we must first have a suitable bitmap to cut out from. I will be using the following bitmap, named test.bmp. We will be looking to select only the red astronaut on the right and display it enlarged by a pair of variable horizontal and vertical scale factors.
We will want to store the information required to select a sprite from a bitmap inside a sprite structure, which I will define as a type called Sprite inside the main.h file. This Sprite structure will need to store a pointer to a Bitmap structure, which I will call bitmap. This will be the source bitmap that the sprite is extracted from.
The next four properties to store inside the Sprite structure are all integers, and they are the starting x and y coordinates and the ending x and y coordinates of the sprite within the bitmap. These will be named startX, startY, endX and endY respectively. Remember that the point (0, 0) is the bottom left corner of the bitmap image. We will use these four values to cut out a sprite from our bitmap source image.
After these, we want to store two more integers: the centre x and y values of the sprite, named centreX and centreY. These make up the point on the sprite that we want to draw from. So, for example, if we wanted to draw from the bottom corner, we would set centreX to 0 and centreY to 0. Then, when we call the function we will later define to draw the sprite, the coordinates that we input as arguments will be the coordinates at which the bottom left corner of the sprite is drawn. If we set centreX to half of the sprite width, and centreY to half of the sprite height, then the sprite will be centered, so will be drawn with it's centre at the x and y coordiantes inputted into the drawSprite function we will later create.
The last two properties to store are both floating point values. These are the horizontal and vertical scales, named xScale and yScale respectively. These are the scale factors that we will enlarge our sprite by when drawing. The centre of enlargement will be (centreX, centreY).
That is all we will need to store in our sprite structure.
Now that we have created a Sprite structure, we need to initialise a sprite for us to later render to the screen. To do this, I will first declare a global variable in main.h. It will be a Sprite structure, and will be named spr_test. We should then declare a void function called "initialiseSprites" inside main.h. The function will take no parameters.
We will define our initialiseSprites function inside render.c. This function will simply just set up spr_test. We will first set the bitmap pointer of the sprite to the address of bitmap structure, using the line "spr_test.bitmap = &bmp_test;".
Next we need to set up the region of the source bitmap to draw. We do this by setting the startX, endX, startY and endY properties. We have a 128x64 px spritesheet, where each sprite is 64x64. We want to select the second sprite. This sprite starts at x = 64, y = 0, and ends at x = 128, y = 64. Therefore, we should set startX to 64 using the line "spr_test.startX = 64;". We should set endX to 128 using the line "spr_test.endX = 128;". We should set startY to 0 with the line "spr_test.startY = 0;", and we should set endY to 64 with the line "spr_test.endY = 64;".
After this, we should set the centre coordinates. Note that these are relative to the size of the sprite, not the source coordinates. I want the sprite to be centred, and, since the sprite we have cut out is 64x64 px, I will set the centre to the coordinates (32, 32). This can be done with the lines: "spr_test.centreX = 32;" and "spr_test.centreY = 32;".
The last two properties to set are the xScale and yScale properties. I will set these both to 1.0 for now, but we can look at varying these later. We can do this using the lines: "spr_test.xScale = 1.0;" and "spr_test.yScale = 1.0;".
In this section we will briefly discuss how we can cut out a specific sprite from a bitmap and draw it scaled up or down. We should start by finding the width and height of the region of the bitmap we want to display. The width is equal to abs(sprite->endX - sprite->startX) and the height is equal to abs(sprite->endY - startY).
Now we need to imagine that this rectangle is at coordinate (0,0) - its bottom left coordinate is at the origin.
After this, we need to translate these points leftwards and downwards by the sprite's centreX and centreY values respectively, so that the vertices are now centred around the origin. We can accomplish this by subtracting centreX from each of the points' x coordinates, and centreY from each of the point's y coordinates, as shown below.
Now that our sprite's vertices are centred around its origin, we can multiply each of the coordinates by the horizontal and vertical scale. For demonstration purposes, I will let xScale = 2.0 and let yScale = 3.0, as you can see below:
Now we need to find the minimum and maximum x and y coordinates. This may seem unusual, but imagine if our horizontal scale was a negative number, then the points would be flipped around and the leftmost point would become the rightmost point. Therefore, we need to compare the points with each other to find the minimum and maximum bounds (which I will call minX, maxX, minY and maxY).
Now we want to add the x and y coordinates that we wish to draw the sprite at to each vertex. This will translate the center of the sprite (currently at point (0, 0)) to the x and y coordinates we specified, meaning that the sprite will be centred around those points.
Now we want to render the actual pixels. To do this, we need to calculate the x and y coordinates of pixel on the original bitmap. To do this, we need to reverse the original transformation. We most recently translated our sprite by the x and y coordinates, so we need to subtract these x and y coordinates. Then we need to divide by the x and y scales, so that the resulting sprite is of the same size as the bitmap. Then we need to add on the centre coordinates, so that the resulting bitmap has its bottom left corner at the point (0, 0). To finish, we simply need to add on the x and y coordinates we want to start from on the bitmap source image, so that we draw the specfic section we cut out. Don't worry if this is confusing for now, as we will look at it in more depth when we look at writing the code.
Now we've looked at the algorithm we will use, we can start to implement it. We should create a drawSprite function. It should be a void function, and should take three parameters. The first two should be integers, and should be the x and y coordinates to draw the sprite at. The last should be a pointer to the sprite structure. We can do this with the line "void drawSprite(int x, int y, Sprite * sprite){".
Next, we need to find the width and height of the sprite we have chosen to cut out. We can do this by subtracting the start coordinates from the end coordinates. This can be done with the lines "int width = abs(sprite->endX - sprite->startX);" and "int height = abs(sprite->endY - sprite->startY);".
After this, we need to create an array of vertices. These will represent the corners of the sprite's bounding box on the screen. The vertices array will be a 2d array, with four lots of sub-arrays storing two integers, the x and y coordinates of the vertex in that order.
The vertices will be initially stored in the order: bottom left, top left, top right, bottom right. We want the sprite to be centred around its centreX and centreY coordinates (specified in the sprite structure). Therefore, we should set the bottom left coordinate to (-centreX, -centreY). This can be done with the lines "vertices[0][0] = -sprite->centreX;" and "vertices[0][1] = -sprite->centreY;". We want to set the top left to the same x coordinate as the bottom left and we want to set it to the y coordinate of the bottom eft vertex added to the height of the sprite. This can be done with the lines "vertices[1][0] = vertices[0][0];" and "vertices[1][1] = vertices[0][1] + height;". Then, we can set the coordinates of the top right vertex. The x coordinate of the top right vertex should be equal to the x coordinate of the bottom left vertex added to the width of the sprite. The y coordinate should be equal to the y coordinate of the top left vertex. This can be accomplished with the lines "vertices[2][0] = vertices[0][0] + width;" and "vertices[2][1] = vertices[1][1];". Finally, the bottom right vertex should have the x coordinate of the top right vertex and the y coordinate of the bottom left vertex. This can be done with the lines "vertices[3][0] = vertices[2][0];" and "vertices[3][1] = vertices[0][1];".
Now we need to iterate through each of the vertices and multiply them by the correct scale factor in each direction. We should first create a loop counter, which I will call i. This can be done with the line "int i;". Now we need to iterate through each of the four vertices. This can be done with the line "for(i = 0; i < 4; i++){". Now we need to mutliply the x coordinate of each vertex by the sprite's xScale property. This can be done with the line "vertices[i][0] *= sprite->xScale;". We also need to multiply the y coordinate of each vertex by the sprite's yScale property. This can be done with the line "vertices[i][1] *= sprite->yScale;".
After applying horizontal and vertical scaling, we need to find the minimum and maximum x and y coordinates for the vertices. We should first create four integers, minX, maxX, minY and maxY. We should initialise these to the x an y coordinates of the bottom left (or any other) vertex. This is so that they are not assigned a random value, as we will need to compare them with the other vertices to find the minimum and maximum x an y coordinates. We can do this with the following lines: "int minX = vertices[0][0];", "int maxX = vertices[0][0];", "int minY = vertices[0][1];" and "int maxY = vertices[0][1];".
Now that we have our minimum and maximum coordinate variables, we atually have to find the minimum and maximum coordinates. We will need to iterate through each of the variables, which we can do with the line "for(i = 0; i < 4; i++){". Inide this loop, we need to check if the x coordinate of vertex i is greater than the maximum x. If so, then we should change the maxX variable to the x coordiante of vertex i. This can be done using the if statement "if(vertices[i][0] > maxX){" and writing the following line inside of it: "maxX = vertices[i][0];". We also want to check if the x coordinate of vertex i is smaller than our current minimum x value, and update said miimum x value if so. This can be done with the if statement: "if(vertices[i][0] < minX){", with the line "minX = vertices[i][0];" inside of it. We then want to do the same for the minY and maxY values. For maxY, we can use the if statement "if(vertices[i][1] > maxY){" with the line "maxY = vertices[i][1];" inside. For minY, we can use the if statement "if(vertices[i][1] < minY){" with the line "minY = vertices[i][1];" inside.
Now we need to translate the minimum an maximum x and y coordinates, so that they are centred around the x and y coordinates we wish to draw to. We can do this with the lines "minX += x;", "maxX += x;", "minY += y;" and "maxY += y;".
As we now have the correct minimum and maximum x and y coordinates, we can start to fill in our sprite's bounding box on screen with pixels. To do this, we will first need to create a pixel pointer to point to the pixel we will draw to. We can do this with the line: "Pixel * pixel;". We must also create another loop counter, which I will call j. We can do this with the line: "int j;".
With these variables set up, we can loop through all of the rows of the sprite. We can do this with the line "for(i = minY; i < maxY; i++){". Inside this loop, we first want to set the pixel pointer to the start of the row. We can do this with the line "pixel = renderBuffer.pixels + i * BUFFER_WIDTH + minX;". We then want to iterate through all the pixels on this row. We can do this with the line: "for(j = minX; j < maxX; j++){". Next, we should check if the pixel is on screen. We can do this with the line: "if(j >= 0 && j < BUFFER_WIDTH && i >= 0 && i < BUFFER_HEIGHT){". If the pixel is on screen, we want to retrieve the coordinates of the corresponding pixel on the original bitmap image. This can be done with the lines "int pixelX = (int) ((j - x) / sprite->xScale) + sprite->centreX + sprite->startX;" and "int pixelY = (int) ((i - y) / sprite->yScale) + sprite->centreY + sprite->startY;". Now we need to check if these pixel coordinates are within the specified region of our sprite. This can be done with the line "if(pixelX >= sprite->startX && pixelX < sprite->endX && pixelY >= sprite->startY && pixelY < sprite->endY){". If the pixel coordinates are in the valid range, we need to get the actual pixel on the source bitmap. This can be done with the following line: "Pixel * srcPixel = sprite->bitmap->pixels + (pixelY * sprite->bitmap->infoHeader.biWidth) + pixelX;". Next, we need to check that the pixel is not transparent. Note that I have defined three constants in main.h. These are TRANSPARENT_RED, TRANSPARENT_GREEN and TRANSPARENT_BLUE. These are the red, green and blue values of the colour I will be treating as transparent. If a pixel on the source bitmap has these RGB values, it should be treated as transparent and should not be drawn. I have defined TRANSPARENT_RED as 163, TRANSPARENT_GREEN as 73 and TRANSPARENT_BLUE as 164. We can check that the pixel is not transparent with the line: "if(srcPixel->red != TRANSPARENT_RED && srcPixel->green != TRANSPARENT_GREEN && srcPixel->blue != TRANSPARENT_BLUE){". If the source pixel is not transparent, then we want to draw it to the address given by the pixel pointer. We can do this with the lines: "pixel->red = srcPixel->red;", "pixel->green = srcPixel->green;" and "pixel->blue = srcPixel->blue;". Before the end of the pixel row loop (the j loop), we want to increment the pixel pointer to the next pixel in the row. This can be done with the line: "pixel ++;".
That's it for our drawSprite function. We need to call it in the render function. I have done this with the line: "drawSprite(BUFFER_WIDTH / 2, BUFFER_HEIGHT / 2, &spr_test);".
Now that we have a working drawSprite function, we can demonstrate its abilities by modifying its scale in real time.
We can make its horizontal scale change when the left and right arrow keys are pressed. To do this, we will need to use the GetAsyncKeyState function. This takes one argument - the key code of the key to check for. We can check if the left arrow key was pressed using the line: "if(GetAsyncKeyState(VK_LEFT)){". Inside this, we should add the line: "spr_test.xScale -= 0.1;". This will decrease the sprite's horizontal scale by 0.1 when the left arrow key is pressed. For the right arrow key, we should use the if statement "if(GetAsyncKeyState(VK_RIGHT)){". Inside this if statement should be the line: "spr_test.xScale += 0.1;".
The process of varying the vertical scale is similar to the horizontal scale. For the up arrow key, we can use the if statement: "if(GetAsyncKeyState(VK_UP)){", with the line "spr_test.yScale += 0.1;" inside. For the down arrow key, we should use the if statement: "if(GetAsyncKeyState(VK_DOWN)){" with the line "spr_test.yScale -= 0.1;" inside.
The code for this section is shown below. I have included all three of our files: main.h, render.c and main.c. I will be compiling the code with the MinGW compiler, using the comman "gcc main.c -lgdi32 -o app". You should be able to use any compiler, but be sure to link the gdi32 library.
main.h
render.c
main.c
When we initially run our program, we should see something similar to this:
By holding the arrow keys, we should be able to adjust the scale of our image. Below are some examples from my program:
That's it for this tutorial. In the next tutorial, we will look at displaying text to the screen.