Win32 Software Renderer in C: Part 12 - Rotating a Bitmap
Note: this tutorial series requires significant knowledge of the C programming language
In this tutorial, we will look at rotating a bitmap. We will write a function to rotate our spr_test sprite (the red astronaut).
The Theory:
To rotate a sprite, we should define a drawRotatedSprite function. It should be avoid function, and should take four parameters. The first two should be integers, and should be the x and y coordinates to draw the point (centreX, centreY) at. These will be named "x" and "y". The next is a floating point number - the angle of rotation, which should be named "angle". The last should be a pointer to the sprite to render. This should be called "sprite". We can create this function with the line: "void drawRotatedSprite(int x, int y, float angle, Sprite * sprite){".
An Overview of Sprite Rotation
Before we write the code inside our function, we must first understand how sprite rotation works. Note that we do not just want to rotate a sprite, but we want to enlarge it by its xScale and yScale as well.
As with our drawSprite function, we first want to define a set of vertices representing the bounding box. We can start with the coordinates (0,0), (0, height), (width, height), (width, 0), and subtract centreX and centreY from the x and y coordinates of each vertex. This will produce a bounding box for the sprite, where the defined centre of the sprite is at the origin point: (0, 0).
Becomes...
Next, we want to enlarge our bounding box by the horizontal and vertical scale factors. We can do this by multiplying each of the x coordinates by the sprite's xScale value, and each of the y coordinates by the sprite's yScale value.
Now we want to rotate the vertices around the origin (remember that we translated the bounding box so that the (centreX, centreY) point was moved to the origin). For each vertex, the x coordinate = lcos(a), where l is the distance between the vertex and the origin, and a is the angle the vertex makes with the positive x-axis. The y coordinate = lsin(a). We want to rotate each vertex by an angle. I will call this b, but in our actual code it is simply called "angle". This means that the total angle after rotation = a + b. Therefore, the new x coordinate of any given vertex = lcos(a + b), and the new y coordinate = lsin(a + b). Now, we can use the double angle formulae to simplify these. cos(a + b) = cos(a)cos(b) - sin(a)sin(b). sin(a + b) = sin(a)cos(b) + cos(a)sin(b). Therefore, the new x coordinate = lcos(a)cos(b) - lsin(a)sin(b). As lcos(a) = the previous x coordinate of the vertex, and lsin(a) = the previous y coordinate of the vertex, the new x = xcos(b) - ysin(b), where x and y are the coordinates of the vertex before rotation. The new y coordinate can be expressed as lsin(a)cos(b) + lcos(a)sin(b), which is equal to ycos(b) + xsin(b). A diagram showing the rotation of a single vertex can be found below.
With the rotated vertices found, we want to add the x and y coordinates (which were passed into the function as parameters) to each vertex. This will translate the origin (the defined centre of the sprite) to the coordinates we wish to draw the sprite at. After we have done this, we should compare each of the vertices to get the mininum x and y coordinates and the maximum x and y coordiates. We will call these minX, maxX, minY and maxY.
Now we can simply iterate through each of the pixels between (minX, minY) and (maxX, maxY) and reverse the tranformation to get the coordinates of source bitmap pixel that corresponds to the screen pixel. We should check that this pixel is within the (startX, startY) to (endX, endY) bounds of the sprite. If so, we should check if the sprite is not transparent. If it isn't transparent, we will need to check if the sprite is in plain colour mode. If so, we should draw the colour stored in the sprite structure. If it is not in plain colour mode, we should get the source pixel and display it to the screen.
Writing the drawRotatedSprite Function Code
The first thing to do in our function is to calculate the width and height of the sprite in the bitmap (without scaling applied). We should store these to two integer variables named "width" and "height". We can calculate width by subtracting startX from endX and taking the absolute value of the result. We can calculate height by taking the absolute value of endY - startY. We can accomplish this with the lines: "int width = abs(sprite->endX - sprite->startX);" and "int height = abs(sprite->endY - sprite->startY);".
Next, we need to create a vertex array, using the line: "float vertices[4][2];". This array is 2d, where each sub-array consists of two floating point numbers: the x coordinate and the y coordinate in that order. We then want to set the bottom left corner to (-centreX, -centreY) so that the defined centre of the sprite is translated to the origin. We should set the bottom left corner with the lines: "vertices[0][0] = -sprite->centreX;" and "vertices[0][1] = -sprite->centreY;". We should then set the top left vertex with the lines: "vertices[1][0] = vertices[0][0];" and "vertices[1][1] = vertices[0][1] + height;". Next, we should set the top right vertex with the lines: "vertices[2][0] = vertices[1][0] + width;" and "vertices[2][1] = vertices[1][1];". After this, we need to set the bottom right coordinate, using the lines: "vertices[3][0] = vertices[2][0];" and "vertices[3][1] = vertices[0][1];".
After this, we need to multiply the x and y coordinates of each vertex by our sprite's xScale and yScale values respectively. We should first create a loop counter with the line: "int i;". Now we need to iterate through each of the vertices with the for loop: "for(i = 0; i < 4; i++){". Inside this loop, we need to multiply the x and y coordinates of the vertex at index i by xScale and yScale respectively. This can be done with the lines: "vertices[i][0] *= sprite->xScale;" and "vertices[i][1] *= sprite->yScale;".
Next, we should rotate each vertex by the angle passed into our function as a parameter. We should first pre-calculate the sine and cosine of our angle, so that we will not have to calculate these values more than necessary. We should do this with the lines: "float sinAngle = sin(angle);" and "float cosAngle = cos(angle);". Next, we should start a loop to iterate through each vertex, using the line: "for(i = 0; i < 4; i++){". Inside this loop, we should store the current x and y coordinates of the vertex, as we will need them in the next few lines. We should store these values to a pair of local variables called prevX and prevY. We can do this with the lines: "float prevX = vertices[i][0];" and "float prevY = vertices[i][1];". After this, we should use the formulae: new x = prevX * cosAngle - prevY * sinAngle and newY = prevY * cosAngle + prevX * sinAngle. We can do this with the lines: "vertices[i][0] = prevX * cosAngle - prevY * sinAngle;" and "vertices[i][1] = prevY * cosAngle + prevX * sinAngle;".
Now that we have our rotated vertices, we need to translate each vertex over to x and y coordinates we wish to draw at. To do this, we need to iterate through each vertex, which we can do using the line: "for(i = 0; i < 4; i++){". Inside the for loop, we should add the lines "vertices[i][0] += x;" and "vertices[i][1] += y;", to translate our vertices to the correct drawing coordinates.
We now need to find the minimum and maximum x and y coordinates. We should initialise four integer variables: minX, maxX, minY and maxY. These should each be initialised to the coordinates of the first vertex, so that these values can be compared with each vertex. We can initialise these variables with the lines: " int minX = vertices[0][0];", "int maxX = vertices[0][0];", "int minY = vertices[0][1];" and "int maxY = vertices[0][1];". Now we should iterate through each of the vertices, using the for loop "for(i = 0; i < 4; i++){", and compare the coordinates of the vertices with minX, maxX, minY and maxY. For any vertex, if the x coordinate is greater than maxX, we want to set maxX to the x coordinate. Likewise, if it is smaller than minX, we want to set minX to the x coordinate. We then want to do this for the y coordinates. We can check if the x coordinate is smaller than minX using the if statement: "if(vertices[i][0] < minX){". If so, we should run the line: "minX = vertices[i][0];". We should next check to see if maxX has been exceeded. We can check this using the if statement: "if(vertices[i][0] > maxX){". Inside this if statement, we should add the line: "maxX = vertices[i][0];". We can check if the y coordinate is less than minY using the if statement: "if(vertices[i][1] < minY){". Inside this if statement, we should add the line: "minY = vertices[i][1];". We can check if maxX has been exceeded using the if statement: "if(vertices[i][1] > maxY){". If so, we want to run the line: "maxY = vertices[i][1];".
Now we can look at actually drawing pixels to the screen. We should first create a pixel pointer with the line: "Pixel * pixel;". Next, we want to iterate through each row of pixels, using the for loop: "for(i = minY; i < maxY; i++){". Inside this loop, we first want to set the pixel pointer to the start of the row, which can be done with the line: "pixel = renderBuffer.pixels + i * BUFFER_WIDTH + minX;". After this, we should create another loop counter, using the line: "int j;", and should loop through the pixels on the row. We can do this with the for loop: "for(j = minX; j < maxX; j++){". Inside this loop, we first want to check if the pixel is within the screen bounds. We can check this with the if statement: "if(j >= 0 && j < BUFFER_WIDTH && i >= 0 && i < BUFFER_HEIGHT){". Inside this loop, we can get the pixel coordinates of the unrotated (but still scaled_ sprite using the lines: "int pixelX = (j - x) * cosAngle + (i - y) * sinAngle;" and "int pixelY = (i - y) * cosAngle - (j - x) * sinAngle;". These two lines reverse the most recent translation of the sprite, before rotating by -angle. After this, we want to divide the pixelX and pixelY values by the xScale and yScale values. We can do this with the lines: "pixelX /= sprite->xScale;" and "pixelY /= sprite->yScale;". After this, we should add the centreX and centreY values, so that the source image bounding box has its bottom left corner at point (0, 0). We can do this with the lines: "pixelX += sprite->centreX;" and "pixelY += sprite->centreY;". After this, we should add the starting x and y coordinates of the sprite on the source bitmap to our pixelX and pixelY values. This can be done with the lines: "pixelX += sprite->startX;" and "pixelY += sprite->startY;".
With the pixel coordinates set up, we should check that the pixel is in the valid region specified within the sprite. We can check this with the if statement: "if(pixelX >= sprite->startX && pixelX < sprite->endX && pixelY >= sprite->startY && pixelY < sprite->endY){". Inside this if statement, we want to get the source pixel, by adding pixelY * sprite->bitmap->infoHeader.biWidth + pixelX to the sprite bitmap pointer. This can be done with the line: "Pixel * srcPixel = sprite->bitmap->pixels + pixelY * sprite->bitmap->infoHeader.biWidth + pixelX;".
After retrieving our source pixel, we should check that it is not transparent. We can do this with the line: "if(srcPixel->red != TRANSPARENT_RED || srcPixel->green != TRANSPARENT_GREEN || srcPixel->blue != TRANSPARENT_BLUE){". Inside this if statement, we want to check if plain colour mode has been enabled. We can check this with the if statement: "if(sprite->isPlainColour == 1){". Inside this if statement, we should set the current pixel to the colour stored in the sprite structure. We can use the lines: "pixel->red = sprite->red;", "pixel->green = sprite->green;" and "pixel->blue = sprite->blue;" for this. If we are not in plain colour mode (i.e. in the else block of the plain colour if statement), we should set the pixel colour to that of the source pixel. We can do this with the lines: "pixel->red = srcPixel->red;", "pixel->green = srcPixel->green;" and "pixel->blue = srcPixel->blue;".
Just before the end of the pixel row loop (the j loop: "for(j = minX; j < maxX; j++){"), we need to increment the pixel pointer, using the line: "pixel++;".
Inside main.h, we want to create a global angle variable and initialise it to 0. This will allow us to store and change the angle of our sprite in real time. In main.h, we can declare this function with the line: "float angle = 0;". Inside the render function in render.c, we want to draw our rotated sprite, using the line: "drawRotatedSprite(BUFFER_WIDTH / 2, BUFFER_HEIGHT / 2, angle, &spr_test);".
Inside the render function, we should check if the left key was pressed using the line: "if(GetAsyncKeyState(VK_LEFT)){". Inside this if statement, we should run the line "angle += 0.01;" to rotate the sprite anticlockwise. To rotate clockwise, we should use the if statement: "if(GetAsyncKeyState(VK_RIGHT)){", with the line "angle -= 0.01;" inside. We can also scale the sprite in real time. We can have the sprite increase in size when the up arrow key is pressed, using the if statement: "if(GetAsyncKeyState(VK_UP)){", with the lines: "spr_test.xScale += 0.01;" and "spr_test.yScale += 0.01;" inside. Similarly, we can have the sprite shrink when the down arrow key is pressed. We can do this using the if statement: "if(GetAsyncKeyState(VK_DOWN)){", with the lines "spr_test.xScale -= 0.01;" and "spr_test.yScale -= 0.01;" inside.
That's all the theory for this tutorial. As usual, the code can be found below.
The Code:
Below is the code for this section. You will need a main.c file, but I have not shown this below as it has not changed over the last few tutorials. I will be compiling it with the MinGW compiler, using the command: "gcc main.c -lgdi32 -o app". You should be able to use any compiler, so long as you are able to link the gdi32 library.
main.h
#include <windows.h>
#include <stdlib.h>
#include <stdio.h>
#include <stdint.h>
#include <math.h>
#include <string.h>
#define BUFFER_WIDTH 640
#define BUFFER_HEIGHT 480
#define TRANSPARENT_RED 163
#define TRANSPARENT_GREEN 73
#define TRANSPARENT_BLUE 164
#define CHARACTER_SIZE 8
typedef struct Pixel {
uint8_t blue;
uint8_t green;
uint8_t red;
} Pixel;
typedef struct RenderBuffer {
HWND windowHandle;
HDC deviceContextHandle;
Pixel * pixels;
BITMAPINFO bitmapInfo;
int scale;
int windowClientWidth;
int windowClientHeight;
} RenderBuffer;
typedef struct Bitmap {
BITMAPFILEHEADER fileHeader;
BITMAPINFOHEADER infoHeader;
Pixel * pixels;
} Bitmap;
typedef struct Sprite {
Bitmap * bitmap;
int startX;
int startY;
int endX;
int endY;
int centreX;
int centreY;
float xScale;
float yScale;
int isPlainColour;
uint8_t red;
uint8_t green;
uint8_t blue;
} Sprite;
int running = 1;
RenderBuffer renderBuffer;
float angle = 0;
Bitmap bmp_test;
Bitmap bmp_font;
Sprite spr_test;
void mainLoop();
void handleEvents();
void drawRectangle(int startX, int startY, int endX, int endY, uint8_t red, uint8_t green, uint8_t blue);
void drawLine(int startX, int startY, int endX, int endY, uint8_t red, uint8_t green, uint8_t blue);
void drawRow(int x1, int x2, int y, uint8_t red, uint8_t green, uint8_t blue);
void drawTriangle(int x1, int y1, int x2, int y2, int x3, int y3, uint8_t red, uint8_t green, uint8_t blue);
void drawCircle(int centreX, int centreY, int radius, uint8_t red, uint8_t green, uint8_t blue);
int loadBMPFile(char filePath[], Bitmap * bitmap);
void loadAllBitmaps();
void drawBitmap(int x, int y, Bitmap * bitmap);
void initialiseSprites();
void drawSprite(int x, int y, Sprite * sprite);
void getCoordinatesFromCharacter(char c, int * x, int * y);
void drawSingleLineText(int x, int y, char text[], int charSize, int length, uint8_t red, uint8_t green, uint8_t blue);
void drawMultiLineText(int x, int y, char text[], int charSize, int maxWidth, uint8_t red, uint8_t green, uint8_t blue);
void render();
LRESULT CALLBACK windowProcedure(HWND windowHandle, UINT messageID, WPARAM wParam, LPARAM lParam);
render.c
void drawRectangle(int startX, int startY, int endX, int endY, uint8_t red, uint8_t green, uint8_t blue){
Pixel * pixel;
int i = 0;
int j = 0;
for(i = startY; i <= endY; i++){
pixel = renderBuffer.pixels + i * BUFFER_WIDTH + startX;
for(j = startX; j <= endX; j++){
if(i >= 0 && i < BUFFER_HEIGHT && j >= 0 && j < BUFFER_WIDTH){
pixel->red = red;
pixel->blue = blue;
pixel->green = green;
};
pixel++;
};
};
};
void drawLine(int startX, int startY, int endX, int endY, uint8_t red, uint8_t green, uint8_t blue){
int dx = endX - startX;
int dy = endY - startY;
float xStep;
float yStep;
int totalSteps = 0;
if(abs(dy) > abs(dx)){
totalSteps = abs(dy);
} else {
totalSteps = abs(dx);
};
yStep = (float) dy / totalSteps;
xStep = (float) dx / totalSteps;
int i = 0;
float x = startX;
float y = startY;
Pixel * pixel;
for(int i = 0; i <= totalSteps; i++){
if(x >= 0 && x < BUFFER_WIDTH && y >= 0 && y < BUFFER_HEIGHT){
pixel = renderBuffer.pixels + ((int) y) * BUFFER_WIDTH + (int) x;
pixel->red = red;
pixel->green = green;
pixel->blue = blue;
};
x += xStep;
y += yStep;
};
};
void drawRow(int x1, int x2, int y, uint8_t red, uint8_t green, uint8_t blue){
Pixel * pixel;
int minX;
int maxX;
if(x1 < x2){
minX = x1;
maxX = x2;
} else {
minX = x2;
maxX = x1;
};
pixel = renderBuffer.pixels + (BUFFER_WIDTH * y) + minX;
int i;
for(i = minX; i <= maxX; i++){
if(i >= 0 && i < BUFFER_WIDTH && y >= 0 && y < BUFFER_HEIGHT){
pixel->red = red;
pixel->green = green;
pixel->blue = blue;
};
pixel ++;
};
};
void drawTriangle(int x1, int y1, int x2, int y2, int x3, int y3, uint8_t red, uint8_t green, uint8_t blue){
int pointsX[3];
int pointsY[3];
pointsX[0] = x1;
pointsX[1] = x2;
pointsX[2] = x3;
pointsY[0] = y1;
pointsY[1] = y2;
pointsY[2] = y3;
int i = 0;
for(i = 1; i < 3; i++){
int j = i;
while(j > 0 && pointsY[j - 1] > pointsY[j]){
int temp = pointsY[j];
pointsY[j] = pointsY[j - 1];
pointsY[j - 1] = temp;
temp = pointsX[j];
pointsX[j] = pointsX[j - 1];
pointsX[j - 1] = temp;
j -= 1;
};
};
float lineX1 = pointsX[0];
float lineX2 = pointsX[0];
float xStep1 = (float) (pointsX[1] - pointsX[0]) / (pointsY[1] - pointsY[0]);
float xStep2 = (float) (pointsX[2] - pointsX[0]) / (pointsY[2] - pointsY[0]);
for(i = pointsY[0]; i < pointsY[1]; i++){
drawRow((int) lineX1, (int) lineX2, i, red, green, blue);
lineX1 += xStep1;
lineX2 += xStep2;
};
xStep1 = (float) (pointsX[2] - pointsX[1]) / (pointsY[2] - pointsY[1]);
for(i = pointsY[1]; i < pointsY[2]; i++){
drawRow((int) lineX1, (int) lineX2, i, red, green, blue);
lineX1 += xStep1;
lineX2 += xStep2;
};
};
void drawCircle(int centreX, int centreY, int radius, uint8_t red, uint8_t green, uint8_t blue){
int i = 0;
for(i = centreY - radius; i <= centreY + radius; i++){
double k = radius * radius - (i - centreY) * (i - centreY);
drawRow((int) centreX - sqrt(k), (int) centreX + sqrt(k), i, red, green, blue);
};
};
int loadBMPFile(char filePath[], Bitmap * bitmap){
FILE * file = fopen(filePath, "rb");
if(!file){
return 0;
};
fseek(file, 0, SEEK_END);
int fileSize = ftell(file);
rewind(file);
if(fileSize < sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER)){
fclose(file);
return 0;
};
fread(&(bitmap->fileHeader), sizeof(BITMAPFILEHEADER), 1, file);
fread(&(bitmap->infoHeader), sizeof(BITMAPINFOHEADER), 1, file);
bitmap->pixels = (Pixel *) malloc(bitmap->infoHeader.biWidth * bitmap->infoHeader.biHeight * sizeof(Pixel));
fread(bitmap->pixels, sizeof(Pixel), bitmap->infoHeader.biWidth * bitmap->infoHeader.biHeight, file);
fclose(file);
return 1;
};
void loadAllBitmaps(){
loadBMPFile("test.bmp", &bmp_test);
loadBMPFile("font.bmp", &bmp_font);
};
void drawBitmap(int x, int y, Bitmap * bitmap){
Pixel * pixel;
int i = 0;
int j = 0;
for(i = 0; i < bitmap->infoHeader.biHeight; i++){
pixel = renderBuffer.pixels + (y + i) * BUFFER_WIDTH + x;
for(j = 0; j < bitmap->infoHeader.biWidth; j++){
if(j >= 0 && j < BUFFER_WIDTH && y + i >= 0 && y + i < BUFFER_HEIGHT){
Pixel * srcPixel = bitmap->pixels + i * bitmap->infoHeader.biWidth + j;
if(srcPixel->red != TRANSPARENT_RED || srcPixel->green != TRANSPARENT_GREEN || srcPixel->blue != TRANSPARENT_BLUE){
pixel->red = srcPixel->red;
pixel->green = srcPixel->green;
pixel->blue = srcPixel->blue;
};
pixel++;
};
};
};
};
void initialiseSprites(){
spr_test.bitmap = &bmp_test;
spr_test.startX = 64;
spr_test.endX = 128;
spr_test.startY = 0;
spr_test.endY = 64;
spr_test.centreX = 32;
spr_test.centreY = 32;
spr_test.xScale = 1.0;
spr_test.yScale = 1.0;
spr_test.isPlainColour = 0;
};
void drawSprite(int x, int y, Sprite * sprite){
int width = abs(sprite->endX - sprite->startX);
int height = abs(sprite->endY - sprite->startY);
float vertices[4][2];
vertices[0][0] = -sprite->centreX;
vertices[0][1] = -sprite->centreY;
vertices[1][0] = vertices[0][0];
vertices[1][1] = vertices[0][1] + height;
vertices[2][0] = vertices[0][0] + width;
vertices[2][1] = vertices[1][1];
vertices[3][0] = vertices[2][0];
vertices[3][1] = vertices[0][1];
int i;
for(i = 0; i < 4; i++){
vertices[i][0] *= sprite->xScale;
vertices[i][1] *= sprite->yScale;
};
int minX = vertices[0][0];
int maxX = vertices[0][0];
int minY = vertices[0][1];
int maxY = vertices[0][1];
for(i = 0; i < 4; i++){
if(vertices[i][0] > maxX){
maxX = vertices[i][0];
};
if(vertices[i][0] < minX){
minX = vertices[i][0];
};
if(vertices[i][1] > maxY){
maxY = vertices[i][1];
};
if(vertices[i][1] < minY){
minY = vertices[i][1];
};
};
minX += x;
maxX += x;
minY += y;
maxY += y;
Pixel * pixel;
int j;
for(i = minY; i < maxY; i++){
pixel = renderBuffer.pixels + i * BUFFER_WIDTH + minX;
for(j = minX; j < maxX; j++){
if(j >= 0 && j < BUFFER_WIDTH && i >= 0 && i < BUFFER_HEIGHT){
int pixelX = (int) ((j - x) / sprite->xScale) + sprite->centreX + sprite->startX;
int pixelY = (int) ((i - y) / sprite->yScale) + sprite->centreY + sprite->startY;
if(pixelX >= sprite->startX && pixelX < sprite->endX && pixelY >= sprite->startY && pixelY < sprite->endY){
Pixel * srcPixel = sprite->bitmap->pixels + (pixelY * sprite->bitmap->infoHeader.biWidth) + pixelX;
if(srcPixel->red != TRANSPARENT_RED || srcPixel->green != TRANSPARENT_GREEN || srcPixel->blue != TRANSPARENT_BLUE){
if(sprite->isPlainColour){
pixel->red = sprite->red;
pixel->green = sprite->green;
pixel->blue = sprite->blue;
} else {
pixel->red = srcPixel->red;
pixel->green = srcPixel->green;
pixel->blue = srcPixel->blue;
};
};
};
};
pixel ++;
};
};
};
void getCoordinatesFromCharacter(char c, int * x, int * y){
if(c >= 'a' && c <= 'z'){
c -= ('a' - 'A');
};
if(c >= 'A' && c <= 'Z'){
*x = (c - ('A')) * CHARACTER_SIZE;
*y = bmp_font.infoHeader.biHeight - CHARACTER_SIZE;
} else if(c >= '0' && c <= '9'){
*x = (c - '0') * CHARACTER_SIZE;
*y = bmp_font.infoHeader.biHeight - 2 * CHARACTER_SIZE;
} else {
*y = bmp_font.infoHeader.biHeight - 2 * CHARACTER_SIZE;
switch(c){
case '.':{
*x = 10 * CHARACTER_SIZE;
break;
};
case ',':{
*x = 11 * CHARACTER_SIZE;
break;
};
case ':':{
*x = 12 * CHARACTER_SIZE;
break;
};
case '!':{
*x = 13 * CHARACTER_SIZE;
break;
};
case '?':{
*x = 14 * CHARACTER_SIZE;
break;
};
case '-':{
*x = 15 * CHARACTER_SIZE;
break;
};
case '+':{
*x = 16 * CHARACTER_SIZE;
break;
};
default:{
*x = 17 * CHARACTER_SIZE;
break;
};
};
};
};
void drawSingleLineText(int x, int y, char text[], int charSize, int length, uint8_t red, uint8_t green, uint8_t blue){
if(length < 1){
length = strlen(text);
};
float scale = (float) charSize / CHARACTER_SIZE;
Sprite character;
character.bitmap = &bmp_font;
character.xScale = scale;
character.yScale = scale;
character.centreX = 0;
character.centreY = 0;
character.isPlainColour = 1;
character.red = red;
character.green = green;
character.blue = blue;
int i;
for(i = 0; i < length; i++){
getCoordinatesFromCharacter(text[i], &character.startX, &character.startY);
character.endX = character.startX + CHARACTER_SIZE;
character.endY = character.startY + CHARACTER_SIZE;
drawSprite(x + i * scale * CHARACTER_SIZE, y, &character);
};
};
void drawMultiLineText(int x, int y, char text[], int charSize, int maxWidth, uint8_t red, uint8_t green, uint8_t blue){
if(charSize > maxWidth){
return;
};
int length = strlen(text);
int startIndex = 0;
int rowNumber = 0;
int charactersPerRow = maxWidth / charSize;
int i;
for(i = 0; i < length; i++){
if((i + 1 - startIndex) * charSize > maxWidth){
int endIndex = -1;
int j = i;
while(j >= 0 && j > i - charactersPerRow){
if(text[j] == ' '){
endIndex = j;
break;
};
j -= 1;
};
if(endIndex == -1){
endIndex = i - 1;
i -= 1;
} else {
i = endIndex;
};
drawSingleLineText(x, y - rowNumber * (charSize * 1.25), text + startIndex, charSize, (endIndex + 1 - startIndex), red, green, blue);
rowNumber += 1;
startIndex = endIndex + 1;
} else if(i == length - 1){
drawSingleLineText(x, y - rowNumber * (charSize * 1.25), text + startIndex, charSize, (i + 1 - startIndex), red, green, blue);
};
};
};
void drawRotatedSprite(int x, int y, float angle, Sprite * sprite){
int width = abs(sprite->endX - sprite->startX);
int height = abs(sprite->endY - sprite->startY);
float vertices[4][2];
vertices[0][0] = -sprite->centreX;
vertices[0][1] = -sprite->centreY;
vertices[1][0] = vertices[0][0];
vertices[1][1] = vertices[0][1] + height;
vertices[2][0] = vertices[1][0] + width;
vertices[2][1] = vertices[1][1];
vertices[3][0] = vertices[2][0];
vertices[3][1] = vertices[0][1];
int i;
for(i = 0; i < 4; i++){
vertices[i][0] *= sprite->xScale;
vertices[i][1] *= sprite->yScale;
};
float sinAngle = sin(angle);
float cosAngle = cos(angle);
for(i = 0; i < 4; i++){
float prevX = vertices[i][0];
float prevY = vertices[i][1];
vertices[i][0] = prevX * cosAngle - prevY * sinAngle;
vertices[i][1] = prevY * cosAngle + prevX * sinAngle;
};
for(i = 0; i < 4; i++){
vertices[i][0] += x;
vertices[i][1] += y;
};
int minX = vertices[0][0];
int maxX = vertices[0][0];
int minY = vertices[0][1];
int maxY = vertices[0][1];
for(i = 0; i < 4; i++){
if(vertices[i][0] < minX){
minX = vertices[i][0];
};
if(vertices[i][0] > maxX){
maxX = vertices[i][0];
};
if(vertices[i][1] < minY){
minY = vertices[i][1];
};
if(vertices[i][1] > maxY){
maxY = vertices[i][1];
};
};
Pixel * pixel;
for(i = minY; i < maxY; i++){
pixel = renderBuffer.pixels + i * BUFFER_WIDTH + minX;
int j;
for(j = minX; j < maxX; j++){
if(j >= 0 && j < BUFFER_WIDTH && i >= 0 && i < BUFFER_HEIGHT){
int pixelX = (j - x) * cosAngle + (i - y) * sinAngle;
int pixelY = (i - y) * cosAngle - (j - x) * sinAngle;
pixelX /= sprite->xScale;
pixelY /= sprite->yScale;
pixelX += sprite->centreX;
pixelY += sprite->centreY;
pixelX += sprite->startX;
pixelY += sprite->startY;
if(pixelX >= sprite->startX && pixelX < sprite->endX && pixelY >= sprite->startY && pixelY < sprite->endY){
Pixel * srcPixel = sprite->bitmap->pixels + pixelY * sprite->bitmap->infoHeader.biWidth + pixelX;
if(srcPixel->red != TRANSPARENT_RED || srcPixel->green != TRANSPARENT_GREEN || srcPixel->blue != TRANSPARENT_BLUE){
if(sprite->isPlainColour == 1){
pixel->red = sprite->red;
pixel->green = sprite->green;
pixel->blue = sprite->blue;
} else {
pixel->red = srcPixel->red;
pixel->green = srcPixel->green;
pixel->blue = srcPixel->blue;
};
};
};
};
pixel++;
};
};
};
void render(){
memset(renderBuffer.pixels, 0, BUFFER_WIDTH * BUFFER_HEIGHT * sizeof(Pixel));
drawRotatedSprite(BUFFER_WIDTH / 2, BUFFER_HEIGHT / 2, angle, &spr_test);
if(GetAsyncKeyState(VK_LEFT)){
angle += 0.01;
};
if(GetAsyncKeyState(VK_RIGHT)){
angle -= 0.01;
};
if(GetAsyncKeyState(VK_UP)){
spr_test.xScale += 0.01;
spr_test.yScale += 0.01;
};
if(GetAsyncKeyState(VK_DOWN)){
spr_test.xScale -= 0.01;
spr_test.yScale -= 0.01;
};
StretchDIBits(
renderBuffer.deviceContextHandle,
renderBuffer.windowClientWidth / 2 - (renderBuffer.scale * BUFFER_WIDTH) / 2,
renderBuffer.windowClientHeight / 2 - (renderBuffer.scale * BUFFER_HEIGHT) / 2,
BUFFER_WIDTH * renderBuffer.scale,
BUFFER_HEIGHT * renderBuffer.scale,
0,
0,
BUFFER_WIDTH,
BUFFER_HEIGHT,
renderBuffer.pixels,
&renderBuffer.bitmapInfo,
DIB_RGB_COLORS,
SRCCOPY
);
};
Running Our Program:
When we run our program, we should get a sprite that we can rotate and resize using the arrow keys. Below are screenshots from the program:
That's all for this tutorial series. You should now have the skills to write a simple software renderer in C.