SDL3 Surfaces and Colors

Explore SDL3 surfaces, the canvases for drawing, understand pixel formats, colors, and set your window's background.

Ryan McCombe
Updated

In this lesson, we'll take our first steps into drawing graphics with SDL3. We'll learn about SDL Surfaces, the canvases we draw onto, understand how colors and pixel formats work, and use this knowledge to change the background color of our window.

Starting Point

In this lesson, we'll continue working on our main.cpp and Window.h files from the previous lesson. We've provided them below:

Files

src
Select a file to view its content

Surfaces

When we write graphical programs, we're essentially telling the computer how to color in a grid of dots on the screen. These dots are pixels, and a complete grid forms an image.

By showing many images in quick succession, we create animations and videos.

SDL uses the SDL_Surface type to manage these 2D grids of pixel colors. Think of an SDL_Surface as a digital canvas containing all the color information for an image, plus details like its width, height, and the specific way colors are encoded.

Window Surfaces

An SDL_Window isn't just an empty frame; it comes equipped with its own SDL_Surface that acts as its drawing board. This is the surface we'll manipulate to change what's displayed inside the window.

The function SDL_GetWindowSurface() gives us access to this surface. It takes the SDL_Window* as an argument and returns an SDL_Surface*.

Let's add a GetSurface() method to our Window class to simplify getting this pointer when we need it:

src/Window.h

#pragma once
#include <SDL3/SDL.h>

class Window {
public:
  Window(){
    SDLWindow = SDL_CreateWindow(
      "Hello Window",
      700, 300, 0
    );
  }

  SDL_Surface* GetSurface() const {
    return SDL_GetWindowSurface(SDLWindow);
  }

  Window(const Window&) = delete;
  Window& operator=(const Window&) = delete;
  
  ~Window() {
    if (SDLWindow && SDL_WasInit(SDL_INIT_VIDEO)) {
      SDL_DestroyWindow(SDLWindow);
    }
  }

private:
  SDL_Window* SDLWindow{nullptr};
};

Colors

To define a specific color in a digital format, we usually break it down into its core components. For display devices, the standard approach is to use the RGB model, which stands for Red, Green, and Blue. Each color we see on screen is a combination of these three primary light colors.

Each component (R, G, and B) can have a range of possible values, representing its intensity. By specifying a value for red, a value for green, and a value for blue, we precisely define a single color out of millions of possibilities.

Pixel Formats

Since computers store everything as numbers (ultimately sequences of binary bits), we need a standardized way to translate these numbers into the RGB color intensities we just discussed. This is where pixel formats come in.

A pixel format defines exactly how the color data for a single pixel is stored in memory - that is, how a color is converted to a sequence of bits, and how a sequence of bits is converted back to a color.

Example Pixel Formats

To understand how to convert a binary sequence to a color, and a color back to a binary sequence, a pixel format typically requires three main pieces of information:

  • Which channels are in the format
  • How many bits are assigned to each channel
  • What order the channels are in

Pixel formats are typically given names, and a common example is RGB888. All of the information we listed above is included in this short name. RGB888 has red (R), green (G), and blue (B) channels, in that order, and each channel is assigned to 8 bits.

8 bits can store one of 256 possible values, so RGB888 can have one of 256 different intensities for each of its red, green, and blue channels. Combined, this means an RGB888 pixel can be one of approximately 16.8 million colors (256×256×256256 \times 256 \times 256)

Even though a pixel in this format only needs 24 bits (8 + 8 + 8), for technical reasons related to memory alignment, we include 8 bits of unused space. As such, an RGB888 color (and generally any color requiring 24 bits) is stored in a 32 bit (4 byte) container:

We cover memory alignment and padding in a dedicated lesson later in the course.

Using SDL_MapRGB()

To create a color in a specific format, SDL provides the SDL_MapRGB() function to help us. It accepts 5 arguments:

  1. A pointer to the format details (SDL_PixelFormatDetails) we want the color to use. We can get this by passing an SDL_Surface's format member to SDL_GetPixelFormatDetails().
  2. An pointer to a color palette, which we'd provide if we were using a specific, limited palette of color options. This is rarely the case, so we can just pass a nullptr here. You can read an introduction to indexed colors and color palettes on Wikipedia, and the SDL_CreatePalette() documentation explains how to use this technique in SDL.
  3. The intensity of the red channel (from 0 to 255)
  4. The intensity of the green channel (from 0 to 255)
  5. The intensity of the blue channel (from 0 to 255)

So, for example, if we wanted to generate a red color to use on the window surface, we'd do this:

src/Window.h

// ...

class Window {
public:
  Window(){
    SDLWindow = SDL_CreateWindow(
      "Hello Window",
      700, 300, 0
    );

    // Get the window surface's pixel format details
    const SDL_PixelFormatDetails* FmtDetails{
      SDL_GetPixelFormatDetails(GetSurface()->format)
    };

    // Create a bright red color value
    // suitable for this surface
    Uint32 RedColor{SDL_MapRGB( 
      FmtDetails, 
      nullptr,    
      255, // Max intensity for Red 
      0,   // Zero intensity for Green 
      0    // Zero intensity for Blue 
    )}; 
  }

  // ...
};

Storing Colors as Unsigned Integers

When we have a small amount of arbitrary binary data, such as a color, it's fairly common to use a fixed-width unsigned integer to store that data. For example, SDL_MapRGB() returns its color as a Uint32:

