..
#gamedev

004 - How to Animate Raw Pixels on the Screen

In the previous post, I showed you how to allocate a back buffer containing a memory block that represents a pixel array.

In this post, I'm going to demonstrate how to animate that back buffer and set some pixels on the screen.

Improving Build script

Before starting, I changed the build script flags to add the full path to the Emacs compiler error. So, I can jump into the line of code that cause the error.

To do that, add into build.bat the flag -FC.

cl -nologo -FC -Zi ..\src\win32_myproject.cpp %LINKER_FLAGS%

The Back Buffer Struct

Let's define a struct that holds some values related to the back buffer:

struct Win32_Back_Buffer {
        int width;
        int height;
        int pitch;
        void* data;
        BITMAPINFO bitmap_info;
};

As you can see, the struct represents our memory with some properties like the memory itself, the pitch and the size of bitmap. Finally, the bitmap information, which is a struct for configuring the renderer.

The Render Gradient

The next step is to create a function that interates through X and Y axes and fill each 4 bytes per pixel, which is equivalent to the AA RR GG BB channels.

The functions calls render_gradient and the code should be:

static void
render_gradient(Win32_Back_Buffer *back_buffer, int x_offset, int y_offset)
{
        unsigned char *row = (unsigned char *) back_buffer->data;
        for (int y = 0; y < back_buffer->height; ++y) {
                unsigned int *pixel = (unsigned int *) row;
                for (int x = 0; x < back_buffer->width; ++x) {
                        unsigned char xx = (x + x_offset);
                        unsigned char yy = (y + y_offset);
                        *pixel = (xx << 8) | yy;
                        pixel++;
                }
                row += back_buffer->pitch;
        }
}

Note that we cast a memory block to an unsigned char (u8) pointer to start at the beginning of the array. Next, we iterate on the X axis, and set ARGB color based on the channel color. Each channel has a capacity of 0-255 bits.

So, to fill a single pixel, we must shift left by 16 bits for the red color, 8 bits for the green color, 0 shift for blue color and use the OR operator to merge them.

0xFF112233
  A R G B

To move to the next line of matrix, we use the pitch which represents how many bytes per row our buffer have.

Windows uses Little Endian Architecture. For this reason, in registers, it fills BB GG RR AA, while in memory, it fills AA RR GG BB

Typedefs

I don't remember if I already mentioned this before, but the way that I learn to program in C is to simplify some things like types using typedef.

The typedef is useful to naming another types and fix the size based on archicture of system. As you can see in the next block, it's much more easier to type and read the code:

#include <stdint.h>
typedef uint8_t  u8;
typedef uint32_t u32;
// ... another types

static void
render_gradient(Win32_Back_Buffer *back_buffer, int x_offset, int y_offset)
{
        u8 *row = (u8 *) back_buffer->data;
        for (int y = 0; y < back_buffer->height; ++y) {
                u32 *pixel = (u32 *) row;
                for (int x = 0; x < back_buffer->width; ++x) {
                        u8 xx = (x + x_offset);
                        u8 yy = (y + y_offset);
                        *pixel = (xx << 8) | yy;
                        pixel++;
                }
                row += back_buffer->pitch;
        }
}

Animating the Back Buffer

Refactor the code to create the back buffer and pass the parameters that it need.

BITMAPINFOHEADER *bmi_header = &back_buffer->bitmap_info.bmiHeader;
bmi_header->biSize   = sizeof(BITMAPINFOHEADER);
bmi_header->biWidth  = width;
bmi_header->biHeight = -height;
// another properties ...

Now, let's call the render_gradient function inside the main loop, and after processing message queue.

For X offset and Y offset parameters, I use temporary int value to animate the gradient.

Last but not least, I call update_window with the current window and swap the back buffer with the newly filled pixel array.

Win32_Back_Buffer back_buffer = {};
create_back_buffer(&back_buffer, WIDTH, HEIGHT);

HDC context = GetDC(window);

int x_offset = 0;
int y_offset = 0;
while(!should_quit) {
        process_message_queue();

        x_offset++;
        y_offset++;
        render_gradient(&back_buffer, x_offset, y_offset);

        update_window(&back_buffer, context, WIDTH, HEIGHT);
 }

Conclusion

Extract the data and bitmap_info variables to the struct called Win32_Back_Buffer. Also, add the width, height and pitch (width * bytes per pixel).

We can unify the BITMAPINFOHEADER property using a pointer.

Now, update the update_window and create_back_buffer to set these values.

Go back to the main-loop and between process queue message and update_window, add a new function called render_gradient to test our renderer with some piece of pixels according the x and y values.

Iterate the whole x-axis and y-axis to fill the pixels color with ARGB channels.

Next, add the x_ofsset and y_offset to increase for each frame and move the gradient to right and down as result.

Review

Extract the data and bitmap_info variables to the struct called Win32_Back_Buffer. Also, add the width, height and pitch (width * bytes per pixel).

We can unify the BITMAPINFOHEADER property using a pointer.

// before
BITMAPINFOHEADER bmi_header = {};
bmi_header.biSize   = sizeof(BITMAPINFOHEADER);
bmi_header.biWidth  = width;
bmi_header.biHeight = -height; // top-down
back_buffer.bitmap_info.bmiHeader = bmi_header;
// ...

// after
BITMAPINFOHEADER *bmi_header = &back_buffer->bitmap_info.bmiHeader;
bmi_header->biSize   = sizeof(BITMAPINFOHEADER);
bmi_header->biWidth  = width;
bmi_header->biHeight = -height; // top-down
// ...

Now, update the update_window and create_back_buffer to set these values.

Go back to the main-loop and between process queue message and update_window, add a new function called render_gradient to test our renderer with some piece of pixels according the x and y values.

Iterate the whole x-axis and y-axis to fill the pixel color with ARGB channels.

Next, add the x_ofsset and y_offset to increase for each frame and move the gradient to right and down as result.

My Quick Access

  • Create a struct of Win32_Back_Buffer;
  • Create a function render gradient to fill pixels based on X and Y offset;
  • Also, add the Win32_Back_Buffer as parameters;
  • Set the properties of bitmapinfo into back buffer;
  • Call render_gradient, update_window after process message queue and update the offset values to move the gradient at screen;