Brightness and Gamma

Learn how to control display brightness and gamma correction using SDL's window management functions

Ryan McCombe
Updated

Game developers need precise control over how their games appear on screen. In this lesson, we'll cover SDL's display management functions, learning how to adjust brightness, work with gamma curves, and implement color correction.

Players are often asked to configure the brightness of our game the first time they launch it. This is because the displays we develop the game on are likely to have different settings to what a player is using, and the environment they're playing in has different lighting to the environment where we made the game.

By letting players calibrate the brightness, it lets them get the experience closer to what we intended:

Brightness settings screen in Dishonored 2

Setting Display Brightness

The SDL_SetWindowBrightness() function can help us set the brightness of a window or, more typically, the display that the window is on. We pass the SDL_Window pointer as the first argument, and a floating point number representing the desired brightness as the second argument:

#include <SDL.h>

int main(int argc, char* argv[]) {
  SDL_Init(SDL_INIT_VIDEO);

  SDL_Window* Window{SDL_CreateWindow(
    "Window",
    SDL_WINDOWPOS_UNDEFINED,
    SDL_WINDOWPOS_UNDEFINED,
    800, 600, 0
  )};

  SDL_SetWindowBrightness(Window, 2.0);

  SDL_DestroyWindow(Window);
  SDL_Quit();
  return 0;
}

The default brightness is 1.0, whilst the range of acceptable values depends on the platform. Recent versions of Windows typically support values from 0.25 to 4.0.

Handling Errors When Setting Brightness

The SDL_SetWindowBrightness() function returns 0 if it was successful, or a negative error code otherwise. If the call fails, we can call SDL_GetError() for an explanation of what went wrong:

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

int main(int argc, char* argv[]) {
  SDL_Init(SDL_INIT_VIDEO);

  if (SDL_SetWindowBrightness(nullptr, 2.0) < 0) {
    std::cout << "Error Setting Brightness: "
      << SDL_GetError();
  }

  SDL_Quit();
  return 0;
}
Error Setting Brightness: Invalid window

Getting Display Brightness

The SDL_GetWindowBrightness() function lets us query the current brightness setting of a display. It returns a floating-point value representing the brightness level, where 1.0 represents normal brightness.

We pass the SDL_Window pointer as an argument:

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

int main(int argc, char* argv[]) {
  SDL_Init(SDL_INIT_VIDEO);

  SDL_Window* Window{SDL_CreateWindow(
    "Brightness Demo",
    SDL_WINDOWPOS_UNDEFINED,
    SDL_WINDOWPOS_UNDEFINED,
    800, 600, 0
  )};

  // Get initial brightness
  float InitialBrightness{
    SDL_GetWindowBrightness(Window)};

  std::cout << "Initial brightness: "
    << InitialBrightness << '\n';

  // Change brightness
  SDL_SetWindowBrightness(Window, 2.0);

  float NewBrightness{
    SDL_GetWindowBrightness(Window)};
    
  std::cout << "New brightness: "
    << NewBrightness << '\n';

  SDL_DestroyWindow(Window);
  SDL_Quit();
  return 0;
}
Initial brightness: 1
New brightness: 2

This can be particularly useful when implementing features like brightness sliders or automatic brightness adjustment based on game events.

Gamma

Conceptually, we can think of any color as having an associated brightness. This is easiest to visualise with grayscale colors, where colors closer to white have higher brightness values. We often represent these brightnesses on a scale from 0.0 to 1.0:

We can think of brightness adjustment as being a function that accepts the color as an argument, and returns a similar color with a different brightness:

float GetAdjustedColor(float Color) {
  float AdjustedColor{/* Calculate me */};
  return AdjustedColor;
}

To do this, we can define an additional floating point number. This number is applied as an exponent for the input color to calculate the desired output color. This exponent is often represented by the greek letter gamma - γ\gamma

Mathematically, the function looks like this:

output=inputγ output = input^{\gamma}

We can use SDL_pow() or the C++ standard library's std::powf() function to calculate exponents. Therefore, in C++, our function might look like this:

#include <iostream>

float GetAdjustedColor(float Color, float Gamma) {
  return std::powf(Color, Gamma);
  
  // Alternatively:
  // return SDL_pow(Color, Gamma);
}

int main() {
  std::cout
    << "Input: 0.0, Gamma: 2.0, Output: "
    << GetAdjustedColor(0, 2)
    << "\nInput: 0.5, Gamma: 2.0, Output: "
    << GetAdjustedColor(0.5, 2)
    << "\nInput: 0.5, Gamma: 0.5, Output: "
    << GetAdjustedColor(0.5, 0.5)
    << "\nInput: 1.0, Gamma: 2.0, Output: "
    << GetAdjustedColor(1, 2);
}
Input: 0.0, Gamma: 2.0, Output: 0
Input: 0.5, Gamma: 2.0, Output: 0.25
Input: 0.5, Gamma: 0.5, Output: 0.707107
Input: 1.0, Gamma: 2.0, Output: 1