const SDL_PixelFormatDetails* Fmt{
  SDL_GetPixelFormatDetails(GetSurface()->format)
};
Uint32 Red{SDL_MapRGB(Fmt, nullptr, 255, 0, 0)};
Uint32 Green{SDL_MapRGB(Fmt, nullptr, 0, 255, 0)};
Uint32 Blue{SDL_MapRGB(Fmt, nullptr, 0, 0, 255)};
Uint32 Yellow{SDL_MapRGB(Fmt, nullptr, 255, 255, 0)};

Even though this type suggests it's an integer, we never use it as one. We don't care what its total numeric value is, and we don't perform any arithmetic on it.

A fixed-width unsigned integer is just a convenient way to allocate a small block of memory of known size - 32 bits, in the case of Uint32.

Transparency / Alpha

Pixels can also include an alpha channel, representing how opaque the color is. A common pixel format that includes alpha is RGBA8888. It is similar to RGB888, except the 8 bits of previously-unused space now contains transparency data:

We'll work with alpha more later in the course when we start loading semi-transparent PNGs into programs. The SDL_MapRGBA() allows us to create colors that includes an alpha channel. It takes an additional argument, representing how opaque the color should be from 0 to 255. Below, we create a mostly-opaque red color:

const SDL_PixelFormatDetails* Fmt{
  SDL_GetPixelFormatDetails(GetSurface()->format)
};
Uint32 Color{SDL_MapRGBA(
  Fmt,
  nullptr,
  255, // Red channel
  0,   // Green Channel
  0,   // Blue Channel
  200  // Alpha Channel
)};

If the pixel format we provide as the first argument doesn't support alpha, our argument will effectively be ignored and our color will be fully opaque. This will generally be the case with window surface formats, as they rarely support alpha.

We can check if a format supports alpha by checking its Amask member:

const SDL_PixelFormatDetails* Fmt{
  SDL_GetPixelFormatDetails(
    SDL_GetWindowSurface(SDLWindow)->format
  )
};

if (Fmt->Amask) {
  std::cout << "Alpha is Supported";
} else {
  std::cout << "No Alpha Support";
}
No Alpha Support

Using SDL_FillSurfaceRect()

Let's use all of this to set the background color of our window. To fill a rectangular area of a surface with a solid color, we use the SDL_FillSurfaceRect() function. It accepts three arguments:

  1. A pointer to the surface we want to fill
  2. A pointer to the rectangle representing which area of the surface we want to fill. We'll cover rectangles in the next chapter but, for now, we'll pass a nullptr here. This indicates to SDL we want to fill the entire surface with our color
  3. The color we want to use

Let's add this to our Window constructor to make its background dark gray:

src/Window.h

// ...

class Window {
public:
  Window(){
    SDLWindow = SDL_CreateWindow(
      "Hello Window",
      700, 300, 0
    );

    const auto* Fmt{SDL_GetPixelFormatDetails(
      GetSurface()->format
    )};

    SDL_FillSurfaceRect(
      GetSurface(),
      nullptr,
      SDL_MapRGB(Fmt, nullptr, 50, 50, 50)
    );
  }

  // ...
};

To see our changes, we need to call SDL_UpdateWindowSurface(), passing a pointer to our SDL_Window*. We'll cover this function in more detail at the end of this chapter.

For now, let's just add it to our constructor, after the call to SDL_FillSurfaceRect():

src/Window.h

// ...

class Window {
public:
  Window(){
    SDLWindow = SDL_CreateWindow(
      "Hello Window",
      700, 300, 0
    );

    const auto* Fmt = SDL_GetPixelFormatDetails(
      GetSurface()->format
    );

    SDL_FillSurfaceRect(
      GetSurface(),
      nullptr,
      SDL_MapRGB(Fmt, nullptr, 50, 50, 50)
    );

    SDL_UpdateWindowSurface(SDLWindow);
  }

  // ...
};

After these changes, we should now see our window adopt the requested background color:

Complete Code

After the changes of this lesson, our completed files look like the following. We'll continue to build on these files in the next lesson.

Files

src
Select a file to view its content

Summary

In this lesson, we learned how SDL3 represents drawable areas using SDL_Surface. We saw how to access the specific surface associated with our window using SDL_GetWindowSurface().

We also explored how colors are defined using RGB components and how pixel formats dictate their storage in memory. Finally, we used SDL_MapRGB() to create a color value and SDL_FillSurfaceRect() to change our window's background, making sure to update the display with SDL_UpdateWindowSurface().

Key Takeaways:

  • Computer graphics involve drawing sequences of images made of pixels.
  • SDL_Surface represents a 2D grid of pixels in SDL.
  • Windows have an associated surface, obtainable via SDL_GetWindowSurface().
  • Colors are often represented by Red, Green, and Blue (RGB) channels.
  • Pixel formats define how color data is stored (channels, order, bits per channel).
  • SDL_MapRGB() converts RGB values into a single integer value suitable for a specific pixel format.
  • SDL_FillSurfaceRect() can fill a surface area with a solid color.
  • SDL_UpdateWindowSurface() makes changes to the window surface visible.
Next Lesson
Lesson 18 of 25

Detecting and Managing Errors in SDL3

Detecting and responding to SDL3 runtime errors, and learn about the updated error-handling patterns in the new library version.

Have a question about this lesson?
Answers are generated by AI models and may not be accurate