How to draw pixels in SDL 2.0?

Learn how to draw pixels in sdl 2.0? with practical examples, diagrams, and best practices. Covers c++, c, sdl development techniques with visual explanations.

Mastering Pixel Drawing in SDL 2.0 for C/C++ Applications

Hero image for How to draw pixels in SDL 2.0?

Learn the fundamental techniques for drawing individual pixels and manipulating surfaces directly in SDL 2.0, essential for custom rendering and graphics effects.

Drawing individual pixels is a foundational skill in computer graphics, especially when working with low-level libraries like SDL 2.0. While SDL provides higher-level primitives for drawing lines, rectangles, and textures, understanding how to manipulate individual pixels directly opens up possibilities for custom rendering algorithms, procedural generation, and unique visual effects. This article will guide you through the process of setting up an SDL window and renderer, accessing pixel data, and drawing pixels efficiently in C and C++.

Understanding SDL 2.0 Rendering Basics

Before diving into pixel manipulation, it's crucial to grasp SDL 2.0's rendering model. SDL 2.0 primarily uses a hardware-accelerated renderer, which is generally faster than direct software rendering. However, drawing individual pixels often requires direct access to a texture's pixel data, which can then be rendered by the hardware accelerator. The typical workflow involves creating a texture with SDL_TEXTUREACCESS_STREAMING or SDL_TEXTUREACCESS_STATIC, locking it to access its pixel buffer, modifying the pixels, unlocking it, and finally copying it to the renderer.

flowchart TD
    A[Initialize SDL] --> B[Create Window]
    B --> C[Create Renderer]
    C --> D[Create Streaming Texture]
    D --> E{Game Loop}
    E --> F[Lock Texture for Pixel Access]
    F --> G[Modify Pixel Data]
    G --> H[Unlock Texture]
    H --> I[Clear Renderer]
    I --> J[Copy Texture to Renderer]
    J --> K[Present Renderer]
    K --> E
    E --> L{Quit?}
    L -->|Yes| M[Destroy Resources]
    M --> N[Quit SDL]

SDL 2.0 Pixel Drawing Workflow

Setting Up the SDL Environment

To begin, you need to initialize SDL, create a window, and then create a renderer. The renderer is responsible for drawing to the window. For pixel manipulation, we'll also need an SDL_Texture that we can directly write to. This texture will be created with SDL_TEXTUREACCESS_STREAMING to allow frequent updates.

#include <SDL.h>
#include <iostream>

// Screen dimensions
const int SCREEN_WIDTH = 640;
const int SCREEN_HEIGHT = 480;

// Global variables for SDL window, renderer, and texture
SDL_Window* gWindow = nullptr;
SDL_Renderer* gRenderer = nullptr;
SDL_Texture* gTexture = nullptr;

bool init()
{
    // Initialize SDL
    if (SDL_Init(SDL_INIT_VIDEO) < 0)
    {
        std::cerr << "SDL could not initialize! SDL_Error: " << SDL_GetError() << std::endl;
        return false;
    }

    // Create window
    gWindow = SDL_CreateWindow("SDL Pixel Drawing", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, SCREEN_WIDTH, SCREEN_HEIGHT, SDL_WINDOW_SHOWN);
    if (gWindow == nullptr)
    {
        std::cerr << "Window could not be created! SDL_Error: " << SDL_GetError() << std::endl;
        return false;
    }

    // Create renderer for window
    gRenderer = SDL_CreateRenderer(gWindow, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC);
    if (gRenderer == nullptr)
    {
        std::cerr << "Renderer could not be created! SDL_Error: " << SDL_GetError() << std::endl;
        return false;
    }

    // Initialize renderer color
    SDL_SetRenderDrawColor(gRenderer, 0xFF, 0xFF, 0xFF, 0xFF);

    // Create streaming texture for pixel manipulation
    gTexture = SDL_CreateTexture(gRenderer, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_STREAMING, SCREEN_WIDTH, SCREEN_HEIGHT);
    if (gTexture == nullptr)
    {
        std::cerr << "Texture could not be created! SDL_Error: " << SDL_GetError() << std::endl;
        return false;
    }

    return true;
}

void close()
{
    // Destroy texture
    SDL_DestroyTexture(gTexture);
    gTexture = nullptr;

    // Destroy renderer
    SDL_DestroyRenderer(gRenderer);
    gRenderer = nullptr;

    // Destroy window
    SDL_DestroyWindow(gWindow);
    gWindow = nullptr;

    // Quit SDL subsystems
    SDL_Quit();
}

