Serebyte


Home | Tutorials | Portfolio

Win32 Software Renderer in C: Part 5 - Drawing a Line


Note: this tutorial series requires significant knowledge of the C programming language


In this tutorial we will look at drawing a line to our render buffer.




The Theory:

To draw a line, we will need to implement a drawLine function and call it in the render function. Our drawLine function will make use of the digital differential analyser algorithm.




The drawLine Function

Our drawLine function should be a void function. It should take seven parameters. The first two should be the x and y coordinates of the start of the line. They should be integers. The third and fourth should be the x and y coordinates of the end point of the line. These should also be integers. The last three should be the red, green and blue values of the colour, and should be unsigned 8-bit integers. We should put the drawLine function in the render.c file.


Our drawLine function will make use of the digital differential analyser algorithm. This algorithm works by finding the change in x (dx) and the change in y (dy). We then take the larger of the magnitudes (so we will need to find the absolute value of both) of the two distances (dy and dx) and step from one end to the other, one unit at the time. In the direction of the smaller of the two distances, we increase by the ratio of the smaller distance to the larger (i.e. the x per unit y whe dy is the larger value, and the y per unit x when dx is the larger value). Every step, we round to the nearest pixel and draw it. This is better explained with an example:

To implement DDA in our code, we first want to find the change in y and the change in x. We can do this with the lines "int dx = endX - startX;" and "int dy = endY - startY;". Next, we need to create three variables. Two are floating point numbers: the xStep and yStep. These are the values that we should move along the x and y axes each step. The last is an integer, called "totalSteps". This will store the total number of steps we will use to draw our line. We should set this to the absolute value of the largest of dy and dx.


Next, we want to check if the magnitude of dy or dx is larger. We can do this with the line "if(abs(dy) > abs(dx)){". If dy > dx, then we want to set the totalSteps variable to the absolute value of dy ("totalSteps = abs(dy);"), since we will be moving along by one unit (either up or down) along the y-direction. If dx > dy, we want to do this the other way around. We should set totalSteps to dx ("totalSteps = abs(dx);"). After this if statement, we want to set yStep to dy / totalSteps, and xStep to dx / totalSteps, using the lines "yStep = (float) dy / totalSteps;" and "xStep = (float) dx / totalSteps;". This is because, each step, we want to move along in each direction by the total distance to cover in that direction divided by the total number of steps.


After setting xStep and yStep, we want to create two floating pointer numbers, the x coordinate and the y coordinate. We should set these to startX and endX. We can do this with the lines "float x = startX;" and "float y = startY;". Next, we should create a pixel pointer to point to the pixel we will draw each step. We can do this with the line "Pixel * pixel;". Then we want to create a loop counter, which I will call "i". We then want to create a loop that will iterate for all steps. We can do this with the line "for(int i = 0; i <= totalSteps; i++){".


Inside our loop, we first want to check that the pixel at the coordiantes x and y is actually on screen (to avoid accessing protected memory). We can do this with the line: "if(x >= 0 && x < BUFFER_WIDTH && y >= 0 && y < BUFFER_HEIGHT){". If the current pixel is on screen, we want to set the pixel pointer to the pixel at the current x and y coordinates. We can do this with the line "pixel = renderBuffer.pixels + ((int) y) * BUFFER_WIDTH + (int) x;". Here, we are taking the start of the render buffer and adding on y (rounded down) lots of BUFFER_WIDTH pixels to get to the correct row, before adding on x pixels to get to the pixel we wish to draw to. Then, we can set the colours of the pixel that are pointer is pointing to. We should set the RGB values of said pixel to the RGB valeus we passed into our function. We can do this with the lines: "pixel->red = red;", "pixel->green = green;" and "pixel->blue = blue;". After our on-screen check if statement, we want to increase the x coordinate by xStep, and the y coordinate by yStep.


Now we just have to call our drawLine function in our render function, to have it execute each frame. I will do this with the line "drawLine(320, 240, 400, 400, 0, 255, 0);" but you can pass in whatever arguments you would like.




The Code:

The code for this section is shown below. I have not modified the main.c file. I have added the drawLine function declaration to the main.h file using the line "void drawLine(int startX, int startY, int endX, int endY, uint8_t red, uint8_t green, uint8_t blue);". As this is thye only modification to main.h, I have not included the main.h file below. Below is the code for our render.c file:


render.c

/*
 render.c

 This file contains all of the code used for rendering shapes, sprites, etc.

 This file is included into main.c directly, so we can still use global variables.
*/


