Wednesday, December 31, 2025
Color in programming
24‑bit color uses three 8‑bit channels—red, green, and blue. Allowing 256 intensity levels per channel. This yields 16,777,216 possible colors, often called “true color.”
/* 24‑bit RGB (8 bits per channel, no alpha) */
typedef struct {
unsigned char r; // red 0‑255
unsigned char g; // green 0‑255
unsigned char b; // blue 0‑255
} rgb24_t;
rgb24_t sky = {135, 206, 235}; // light sky blue
32‑bit color adds an extra 8‑bit channel to the 24‑bit RGB format. The fourth channel is typically an alpha (transparency) channel, giving each pixel 256 levels of opacity.
/* 32‑bit RGBA (8 bits per channel, includes alpha) */
typedef struct {
unsigned char r; // red
unsigned char g; // green
unsigned char b; // blue
unsigned char a; // alpha (0 = transparent, 255 = opaque)
} rgba32_t;
rgba32_t pixel = {255, 0, 0, 128}; // semi‑transparent red
OpenGL supports color components with more than 8 bits (256 levels) per channel. Modern OpenGL provides several higher‑precision formats:
| Bits per component |
Typical internal format |
Typical use case |
| 10 bits |
GL_RGB10_A2 |
HDR rendering, packed 10‑bit RGB + 2‑bit alpha |
| 11 bits |
GL_R11F_G11F_B10F |
HDR with floating‑point storage |
| 16 bits (fixed) |
GL_RGB16, GL_RGBA16 |
High‑precision color buffers, scientific visualization |
| 16 bits (float) |
GL_R16F, GL_RG16F, GL_RGB16F, GL_RGBA16F |
HDR rendering, tone‑mapping pipelines |
| 32 bits (float) |
GL_R32F, GL_RG32F, GL_RGB32F, GL_RGBA32F |
Full‑precision HDR, linear color pipelines |
| 64 bits (float) |
GL_R64F, GL_RG64F, GL_RGB64F, GL_RGBA64F |
Very high dynamic range, scientific computing |
Photoshop (most popular graphical editor) supports:
| OpenGL format (bits per component) |
Photoshop equivalent |
10 bits – GL_RGB10_A2 |
Not available; Photoshop does not expose a 10‑bit packed RGB + 2‑bit alpha mode. |
11 bits – GL_R11F_G11F_B10F |
Not available; Photoshop has no 11‑bit floating‑point channel layout. |
16 bits (fixed) – GL_RGB16, GL_RGBA16 |
16‑bit/channel (per‑channel integer) – Photoshop’s “16 Bits/Channel” mode matches these fixed‑point formats. |
16 bits (float) – GL_R16F, GL_RG16F, GL_RGB16F, GL_RGBA16F |
16‑bit/channel (per‑channel floating‑point) – Photoshop’s “16 Bits/Channel (Float)” mode (available in the 16‑bit workflow) corresponds to these floating‑point formats. |
32 bits (float) – GL_R32F, GL_RG32F, GL_RGB32F, GL_RGBA32F |
32‑bit/channel (per‑channel floating‑point) – Photoshop’s “32 Bits/Channel (Float)” mode (used for HDR/EXR files) matches these formats. |
64 bits (float) – GL_R64F, GL_RG64F, GL_RGB64F, GL_RGBA64F |
Not available; Photoshop does not provide a 64‑bit per‑channel mode. |
Modern AMD Ryzen systems (including those with integrated Radeon Graphics, e.g., Ryzen 5000/7000/8000/9000 series APUs, and discrete Radeon GPUs like RX 6000/7000/8000 series) fully support 32-bit floating-point formats such as GL_R32F, GL_RG32F, GL_RGB32F, GL_RGBA32F for textures, renderbuffers, and framebuffers in OpenGL.
These formats are part of the core OpenGL specification since version 3.0 (via ARB_color_buffer_float and related extensions), and AMD's drivers have supported them reliably for many years across Radeon hardware. They are widely used for HDR rendering, deferred shading, compute shaders, and other high-precision pipelines without issues on modern AMD GPUs.
However, 64-bit floating-point formats (double-precision) such as GL_R64F, GL_RG64F, GL_RGB64F, GL_RGBA64F are not supported on modern AMD Ryzen integrated graphics or consumer-grade Radeon GPUs (RDNA/RDNA2/RDNA3/RDNA4 architectures).
These formats require the ARB_gpu_shader_fp64 extension (for double-precision computations) and hardware-level support for double-precision textures/renderbuffers. AMD's consumer GPUs, including Ryzen APUs, lack native double-precision floating-point units at the required performance/level for OpenGL exposure. While some older professional FirePro cards or specific extensions provided limited/emulated support in the past, current Ryzen systems do not expose these 64-bit float formats. Attempting to use them will result in errors (e.g., framebuffer incomplete or unsupported internal format).
In summary:
| 32-bit float formats |
Fully supported and recommended for high-dynamic-range workflows. |
| 64-bit float formats |
Not available; use 32-bit floats instead, or switch to Vulkan/DirectX for potential alternatives on AMD hardware. |
Display Limitations
Physical panel capabilities: Virtually all consumer monitors (including HDR-capable ones) use panels with 8-bit or 10-bit per channel native depth (some premium professional displays reach 12-bit, but rarely higher). No consumer display panel natively supports 32-bit or 64-bit floating-point per channel. The panel physically modulates light in fixed integer steps (typically 256–1024 levels per channel for SDR, up to ~1024 for HDR10).
Output signal limitations: Modern connections (HDMI 2.1, DisplayPort 1.4+) and GPUs support at most 12-bit integer per channel output for HDR (e.g., via HDR10 or Dolby Vision signaling). OpenGL's default framebuffer or swapchain typically outputs in 8-bit or 10-bit integer formats. Even when using floating-point pixel formats for the window surface (via extensions like WGL_ARB_pixel_format_float on Windows), the actual signal sent to the monitor converts to integer bits—usually 10-bit for HDR.
Tone mapping requirement: 32-bit float formats allow unbounded HDR values (>1.0, even thousands for bright lights) with high precision. However, monitors cannot display values outside their brightness range (e.g., typical HDR monitors peak at 400–2000 nits). You must apply tone mapping (and often gamma/sRGB conversion) in a shader before presenting to the default framebuffer. Without this, bright areas clamp or appear blown out, and subtle gradients may band due to final integer output.
HDR-specific constraints: True HDR monitors require 10-bit signaling (often with scRGB or HDR10 metadata). Floating-point rendering excels internally for HDR pipelines (avoiding clamping during lighting calculations), but the final output to an HDR display still reduces to 10-bit integer per channel after tone mapping.
File support
| File Format |
Channels supported |
32‑bit float per channel? |
Typical use case |
Notes / Limitations |
| OpenEXR (.exr) |
R, RG, RGB, RGBA, multi‑channel |
Yes (full support) |
HDR rendering, VFX, film production, photography |
Industry standard for HDR. Supports 16‑bit half‑float & 32‑bit float. Very flexible (layers, metadata). |
| Radiance HDR (.hdr) |
RGB only |
Yes |
HDR photography, environment maps |
Older format, simpler than EXR. Only RGB (no alpha), no metadata layers. |
| TIFF (.tif/.tiff) |
R, RG, RGB, RGBA, multi‑channel |
Yes (with Float32) |
Professional photography, scientific imaging |
Must specify 32‑bit float samples. Not all software reads float TIFF correctly. |
| PNG |
RGB, RGBA |
No (only 8‑bit or 16‑bit integer) |
General web & graphics |
PNG does not support floating‑point. |
| JPEG / JPEG 2000 |
RGB, grayscale |
No |
Photos, web |
Lossy or integer‑only. No float support. |
| DDS (.dds) |
R, RG, RGB, RGBA |
Yes (with R32F, RG32F, RGB32F, RGBA32F) |
Real‑time graphics, games |
DirectDraw Surface. Used by DirectX/OpenGL apps. Good for textures. |
| KTX / KTX2 (.ktx/.ktx2) |
R, RG, RGB, RGBA |
Yes |
Textures for OpenGL/Vulkan |
Modern container for GPU textures. Supports float formats natively. |
| PFM (.pfm) |
RGB, grayscale |
Yes |
Simple HDR images, academic/research |
Very basic format, no compression, no alpha, no metadata. |
| ILM OpenEXR (deep) |
Multi‑layer / deep RGBA |
Yes |
Advanced VFX (deep compositing) |
Extension of OpenEXR for deep images (multiple samples per pixel). |
Neither WebP nor BMP supports 32-bit floating-point per channel (i.e., true GL_R32F, GL_RG32F, GL_RGB32F, or GL_RGBA32F formats with IEEE 754 single-precision floats)
GIMP (current stable versions, e.g., 2.10/3.0 series as of late 2025) and Krita (current versions, e.g., 5.2+) fully support 32-bit floating-point per channel workflows. This corresponds exactly to OpenGL's GL_R32F, GL_RG32F, GL_RGB32F, and GL_RGBA32F formats: single-precision IEEE 754 floating-point data per channel, allowing unbounded HDR values (above 1.0 or even negative) with high dynamic range and precision.
| Software |
Key 32‑bit Float Features |
| GIMP |
- Convert images to 32‑bit floating point via Image → Precision → 32‑bit floating point (Linear or Perceptual gamma).
- Internal processing always 32‑bit float → true HDR editing (exposure, curves, etc. without clipping).
- Import/Export 32‑bit float through OpenEXR (.exr) and TIFF (Float32).
- Ideal for HDR photography, scientific imaging, or high‑precision intermediates (higher RAM usage).
|
| Krita |
- Native 32‑bit float per channel (set when creating or converting documents).
- ~23‑24 bits effective precision in 0‑1 range with extended dynamic range for HDR/scene‑referenced work.
- Optimized for digital painting in HDR (OCIO color management, unbounded blending, values > 1.0).
- Robust OpenEXR (.exr) import/export, including multilayer support – great for VFX/matte‑painting pipelines.
- Also supports 16‑bit half‑float; 32‑bit float offers maximum precision at higher RAM/performance cost.
|
C examples
Here are C language structs that directly match the memory layout and size of OpenGL's 32-bit floating-point texture/internal formats (GL_R32F, GL_RG32F, GL_RGB32F, GL_RGBA32F), along with simple usage examples.
#include <stdint.h>
#include <stdio.h>
// 1 channel: GL_R32F → one 32-bit float
typedef struct {
float r;
} Float1;
// 2 channels: GL_RG32F → two 32-bit floats
typedef struct {
float r;
float g;
} Float2;
// 3 channels: GL_RGB32F → three 32-bit floats
typedef struct {
float r;
float g;
float b;
} Float3;
// 4 channels: GL_RGBA32F → four 32-bit floats
typedef struct {
float r;
float g;
float b;
float a;
} Float4;
Size and Alignment Check
printf("Size of Float1 (GL_R32F): %zu bytes\n", sizeof(Float1)); // 4
printf("Size of Float2 (GL_RG32F): %zu bytes\n", sizeof(Float2)); // 8
printf("Size of Float3 (GL_RGB32F): %zu bytes\n", sizeof(Float3)); // 12
printf("Size of Float4 (GL_RGBA32F): %zu bytes\n", sizeof(Float4)); // 16
Usage Example
// Example 1: Single red channel value (e.g., heightmap or distance field)
Float1 height = { .r = 1234.567f };
printf("Height value: %.3f\n", height.r);
// Example 2: 2D position or normal (x, y)
Float2 position = { .r = -5.5f, .g = 10.0f };
printf("Position: (%.2f, %.2f)\n", position.r, position.g);
// Example 3: HDR color without alpha
Float3 hdr_color = { .r = 10.5f, .g = 2.3f, .b = 0.8f }; // bright sunlight-like
printf("HDR Color: r=%.2f g=%.2f b=%.2f\n",
hdr_color.r, hdr_color.g, hdr_color.b);
// Example 4: Full HDR color with alpha
Float4 hdr_rgba = { .r = 15.0f, .g = 8.0f, .b = 3.0f, .a = 1.0f };
printf("HDR RGBA: r=%.2f g=%.2f b=%.2f a=%.2f\n",
hdr_rgba.r, hdr_rgba.g, hdr_rgba.b, hdr_rgba.a);
// Example 5: Array of pixels (e.g., for uploading to OpenGL texture)
Float4 pixels[4] = {
{ 1.0f, 0.0f, 0.0f, 1.0f }, // red
{ 0.0f, 1.0f, 0.0f, 1.0f }, // green
{ 0.0f, 0.0f, 1.0f, 1.0f }, // blue
{ 10.0f, 10.0f, 10.0f, 1.0f } // very bright white (HDR)
};
// In real OpenGL code, you would upload like this:
// glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, 2, 2, 0, GL_RGBA, GL_FLOAT, pixels);
SDL2 Example
// gcc -o float_texture main.c -lSDL2 -lGLEW -lGL
#include <SDL2/SDL.h>
#include <GL/glew.h> // or glad.h if you prefer GLAD
#include <stdio.h>
#include <stdlib.h>
#define WINDOW_WIDTH 800
#define WINDOW_HEIGHT 600
#define TEX_WIDTH 4
#define TEX_HEIGHT 4
// Simple vertex shader
const char* vertexShaderSource = R"(
#version 330 core
layout (location = 0) in vec2 aPos;
layout (location = 1) in vec2 aTexCoord;
out vec2 TexCoord;
void main() {
gl_Position = vec4(aPos, 0.0, 1.0);
TexCoord = aTexCoord;
}
)";
// Fragment shader with basic Reinhard tone mapping
const char* fragmentShaderSource = R"(
#version 330 core
in vec2 TexCoord;
out vec4 FragColor;
uniform sampler2D hdrTexture;
void main() {
vec4 color = texture(hdrTexture, TexCoord);
// Simple Reinhard tone mapping: color / (color + 1.0)
vec3 toned = color.rgb / (color.rgb + vec3(1.0));
// Gamma correction
toned = pow(toned, vec3(1.0/2.2));
FragColor = vec4(toned, color.a);
}
)";
GLuint compileShader(GLenum type, const char* source) {
GLuint shader = glCreateShader(type);
glShaderSource(shader, 1, &source, NULL);
glCompileShader(shader);
GLint success;
glGetShaderiv(shader, GL_COMPILE_STATUS, &success);
if (!success) {
char infoLog[512];
glGetShaderInfoLog(shader, 512, NULL, infoLog);
printf("Shader compilation failed:\n%s\n", infoLog);
exit(1);
}
return shader;
}
int main(int argc, char* argv[]) {
if (SDL_Init(SDL_INIT_VIDEO) < 0) {
printf("SDL_Init Error: %s\n", SDL_GetError());
return 1;
}
// Request OpenGL 3.3 core profile
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 3);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE);
// Important: Request floating-point framebuffer support (optional but helpful)
SDL_GL_SetAttribute(SDL_GL_RED_SIZE, 8);
SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE, 8);
SDL_GL_SetAttribute(SDL_GL_BLUE_SIZE, 8);
SDL_GL_SetAttribute(SDL_GL_ALPHA_SIZE, 8);
SDL_Window* window = SDL_CreateWindow("SDL2 + OpenGL 32-bit Float Texture",
SDL_WINDOWPOS_CENTERED,
SDL_WINDOWPOS_CENTERED,
WINDOW_WIDTH, WINDOW_HEIGHT,
SDL_WINDOW_OPENGL | SDL_WINDOW_SHOWN);
if (!window) {
printf("Window creation failed: %s\n", SDL_GetError());
SDL_Quit();
return 1;
}
SDL_GLContext context = SDL_GL_CreateContext(window);
if (!context) {
printf("OpenGL context creation failed: %s\n", SDL_GetError());
SDL_DestroyWindow(window);
SDL_Quit();
return 1;
}
// Initialize GLEW (or GLAD)
glewExperimental = GL_TRUE;
if (glewInit() != GLEW_OK) {
printf("GLEW init failed\n");
return 1;
}
// Compile shaders
GLuint vertexShader = compileShader(GL_VERTEX_SHADER, vertexShaderSource);
GLuint fragmentShader = compileShader(GL_FRAGMENT_SHADER, fragmentShaderSource);
GLuint shaderProgram = glCreateProgram();
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram);
GLint success;
glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success);
if (!success) {
char infoLog[512];
glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog);
printf("Program linking failed:\n%s\n", infoLog);
return 1;
}
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
// Fullscreen quad vertices (position + texcoord)
float quadVertices[] = {
// positions // tex coords
-1.0f, 1.0f, 0.0f, 1.0f,
-1.0f, -1.0f, 0.0f, 0.0f,
1.0f, -1.0f, 1.0f, 0.0f,
-1.0f, 1.0f, 0.0f, 1.0f,
1.0f, -1.0f, 1.0f, 0.0f,
1.0f, 1.0f, 1.0f, 1.0f
};
GLuint VAO, VBO;
glGenVertexArrays(1, &VAO);
glGenBuffers(1, &VBO);
glBindVertexArray(VAO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(quadVertices), quadVertices, GL_STATIC_DRAW);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void*)0);
glEnableVertexAttribArray(1);
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void*)(2 * sizeof(float)));
// Create GL_RGBA32F texture (4x4 for demo)
GLuint hdrTexture;
glGenTextures(1, &hdrTexture);
glBindTexture(GL_TEXTURE_2D, hdrTexture);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, TEX_WIDTH, TEX_HEIGHT, 0, GL_RGBA, GL_FLOAT, NULL);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
// Fill with HDR data (values > 1.0!)
float pixels[TEX_WIDTH * TEX_HEIGHT * 4] = {
5.0f, 0.1f, 0.1f, 1.0f, // bright red
0.1f, 4.0f, 0.1f, 1.0f, // bright green
0.1f, 0.1f, 8.0f, 1.0f, // bright blue
10.0f, 10.0f, 10.0f, 1.0f, // very bright white
0.5f, 0.5f, 0.5f, 1.0f, // gray
2.0f, 1.0f, 0.5f, 0.8f, // orange-ish with alpha
0.0f, 0.0f, 0.0f, 0.0f, // transparent black
1.0f, 1.0f, 1.0f, 1.0f, // normal white
// ... repeat or extend as needed
3.0f, 2.0f, 1.0f, 1.0f,
1.0f, 2.0f, 3.0f, 1.0f,
2.0f, 3.0f, 1.0f, 1.0f,
15.0f, 8.0f, 4.0f, 1.0f, // extremely bright
0.2f, 0.3f, 0.8f, 1.0f,
0.8f, 0.2f, 0.4f, 1.0f,
0.4f, 0.6f, 0.9f, 1.0f,
20.0f, 20.0f, 20.0f, 1.0f // blinding white
};
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, TEX_WIDTH, TEX_HEIGHT, GL_RGBA, GL_FLOAT, pixels);
// You can also create other formats:
// GLuint r32f_tex; glTexImage2D(..., GL_R32F, ...);
// GLuint rg32f_tex; glTexImage2D(..., GL_RG32F, ...);
// GLuint rgb32f_tex; glTexImage2D(..., GL_RGB32F, ...);
glUseProgram(shaderProgram);
glUniform1i(glGetUniformLocation(shaderProgram, "hdrTexture"), 0);
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
int running = 1;
SDL_Event event;
while (running) {
while (SDL_PollEvent(&event)) {
if (event.type == SDL_QUIT) running = 0;
}
glClearColor(0.1f, 0.1f, 0.1f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, hdrTexture);
glBindVertexArray(VAO);
glDrawArrays(GL_TRIANGLES, 0, 6);
SDL_GL_SwapWindow(window);
}
// Cleanup
glDeleteTextures(1, &hdrTexture);
glDeleteVertexArrays(1, &VAO);
glDeleteBuffers(1, &VBO);
glDeleteProgram(shaderProgram);
SDL_GL_DeleteContext(context);
SDL_DestroyWindow(window);
SDL_Quit();
return 0;
}