This function has two notable characteristics. First, it doesn't change the range of our color values - the outputs are also in the range 0.0 to 1.0, as 0γ=00^{\gamma} = 0 and 1γ=11^{\gamma} = 1, regardless of the γ\gamma value we use.

Secondly, it means lower gamma values result in brighter output colors, due to the effect that exponents have on values in the range of 0.0 to 1.0.

For example, let's imagine we have an input color of 0.50.5.

  • Applying a gamma value of 22 will reduce this brightness, as 0.52=0.250.5^2 = 0.25.
  • Applying a gamma value of 0.50.5 will increase the brightness, as 0.50.50.710.5^{0.5} \thickapprox 0.71.

Whilst these examples have used grayscale colors, the same idea can be applied to all colors. The only difference is that, instead of representing our color by a single floating point number, we would have three, representing the red, green and blue channels:

A C++ type to represent a color, and a function to change it, might look something like this:

#include <cmath>

struct Color {
  float Red;
  float Green;
  float Blue;
};

Color GetAdjustedColor(Color C, float Gamma) {
  return {
    std::powf(C.Red, Gamma),
    std::powf(C.Green, Gamma),
    std::powf(C.Blue, Gamma),
  };
}

Gamma Curves

We can visualise the effect of different gamma correcting functions as a curve on a chart. Below, we plot the input values on the horizontal axis, and the corresponding output value of our gamma-correcting function on the vertical axis.

If our function returns the exact same value as its input (ie, the gamma value is 1.0), our function will be a straight line, and our output will be identical to our input:

A gamma curve with a gamma value of 1

Gamma values other than 1.0 will change the shape of our curve, and the effect the corresponding function will have on our output:

A gamma curve with a gamma value of 0.5 being used to increase image brightness

A gamma curve with a gamma value of 1

A gamma curve with a gamma value of 2 being used to reduce image brightness

We're not restricted to using simple output=inputγoutput = input^{\gamma} functions to define these ramps. Whilst these functions are useful if we just want to adjust the brightness, we can use gamma ramps for other effects too:

Using an S-shaped curve to increase image contrast

Using a gamma curve to invert the image

We can also have different gamma ramps for each of the red, green and blue components of a color. Below, we use this technique to reduce the amount of red in our output, whilst increasing the green and blue:

Using gamma curves to color-correct the image

Gamma Ramps in SDL

SDL refers to these gamma curves as ramps, and represents them as a C-style array of 256 unsigned integers, each having 16 bits (Uint16). This means that our luminance values are represented by integers from 0 to 65,535, the maximum value of a Uint16.

Whilst this makes the maths a little different from the more common 0.0 to 1.0 representation, SDL takes care of that for us, and the underlying concepts are the same.

We are free to use any technique we want to construct an array of 256 Uint16 values, but for the simple case where we want to implement an output=inputγoutput = input^{\gamma} function, the SDL_CalculateGammaRamp() function can help us.

We pass the gamma value we want to use, and the pointer to the 256-element array we want to populate:

Uint16 Ramp[256];
SDL_CalculateGammaRamp(1.0, Ramp);
std::cout << "Ramp from " << Ramp[0]
  << " to " << Ramp[255];
Ramp from 0 to 65535

We cover C-style arrays in more detail here:

C-Style Arrays

A detailed guide to working with classic C-style arrays within C++, and why we should avoid them where possible

Setting the Gamma Ramp

Once we have created our 256 integers representing a gamma ramp, we can apply it to our display using the SDL_SetWindowGammaRamp() function. We pass the SDL_Window* as the first argument, and the ramp to the second, third and fourth arguments, representing the red, green, and blue channel respectively.

Below, we recreate our previous example where we set the display's brightness to 2.0, except this time using the gamma-based approach:

#include <SDL.h>

int main(int argc, char* argv[]) {
  SDL_Init(SDL_INIT_VIDEO);

  SDL_Window* Window{SDL_CreateWindow(
    "Window",
    SDL_WINDOWPOS_UNDEFINED,
    SDL_WINDOWPOS_UNDEFINED,
    800, 600,
    SDL_WINDOW_SHOWN
  )};

  Uint16 Ramp[256];
  SDL_CalculateGammaRamp(2.0, Ramp);
  SDL_SetWindowGammaRamp(
    Window, Ramp, Ramp, Ramp);

  SDL_DestroyWindow(Window);
  SDL_Quit();
  return 0;
}