int main(int argc, char* args[])
{
    if (!init())
    {
        std::cerr << "Failed to initialize!" << std::endl;
    }
    else
    {
        // Main loop flag
        bool quit = false;

        // Event handler
        SDL_Event e;

        // While application is running
        while (!quit)
        {
            // Handle events on queue
            while (SDL_PollEvent(&e) != 0)
            {
                // User requests quit
                if (e.type == SDL_QUIT)
                {
                    quit = true;
                }
            }

            // Clear screen
            SDL_SetRenderDrawColor(gRenderer, 0x00, 0x00, 0x00, 0xFF); // Black background
            SDL_RenderClear(gRenderer);

            // Update screen
            SDL_RenderPresent(gRenderer);
        }
    }

    close();

    return 0;
}

Drawing Individual Pixels

To draw pixels, you need to lock the texture to gain direct access to its pixel buffer. SDL_LockTexture provides a pointer to the pixel data and the pitch (number of bytes in a row). You can then iterate through this buffer and set individual pixel colors. Remember to unlock the texture with SDL_UnlockTexture after you're done modifying it, so SDL can use it for rendering.

// Function to put a pixel on the texture
void putPixel(int x, int y, Uint32 color)
{
    if (gTexture == nullptr) return;

    void* pixels;
    int pitch;

    // Lock texture for direct pixel access
    if (SDL_LockTexture(gTexture, nullptr, &pixels, &pitch) != 0)
    {
        std::cerr << "Unable to lock texture! SDL_Error: " << SDL_GetError() << std::endl;
        return;
    }

    // Calculate the pixel's memory address
    // pitch is in bytes, so divide by sizeof(Uint32) to get pitch in pixels
    Uint32* pixelBuffer = static_cast<Uint32*>(pixels);
    pixelBuffer[y * (pitch / sizeof(Uint32)) + x] = color;

    // Unlock texture
    SDL_UnlockTexture(gTexture);
}

// Example usage in main loop (replace the existing main loop content):
// ... inside the while(!quit) loop ...

            // Clear screen
            SDL_SetRenderDrawColor(gRenderer, 0x00, 0x00, 0x00, 0xFF); // Black background
            SDL_RenderClear(gRenderer);

            // Draw a red pixel at (100, 100)
            putPixel(100, 100, 0xFFFF0000); // ARGB: Alpha=FF (opaque), Red=FF, Green=00, Blue=00

            // Draw a blue pixel at (150, 150)
            putPixel(150, 150, 0xFF0000FF);

            // Draw a green line using putPixel
            for (int i = 0; i < 50; ++i)
            {
                putPixel(200 + i, 200, 0xFF00FF00);
            }

            // Copy texture to renderer
            SDL_RenderCopy(gRenderer, gTexture, nullptr, nullptr);

            // Update screen
            SDL_RenderPresent(gRenderer);

// ... rest of main function ...

Optimized Pixel Drawing

For drawing many pixels, you should lock the texture once, get a pointer to the pixel buffer, and then directly manipulate the pixels. The pitch value is crucial here; it tells you how many bytes are in one row of the texture. You'll typically cast the void* pixels pointer to a Uint32* (for ARGB8888 format) and then access it like a 2D array, taking pitch into account.

// Optimized function to draw multiple pixels
void drawPixelsOptimized()
{
    if (gTexture == nullptr) return;

    void* pixels;
    int pitch;

    // Lock texture once for all pixel operations
    if (SDL_LockTexture(gTexture, nullptr, &pixels, &pitch) != 0)
    {
        std::cerr << "Unable to lock texture! SDL_Error: " << SDL_GetError() << std::endl;
        return;
    }

    Uint32* pixelBuffer = static_cast<Uint32*>(pixels);
    int pixelPitch = pitch / sizeof(Uint32);

    // Clear the texture to black (optional, if you want to redraw everything)
    for (int y = 0; y < SCREEN_HEIGHT; ++y)
    {
        for (int x = 0; x < SCREEN_WIDTH; ++x)
        {
            pixelBuffer[y * pixelPitch + x] = 0xFF000000; // Black
        }
    }

    // Draw a diagonal line (red)
    for (int i = 0; i < std::min(SCREEN_WIDTH, SCREEN_HEIGHT); ++i)
    {
        pixelBuffer[i * pixelPitch + i] = 0xFFFF0000; // Red
    }

    // Draw a checkerboard pattern (blue and green)
    for (int y = 0; y < SCREEN_HEIGHT; ++y)
    {
        for (int x = 0; x < SCREEN_WIDTH; ++x)
        {
            if ((x / 10 + y / 10) % 2 == 0)
            {
                pixelBuffer[y * pixelPitch + x] = 0xFF0000FF; // Blue
            }
            else
            {
                pixelBuffer[y * pixelPitch + x] = 0xFF00FF00; // Green
            }
        }
    }

    // Unlock texture after all modifications
    SDL_UnlockTexture(gTexture);
}

