SDL2 Textures

Our previous reading introduced the SDL2 library, which gives us a portable way to create windows and draw graphics from C. We started off using SDL’s surfaces to create graphics, but this reading will introduce a new method: textures.

The main difference between surfaces and textures is where the image data is stored. Surfaces live entirely on the computer’s main memory, but textures are stored on a graphics card. That has important consequences for our programs: when we manipulate surfaces we are using the CPU, but manipulating textures using SDL functions can be accelerated by special graphics hardware.

SDL gives us some useful functions for drawing and filling shapes on textures, which is the main reason we’re moving on to textures now. Using the graphics card will also improve the performance of our graphics code, but we’re far from needing sophisticated hardware acceleration for the basic graphics we’re producing so far.

Getting Started

We’ll work through the example code at https://git.cs.grinnell.edu/csc161-s25/SDL2-texture-demo. This code is based on the code we produced in the prior reading. You can clone this repository or follow along if you’d like.

Setting Up

Before we can use textures, we need to set up a renderer. SDL represents graphics hardware as a renderer, although we can also create a renderer that does not use any special graphics hardware. This code sets up a renderer, and will replace the call to SDL_WindowGetSurface from our previous example:

// Create a renderer for the window
SDL_Renderer* renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED);
if (renderer == NULL) {
  // Something went wrong. Log and error and shut down
  fprintf(stderr, "Failed to create SDL renderer: %s\n", SDL_GetError());
  SDL_DestroyWindow(window);
  SDL_Quit();
  exit(EXIT_FAILURE);
}

When the program shuts down we will need to clean up by calling SDL_DestroyRenderer(renderer). Make sure you also include this call in any other error handling code after this point.

Creating a Texture

Once we have a renderer created, we can load an image and generate a texture. Images are loaded as surfaces (remember, they are stored in the program’s memory) but then we can convert them to textures. That conversion process will transfer the pixel data to the graphics hardware, and it could also change the way colors are represented depending on the formats the renderer expects. Luckily, SDL handles all of the details for us so we can just run this code to load an image and create a texture:

// Load an image
SDL_Surface* ball_surface = IMG_Load("ball.png");
if (ball_surface == NULL) {
  fprintf(stderr, "Failed to load image: %s\n", SDL_GetError());
  SDL_DestroyRenderer(renderer);
  SDL_DestroyWindow(window);
  SDL_Quit();
  exit(EXIT_FAILURE);
}

// Create a texture from the loaded surface
SDL_Texture* ball = SDL_CreateTextureFromSurface(renderer, ball_surface);
if (ball == NULL) {
  fprintf(stderr, "Failed to create texture: %s\n", SDL_GetError());
  SDL_FreeSurface(ball_surface);
  SDL_DestroyRenderer(renderer);
  SDL_DestroyWindow(window);
  SDL_Quit();
  exit(EXIT_FAILURE);
}

// Now that we have a texture we are done with the surface
SDL_FreeSurface(ball_surface);

We’ll need to release the texture when our program exits by calling SDL_DestroyTexture(ball).

Using the Renderer

Now that we have a renderer and texture set up, we’ll need to update the actual drawing code. All the code that manages game state or handles input will remain the same, but the code that draws the graphics will all need to change. We’ll work through those changes step-by-step now.

Clearing the Screen

To clear the screen, we’ll set the renderer’s current color and then call SDL_RenderClear. This code clears the screen to a grassy green color:

// Clear the screen
SDL_SetRenderDrawColor(renderer, 32, 160, 32, 255);
SDL_RenderClear(renderer);

Many of the renderer operations draw using the renderer’s current color, which the code above sets. Colors are specified in RGBA format, with each component having a value from 0–255. The A (alpha) component sets transparency, but using transparency requires some additional setup. You can find the relevant operations documented on the SDL wiki renderer API.

Drawing a Texture

Next, we’ll use the renderer to draw the ball texture. This code is similar to the old version, but we’ll use SDL_RenderCopy instead:

// Draw the ball
SDL_Rect dest = {
  .x = ball_pos.x - BALL_SIZE / 2, 
  .y = ball_pos.y - BALL_SIZE / 2, 
  .w = BALL_SIZE, 
  .h = BALL_SIZE
};
SDL_RenderCopy(renderer, ball, NULL, &dest);

Updating the Window

Finally, we need to tell SDL that we want the renderer to display its output in the window:

// Update the display
SDL_RenderPresent(renderer);

Drawing Shapes with SDL

Now that we’ve migrated our drawing code to use the SDL renderer, we also have the option of drawing points, lines, rectangles, and arbitrary polygons with the graphics hardware. While these seem like low-level operations, they are the basic building blocks for hardware-accelerated 2D and 3D graphics. We’ll use the rectangle drawing code to add a white outline to the soccer field we’re drawing.

We could call SDL_DrawRect to draw a rectangle with a one pixel outline, but we probably want a wider line. You could accomplish this by drawing multiple rectangles next to each other, but a faster approach would be to fill a rectangle with white pixels, then fill a slightly smaller rectangle with green pixels. Here’s the code to do just that, which you’ll find in the demo as well:

// Draw the field outline in white
SDL_Rect field_outline = {
  .x = XMARGIN,
  .y = YMARGIN,
  .w = SCREEN_WIDTH - 2 * XMARGIN,
  .h = SCREEN_HEIGHT - 2 * YMARGIN
};
SDL_SetRenderDrawColor(renderer, 255, 255, 255, 255);
SDL_RenderFillRect(renderer, &field_outline);

// Draw the field in green
SDL_Rect field = {
  .x = XMARGIN + LINESIZE,
  .y = YMARGIN + LINESIZE,
  .w = SCREEN_WIDTH - 2 * XMARGIN - 2 * LINESIZE,
  .h = SCREEN_HEIGHT - 2 * YMARGIN - 2 * LINESIZE
};
SDL_SetRenderDrawColor(renderer, 32, 160, 32, 255);
SDL_RenderFillRect(renderer, &field);

This code relies on some constants defined at the top of the file:

#define XMARGIN 20
#define YMARGIN 40
#define LINESIZE 5

It’s important that we do this before drawing the ball, otherwise the field and outline will cover the ball.

Reading Questions

We will explore more of the SDL renderer interface in our lab. To prepare for class, consider the following questions:

  1. SDL has functions to draw a single shape (e.g. SDL_DrawRect) or an array of multiple shapes of the same type (SDL_DrawRects). Why do you think the library would include both?

  2. There is no SDL function to draw a circle. How could you use points, lines, rectangles, or polygons to create a circle?