The SDL_SetWindowGammaRamp() gives us more flexibility than SDL_SetWindowBrightness() in two ways

  1. Our ramp doesn't need to come from SDL_CalculateGammaRamp(), or any other form of simple output=inputγoutput = input^{\gamma} calculation. We can generate the 256-element array in any way we want, giving us complete control over the shape of our gamma curve.
  2. Each color channel (red, green and blue) can use different gamma ramps, letting us completely change the look of our output, controlling how shadows, midtones and highlights are handled for each color.

Professional photo and video editing tools combine these techniques for their color grading workflows, allowing users to define gamma curves on a per-channel basis:

Curves Editor in DaVinci Resolve

Handling Errors When Setting Gamma

The SDL_SetWindowGammaRamp() function handles errors in a similar way to other SDL functions. It returns a negative error code if it encounters an error, and we can call SDL_GetError() for an explanation of what went wrong:

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

int main(int argc, char* argv[]) {
  SDL_Init(SDL_INIT_VIDEO);

  Uint16 Ramp[256];
  SDL_CalculateGammaRamp(2.0, Ramp);

  if (SDL_SetWindowGammaRamp(
    nullptr, Ramp, Ramp, Ramp) < 0
  ) {
    std::cout << "Error setting gamma ramp: "
      << SDL_GetError();
  }

  SDL_Quit();
  return 0;
}
Error setting gamma ramp: Invalid window

Getting the Gamma Ramp

We can retrieve the gamma ramp associated with a window or the display the window is on using the SDL_GetWindowGammaRamp() function. We pass four arguments:

  1. The SDL_Window pointer we want to query
  2. The memory location to update with the red channel ramp
  3. The memory location to update with the green channel ramp
  4. The memory location to update with the blue channel ramp

Each of the memory locations should have enough space to store a collection of 256 integers, each of 16 bits:

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

int main(int argc, char* argv[]) {
  SDL_Init(SDL_INIT_VIDEO);

  SDL_Window* Window{SDL_CreateWindow(
    "Window",
    SDL_WINDOWPOS_UNDEFINED,
    SDL_WINDOWPOS_UNDEFINED,
    800, 600, 0
  )};

  Uint16 Red[256];
  Uint16 Green[256];
  Uint16 Blue[256];
  SDL_GetWindowGammaRamp(
    Window, Red, Green, Blue);

  std::cout << "Red ramping from " << Red[0]
    << " to " << Red[255];

  SDL_DestroyWindow(Window);
  SDL_Quit();
  return 0;
}
Red ramping from 0 to 65535

We can pass nullptr values to any of the memory locations if we don't care about the ramp that is used in that specific channel:

Uint16 Red[256];
  
// Only get the red channel ramp
SDL_GetWindowGammaRamp(
  Window, Red, nullptr, nullptr);

Handling Errors When Getting Gamma

The SDL_GetWindowGammaRamp() also returns a negative error code if it encounters an error, and we can use the SDL_GetError() function for a description of that error:

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

int main(int argc, char* argv[]) {
  SDL_Init(SDL_INIT_VIDEO);

  Uint16 Red[256];
  if (SDL_GetWindowGammaRamp(
    nullptr, Red, nullptr, nullptr
  ) < 0) {
    std::cout << "Failed to get window ramp: "
      << SDL_GetError();
  }

  SDL_Quit();
  return 0;
}
Failed to get window ramp: Invalid window

Summary

In this lesson, we've explored SDL's display management capabilities, learning how to control brightness and implement gamma correction for our game graphics. Key takeaways:

  • Use SDL_SetWindowBrightness() for simple brightness adjustments
  • SDL_SetWindowGammaRamp() provides advanced color control
  • Gamma curves can be customized for specific visual effects
  • Each color channel can be controlled independently
  • Remember to to consider and handle errors when working with display functions
Next Lesson
Lesson 70 of 129

Pixel Density and High-DPI Displays

Learn how to create SDL applications that look great on modern displays across different platforms

Questions & Answers

Answers are generated by AI models and may not have been reviewed. Be mindful when running any code on your device.

Creating a Gamma Curve Visualisation
How can I draw a chart to visualise an SDL gamma ramp?
Create Smooth Brightness Transitions
How can I make the brightness change gradually over time instead of instantly?
Control Individual Window Brightness
Why does changing window brightness affect the whole display? Is there a way to only change my game's brightness?
Create Visual Effects with Gamma
Can I use gamma correction to create special effects like flashing lights or day/night transitions?
Create a Brightness Settings Slider
How do I implement a brightness slider in my game's settings menu?
Check Gamma Correction Support
How do I detect if the system supports gamma correction before trying to use it?
Save and Restore Brightness Settings
How can I save the player's preferred brightness settings and restore them the next time they launch the game?
Or Ask your Own Question
Purchase the course to ask your own questions