// Example usage in main loop (replace the existing putPixel calls):
// ... inside the while(!quit) loop ...

            // Clear screen
            SDL_SetRenderDrawColor(gRenderer, 0x00, 0x00, 0x00, 0xFF); // Black background
            SDL_RenderClear(gRenderer);

            // Call the optimized drawing function
            drawPixelsOptimized();

            // Copy texture to renderer
            SDL_RenderCopy(gRenderer, gTexture, nullptr, nullptr);

            // Update screen
            SDL_RenderPresent(gRenderer);

// ... rest of main function ...

Full Example: Drawing a Simple Gradient

Let's put it all together to draw a simple horizontal gradient across the screen. This demonstrates how to iterate through the pixel buffer and calculate colors dynamically.

#include <SDL.h>
#include <iostream>
#include <algorithm> // For std::min

// Screen dimensions
const int SCREEN_WIDTH = 640;
const int SCREEN_HEIGHT = 480;

// Global variables for SDL window, renderer, and texture
SDL_Window* gWindow = nullptr;
SDL_Renderer* gRenderer = nullptr;
SDL_Texture* gTexture = nullptr;

bool init()
{
    if (SDL_Init(SDL_INIT_VIDEO) < 0)
    {
        std::cerr << "SDL could not initialize! SDL_Error: " << SDL_GetError() << std::endl;
        return false;
    }

    gWindow = SDL_CreateWindow("SDL Gradient Drawing", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, SCREEN_WIDTH, SCREEN_HEIGHT, SDL_WINDOW_SHOWN);
    if (gWindow == nullptr)
    {
        std::cerr << "Window could not be created! SDL_Error: " << SDL_GetError() << std::endl;
        return false;
    }

    gRenderer = SDL_CreateRenderer(gWindow, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC);
    if (gRenderer == nullptr)
    {
        std::cerr << "Renderer could not be created! SDL_Error: " << SDL_GetError() << std::endl;
        return false;
    }

    SDL_SetRenderDrawColor(gRenderer, 0xFF, 0xFF, 0xFF, 0xFF);

    gTexture = SDL_CreateTexture(gRenderer, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_STREAMING, SCREEN_WIDTH, SCREEN_HEIGHT);
    if (gTexture == nullptr)
    {
        std::cerr << "Texture could not be created! SDL_Error: " << SDL_GetError() << std::endl;
        return false;
    }

    return true;
}

void close()
{
    SDL_DestroyTexture(gTexture);
    gTexture = nullptr;

    SDL_DestroyRenderer(gRenderer);
    gRenderer = nullptr;

    SDL_DestroyWindow(gWindow);
    gWindow = nullptr;

    SDL_Quit();
}

void drawGradient()
{
    if (gTexture == nullptr) return;

    void* pixels;
    int pitch;

    if (SDL_LockTexture(gTexture, nullptr, &pixels, &pitch) != 0)
    {
        std::cerr << "Unable to lock texture! SDL_Error: " << SDL_GetError() << std::endl;
        return;
    }

    Uint32* pixelBuffer = static_cast<Uint32*>(pixels);
    int pixelPitch = pitch / sizeof(Uint32);

    for (int y = 0; y < SCREEN_HEIGHT; ++y)
    {
        for (int x = 0; x < SCREEN_WIDTH; ++x)
        {
            // Calculate color based on x position (red gradient)
            Uint8 red = (Uint8)(255.0f * x / SCREEN_WIDTH);
            Uint32 color = (0xFF << 24) | (red << 16) | (0x00 << 8) | 0x00; // ARGB
            pixelBuffer[y * pixelPitch + x] = color;
        }
    }

    SDL_UnlockTexture(gTexture);
}

int main(int argc, char* args[])
{
    if (!init())
    {
        std::cerr << "Failed to initialize!" << std::endl;
    }
    else
    {
        bool quit = false;
        SDL_Event e;

        // Draw the gradient once
        drawGradient();

        while (!quit)
        {
            while (SDL_PollEvent(&e) != 0)
            {
                if (e.type == SDL_QUIT)
                {
                    quit = true;
                }
            }

            SDL_SetRenderDrawColor(gRenderer, 0x00, 0x00, 0x00, 0xFF);
            SDL_RenderClear(gRenderer);

            // Copy the texture to the renderer
            SDL_RenderCopy(gRenderer, gTexture, nullptr, nullptr);

            SDL_RenderPresent(gRenderer);
        }
    }

    close();

    return 0;
}