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:
The techniques we cover in this lesson are not entirely stable or widely supported. There are two particularly notable problems:
If we’re relying on these techniques, we should ensure they behave as we expect on the platform we’re shipping on.
We cover more modern techniques to control the color correction of our program in the advanced course, but most of the concepts we cover in this lesson are still applicable to those more advanced techniques.
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
.
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
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.
In this lesson, we’ve combined the related concepts of brightness and luminosity to simplify explanations. However, the reality is more complex, and a dedicated field of study called color science.
Luminosity is the amount of physical energy emitted by a light source, such as a color being displayed on a monitor, whilst brightness is how that light is perceived by an observer.
More luminous sources are typically perceived as brighter but, due to the complexities of the human perception system, this relationship is not as straightforward as we might expect.
Color & Light published by 3dtotal is a highly recommended introduction to the theory and applications of this field.
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 -
Mathematically, the function looks like this:
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 and , regardless of the 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 .
It’s quite unintuitive that increasing a brightness setting will result in reduced brightness, so many APIs will invert this behavior in their public-facing design.
This includes the SDL API. For example, if we call SDL_SetBrightness(Window, 2.0)
, behind the scenes SDL will use the inverse of 2
, that is 1/2.0
, as the gamma value.
Applying a gamma value of 0.5
results in brighter colors, which is what we would intuitively expect when changing a brightness setting from 1
to 2
.
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),
};
}
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:
Gamma values other than 1.0
will change the shape of our curve, and the effect the corresponding function will have on our output:
We’re not restricted to using simple 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:
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:
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 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:
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
SDL_CalculateGammaRamp()
, or any other form of simple calculation. We can generate the 256-element array in any way we want, giving us complete control over the shape of our gamma curve.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:
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
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:
SDL_Window
pointer we want to queryEach 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);
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
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:
SDL_SetWindowBrightness()
for simple brightness adjustmentsSDL_SetWindowGammaRamp()
provides advanced color controlLearn how to control display brightness and gamma correction using SDL's window management functions
Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games