RGB, sRGB, лінійне світло, HSL, HSV, CIE XYZ, CIELAB та ACEScg — формули перетворення, гамма-корекція та точні місця, де конвеєри WebGL і Three.js найчастіше помиляються з кольором.
RGB — це не один кольоровий простір, а ціла родина: три числа набувають сенсу лише тоді, коли зафіксовано набір первинних кольорів (хроматичність червоного, зеленого, синього) і передавальну функцію (як числові значення відображаються на інтенсивність світла). Веб, файли PNG/JPEG і більшість текстур використовують первинні кольори та передавальну функцію sRGB, визначені стандартом IEC 61966-2-1.
| Простір | Первинні кольори | Типове застосування |
|---|---|---|
| sRGB (кодований) | Гама Rec. 709 | Текстури, кольори UI, PNG/JPEG, canvas 2D |
| Лінійний sRGB | Гама Rec. 709 | Обчислення освітлення, блендинг у фреймбуфері |
| Display-P3 | Гама DCI-P3 | Дисплеї з розширеною гамою, HDR canvas |
| Rec. 2020 | Надширока гама | Мовлення 4K/8K, деякі HDR-конвеєри |
Текселя, збережений як #RRGGBB, пакує кожен канал у
діапазон $[0,255]$; перед будь-якими обчисленнями його треба
нормалізувати до $[0,1]$:
Інтенсивність світла складається лінійно — два фотони несуть удвічі більше енергії, ніж один, — але значення пікселів, закодовані у sRGB, не складаються так само. sRGB застосовує криву, наближену до степеня 2.2, щоб 8-бітні значення виділяли більше точності темним тонам, до яких око найчутливіше. Будь-яке освітлення, блендинг або генерація міп-мапів, виконані безпосередньо на закодованих sRGB-байтах, фізично некоректні й дають вимитий або каламутний результат.
Дешеве наближення, яке часто зустрічається в шейдерах — з похибкою до 1% — замінює кусково-задану криву простим степенем 2.2:
| Етап | Має виконуватись у |
|---|---|
| Обчислення освітлення / затінення | Лінійному просторі |
| Альфа-блендинг, MSAA-резолв | Лінійному просторі |
| Генерація міп-мапів / фільтрація текстур | Лінійному просторі |
| Фінальний фреймбуфер / вивід у swap-chain | Кодуванні sRGB |
| Зберігання дифузних / альбедо-текстур на диску | Кодуванні sRGB |
| Карти нормалей, шорсткості/металевості, дата-текстури | Лінійному просторі (без прапорця sRGB!) |
// GLSL: перетворення sRGB <-> лінійний для одного каналу
float srgbToLinear(float c) {
return c <= 0.04045
? c / 12.92
: pow((c + 0.055) / 1.055, 2.4);
}
float linearToSrgb(float c) {
return c <= 0.0031308
? c * 12.92
: 1.055 * pow(c, 1.0 / 2.4) - 0.055;
}
vec3 srgbToLinear(vec3 c) {
return vec3(srgbToLinear(c.r), srgbToLinear(c.g), srgbToLinear(c.b));
}
HSL (відтінок, насиченість, світлота) і HSV/HSB (відтінок, насиченість, яскравість) — це циліндричні перепараметризації RGB, створені для зручних людині палітр вибору кольору, а не для обчислень освітлення. Відтінок $H \in [0°,360°)$ спільний для обох; насиченість і вісь світлоти/яскравості відрізняються.
Чистий відтінок при $V=1, S=1$ у HSV повністю насичений і яскравий — але той самий відтінок у HSL досягає максимальної насиченості лише при $L=0.5$; при $L=1$ HSL завжди сходиться до білого незалежно від відтінку. Саме тому палітри HSL відчуваються «збалансованішими» для роботи з UI, а HSV — природнішим для художників, які змішують відтінки й тіні наче фарби.
// GLSL: відтінок [0,1] -> RGB (повна насиченість, повна яскравість)
vec3 hue2rgb(float h) {
vec3 p = abs(fract(vec3(h) + vec3(1.0, 2.0/3.0, 1.0/3.0)) * 6.0 - 3.0);
return clamp(p - 1.0, 0.0, 1.0);
}
vec3 hsv2rgb(vec3 hsv) {
vec3 rgb = hue2rgb(hsv.x);
return mix(vec3(1.0), rgb, hsv.y) * hsv.z;
}
CIE 1931 XYZ — це незалежний від пристрою кольоровий простір, виведений безпосередньо з кривих відгуку колбочок ока людини — будь-який інший кольоровий простір визначається через перетворення до або з XYZ. $Y$ несе яскравість (люмінансу); $X$ і $Z$ несуть хроматичну інформацію, яка сама по собі не має перцептивного сенсу.
XYZ все ще не є перцептивно рівномірним — числова відстань 0.01 в одній ділянці може виглядати однаково, а та сама відстань в іншій — разюче по-іншому. CIELAB (L*a*b*) було створено спеціально, щоб виправити це: рівні числові відстані приблизно відповідають рівним сприйнятим відмінностям кольору, тому саме цей простір використовують для метрик відмінності кольору ($\Delta E$) і мапінгу гами.
| Простір | Перцептивно рівномірний? | Типове застосування |
|---|---|---|
| CIE XYZ | Ні | Проміжний формат між кольоровими просторами |
| CIELAB | Приблизно | Відмінність кольору ΔE, мапінг друкованої гами |
| OKLab / OKLCH | Краще за LAB | Сучасні градієнти CSS, перцептивне обертання відтінку |
Корельована колірна температура (CCT), виміряна в кельвінах, описує колір ідеального випромінювача абсолютно чорного тіла при цій температурі. Нижчі значення кельвінів виглядають теплими/оранжевими (полум'я свічки ≈ 1900K), вищі — холодними/ синіми (похмуре небо ≈ 7000K і вище). «Точка білого» — це просто CCT, яку рендерер вважає нейтрально білою — D65 (~6504K) є стандартом для sRGB і Rec. 709.
| Джерело | Приблизна CCT |
|---|---|
| Полум'я свічки | 1900 K |
| Лампа розжарювання | 2700 K |
| Схід / захід сонця | 3000–4000 K |
| D50 (точка білого для друку / графічного дизайну) | 5003 K |
| D65 (точка білого sRGB / Rec. 709) | 6504 K |
| Похмуре денне світло | 7000–10 000 K |
У рендерері CCT зазвичай перетворюється на лінійний RGB- множник (наближення планківського локусу) і застосовується як тонування балансу білого до фінального освітленого зображення, або передається в колір спрямованого джерела світла для симуляції конкретного джерела освітлення.
Фізично коректні рендерери накопичують необмежену лінійну радіанс — яскраве джерело світла чи відблиск на дзеркальній поверхні може багаторазово перевищувати 1.0. Перш ніж таке HDR-зображення можна показати на SDR-дисплеї (стандартний динамічний діапазон), його потрібно тон-мапити у придатний для відображення діапазон $[0,1]$, а потім повторно закодувати в sRGB.
ACEScg — це лінійний робочий простір широкої
гами, що використовується всередині конвеєра Academy Color
Encoding System для кіно та високоякісного рендерингу
реального часу — він має первинні кольори ширші за Rec. 2020
саме для того, щоб проміжні обчислення освітлення не
обрізалися до фінального етапу тон-мапінгу. Налаштування
рендерера Three.js ACESFilmicToneMapping реалізує
наведене вище наближення Narkowicz.
// GLSL: наближення ACES filmic
vec3 acesFilmic(vec3 x) {
const float a = 2.51;
const float b = 0.03;
const float c = 2.43;
const float d = 0.59;
const float e = 0.14;
return clamp((x * (a * x + b)) / (x * (c * x + d) + e), 0.0, 1.0);
}
Починаючи з r152 Three.js за замовчуванням вмикає
THREE.ColorManagement: кольори, задані в
JavaScript (hex, CSS-рядки), трактуються як sRGB і автоматично
перетворюються на лінійні внутрішньо, а текстури потребують
явного тегу кольорового простору, щоб рендерер знав, чи їх
треба декодувати.
import * as THREE from 'three';
// Альбедо / дифузні текстури несуть колір, заданий людиною -> sRGB
const albedoMap = new THREE.TextureLoader().load('brick-albedo.jpg');
albedoMap.colorSpace = THREE.SRGBColorSpace;
// Карти нормалей / шорсткості / металевості / AO зберігають сирі дані -> лінійний (без декодування)
const normalMap = new THREE.TextureLoader().load('brick-normal.jpg');
normalMap.colorSpace = THREE.NoColorSpace;
// Кодування виводу рендерера + фільмовий тон-мапінг
renderer.outputColorSpace = THREE.SRGBColorSpace;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.0;
// Hex/CSS-кольори задаються в sRGB і автоматично перетворюються на лінійні
const mat = new THREE.MeshStandardMaterial({
map: albedoMap,
normalMap: normalMap,
color: 0xff6b35, // трактується ColorManagement як sRGB
});
Позначення текстури альбедо як NoColorSpace (або
застарілого encoding = LinearEncoding) пропускає
декодування sRGB, тож освітлення обчислюється на
гамма-кодованих значеннях — поверхні виглядають надто темними
й знебарвленими в тінях, надто пласкими в середніх тонах.
Позначення карти нормалей як SRGBColorSpace
спотворює векторні дані небажаною гамма-кривою, що дає явно
неправильне затінення та артефакти освітлення.
SRGB8, щоб GPU декодував перед
фільтрацією.
pow(color, 1.0/2.2) у шейдері
та одночасне встановлення
renderer.outputColorSpace = SRGBColorSpace
дають вимитий, надто яскравий результат — оберіть рівно один
етап кодування.
atan2.