//draw rectangle
void drawRectangle(int startX, int startY, int endX, int endY, uint8_t red, uint8_t green, uint8_t blue){
/*
  drawRectangle
  
  This function draws a rectangle to the render buffer.
  It takes seven parameters:
   -The x coordinate of the bottom left corner
   -The y coordinate of the bottom left corner
   -The x coordinate of the top right corner
   -The y coordinate of the top right corner
   -The red value of the pixel
   -The green value of the pixel
   -The blue value of the pixel
 */


//create pixel pointer
 Pixel * pixel;

//iterate from startY to endY (iterate through each row)
 int i = 0;
 int j = 0;

 for(i = startY; i <= endY; i++){
  /*
   The bottom left corner of the render buffer is (0,0).
   We need to set the pixel pointer to left side of rectangle on row i so that we
   can begin rendering the new row.
   Therefore, we want set the pixel pointer to renderBuffer.pixels + i * BUFFER_WIDTH + startX.
   renderBuffer.pixels marks the start of the buffer. We want to add a row (of width BUFFER_WIDTH) to our
   pixel pointer until we get to the row we want to draw on. Therefore, we want to move i rows
   up. We then add startX to move the pixel pointer to the left side of the rectangle on that row.
  */


  //set pixel pointer to left side of rectangle on current row
  pixel = renderBuffer.pixels + i * BUFFER_WIDTH + startX;
  
  //iterate from startX to endX and draw pixels on row
  for(j = startX; j <= endX; j++){
   //check if pixel is on screen 
   if(i >= 0 && i < BUFFER_HEIGHT && j >= 0 && j < BUFFER_WIDTH){
    //render pixel by setting red, green and blue values
    pixel->red = red;
    pixel->blue = blue;
    pixel->green = green;
   };
   
   //increment pixel
   pixel++;
  };
 };
};

//draw line
void drawLine(int startX, int startY, int endX, int endY, uint8_t red, uint8_t green, uint8_t blue){
//calculate changes in x and y
 int dx = endX - startX;
 int dy = endY - startY;

//individual steps for x and y
 float xStep;
 float yStep;
 int totalSteps = 0;

//check if dy > dx
 if(abs(dy) > abs(dx)){
  /*
  The total number of steps is equal to the total increase in y, as we are increasing y by 1 (or -1 if dy < 0) each time.
  We are increasing y by 1 (or -1 if dy < 0) because it dy is larger than dx in terms of magnitude, so we must choose to increment y
  to avoid skipping pixels. If we were to increase x by 1, we would get increases of y larger than 1 as dy/dx > 1. This would cause
  some pixels to be left out of our line.
  */

  totalSteps = abs(dy);
 } else {
  /*
  The total number of steps is equal to the total increase in x, as we are increasing x by 1 (or -1 if dx < 0) each time.
  We are increasing x by 1 (or -1 if dx < 0) because it dx is larger than dy in terms of magnitude, so we must choose to increment x
  to avoid skipping pixels. If we were to increase y by 1, we would get increases of x larger than 1 as dx/dy > 1. This would cause
  some pixels to be left out of our line.
  */

  totalSteps = abs(dx);
 };

//absolute value of gradient is steeper than 1, so increase by 1 along the y-axis each step
 yStep = (float) dy / totalSteps; //calculate the change in y per step
 xStep = (float) dx / totalSteps; //the increase in x per unit y (i.e. the amount to increase x by each step)

//create loop counter and set to 0
 int i = 0;

//create x and y values
 float x = startX;
 float y = startY;

//create pixel pointer - this will always point to the pixel to set
 Pixel * pixel;

//iterate for all steps
 for(int i = 0; i <= totalSteps; i++){
  //check that x and y are within bounds
  if(x >= 0 && x < BUFFER_WIDTH && y >= 0 && y < BUFFER_HEIGHT){
   //plot (x,y)
   //first, set pixel pointer to current pixel (equal to renderBuffer.pixels + y * BUFFER_WIDTH + x)
   //Note that we add y * BUFFER_WIDTH, as we are adding y rows of size BUFFER_WIDTH to the start of our pixel pointer to move it to the correct y-value
   pixel = renderBuffer.pixels + ((int) y) * BUFFER_WIDTH + (int) x;
   
   //set pixel colours
   pixel->red = red;
   pixel->green = green;
   pixel->blue = blue;
  };
  
  //increase x and y
  x += xStep;
  y += yStep;
 };
};

//render function
void render(){
//set all pixels to 0 red, 0 blue, 0 green
 memset(renderBuffer.pixels, 0, BUFFER_WIDTH * BUFFER_HEIGHT * sizeof(Pixel));

//render test line
 drawLine(320, 240, 400, 400, 0, 255, 0);

/*
  Send renderbuffer data to client area of window.
  We can do this with the StretchDIBits function.
  This takes many parameters, which are detailed below:
 */

 StretchDIBits(
  renderBuffer.deviceContextHandle, //a handle to the device context we wish to render to
  renderBuffer.windowClientWidth / 2 - (renderBuffer.scale * BUFFER_WIDTH) / 2, //the x coordinate of the top left coordinate of our buffer on the window client area
  renderBuffer.windowClientHeight / 2 - (renderBuffer.scale * BUFFER_HEIGHT) / 2, //the y coordinate of the top left coordinate of our buffer on the window client area
  BUFFER_WIDTH * renderBuffer.scale, //the width of the buffer on the window client area
  BUFFER_HEIGHT * renderBuffer.scale, //the height of the buffer on the window client area
  0, //the starting x coordinate on the source buffer to render from (we want to render all data, so this is 0)
  0, //the starting y coordinate on the source buffer to render from (we want to render all data, so this is 0)
  BUFFER_WIDTH, //the width of the source buffer
  BUFFER_HEIGHT, //the height of the source buffer
  renderBuffer.pixels, //a pointer to the actual data we want to send
  &renderBuffer.bitmapInfo, //a pointer to the bitmap info structure for our renderBuffer
  DIB_RGB_COLORS, //use RGB colours
  SRCCOPY //copy the source into the window's buffer
 );
};



Running the Program:

Running our code should produce the following output:


We should see this scale when we enter fullscreen mode:


That is all for this tutorial. In the next tutorial, we will look at drawing a triangle.