HSV e OKLab
HSV para ajustes que as pessoas sabem nomear, como girar o matiz ou tirar saturação, e OKLab para misturas e rampas que parecem uniformes.
Como funciona
RGB é como a tela produz cor: três quantidades de luz. Não é como as pessoas descrevem cor, e os degraus dele não parecem uniformes ao olho. “Deixa um pouco mais laranja”, “menos saturado”, “o mesmo brilho, só que mais azul” são difíceis de escrever como mudanças em vermelho, verde e azul. Um espaço de cor é outro trio de números para as mesmas cores, escolhido para que alguns desses ajustes fiquem simples. Dois cobrem quase tudo o que um shader precisa: HSV para ajustes intuitivos, OKLab para misturas perceptualmente uniformes.
HSV: matiz, saturação, valor
O HSV reorganiza o cubo RGB em volta da diagonal dos cinzas. O matiz é o ângulo no círculo cromático (aqui de 0 a 1 em vez de 0 a 360°, vermelho em 0, verde em 1/3, azul em 2/3), a saturação é o quanto a cor se afasta do cinza, e o valor é o maior dos três canais.
vec3 rgbToHsv(vec3 c) {
float hi = max(c.r, max(c.g, c.b));
float lo = min(c.r, min(c.g, c.b));
float range = hi - lo;
float h = 0.0; // cinza não tem matiz; fica 0
if (range > 0.0) {
if (hi == c.r) h = mod((c.g - c.b) / range, 6.0);
else if (hi == c.g) h = (c.b - c.r) / range + 2.0;
else h = (c.r - c.g) / range + 4.0;
}
float s = hi > 0.0 ? range / hi : 0.0;
return vec3(h / 6.0, s, hi);
}
vec3 hsvToRgb(vec3 hsv) {
vec3 k = mod(vec3(5.0, 3.0, 1.0) + hsv.x * 6.0, 6.0); // o lugar de cada canal no círculo
vec3 ramp = clamp(min(k, 4.0 - k), 0.0, 1.0); // 1 onde o canal está apagado
return hsv.z * (1.0 - hsv.y * ramp);
}
Com essas duas, os ajustes comuns ficam com uma linha cada, e cada um dá a volta ou trava do jeito que o nome sugere:
vec3 hsv = rgbToHsv(color);
hsv.x = fract(hsv.x + 0.1); // gira o matiz um décimo da volta, voltando ao início depois de 1
hsv.y *= 0.5; // metade da saturação
vec3 edited = hsvToRgb(hsv);
O HSV é uma reorganização, não um modelo da visão, e isso aparece. O amarelo e o azul com saturação total têm o mesmo valor, mas não se parecem nada em brilho. O matiz é indefinido para os cinzas, então um pixel cinza não pode ser “girado”. E interpolar o matiz pede cuidado: o meio do caminho entre 0.9 e 0.1 é 0.0 pelo lado curto, não 0.5. O HSV foi pensado para valores codificados, do jeito que os programas de pintura o usam; aplicado a valores lineares, o mesmo ajuste parece diferente, então escolha um e use sempre o mesmo.
OKLab: degraus uniformes
O OKLabUm espaço de cor em que distâncias iguais parecem diferenças iguais: L é a luminosidade, a vai do verde ao vermelho e b do azul ao amarelo. Rampas misturadas nele ficam uniformes. OKLab Um espaço de cor em que distâncias iguais parecem diferenças iguais: L é a luminosidade, a vai do verde ao vermelho e b do azul ao amarelo. Rampas misturadas nele ficam uniformes.a vai do verde ao vermelho e b
do azul ao amarelo. Ele parte do RGB linear, então decodifique antes:
// Coeficientes da definição do OKLab por Ottosson (entrada em sRGB linear).
// Cada linha de três números abaixo é uma linha da matriz dele; o GLSL preenche a mat3 coluna por coluna, então o vetor vai à esquerda.
const mat3 RGB_TO_LMS = mat3(
0.4122214708, 0.5363325363, 0.0514459929,
0.2119034982, 0.6806995451, 0.1073969566,
0.0883024619, 0.2817188376, 0.6299787005);
const mat3 LMS_TO_LAB = mat3(
0.2104542553, 0.7936177850, -0.0040720468,
1.9779984951, -2.4285922050, 0.4505937099,
0.0259040371, 0.7827717662, -0.8086757660);
const mat3 LAB_TO_LMS = mat3(
1.0, 0.3963377774, 0.2158037573,
1.0, -0.1055613458, -0.0638541728,
1.0, -0.0894841775, -1.2914855480);
const mat3 LMS_TO_RGB = mat3(
4.0767416621, -3.3077115913, 0.2309699292,
-1.2684380046, 2.6097574011, -0.3413193965,
-0.0041960863, -0.7034186147, 1.7076147010);
vec3 cbrt3(vec3 x) { return sign(x) * pow(abs(x), vec3(1.0 / 3.0)); } // pow sozinho falha abaixo de 0
vec3 linearToOklab(vec3 rgb) { return cbrt3(rgb * RGB_TO_LMS) * LMS_TO_LAB; }
vec3 oklabToLinear(vec3 lab) {
vec3 lms = lab * LAB_TO_LMS;
return (lms * lms * lms) * LMS_TO_RGB;
}
O uso que mais compensa é a mistura. Uma rampa entre duas cores misturada em RGB pode afundar no brilho ou passar por um meio apagado e acinzentado. A mesma rampa misturada em OKLab muda a luminosidade por igual de uma ponta à outra:
vec3 mixOklab(vec3 a, vec3 b, float t) { // a, b e o resultado são RGB linear
return oklabToLinear(mix(linearToOklab(a), linearToOklab(b), t));
}
A forma polar, OKLCH, transforma a e b em croma length(lab.yz) e matiz atan(lab.z, lab.y). Funciona como o
HSV, mas com uma luminosidade que quer dizer o que diz: gire o matiz e o brilho fica onde está, e um conjunto de cores
com o mesmo L parece igualmente claro.
Alguns valores de OKLab não têm equivalente em RGB: aumente o croma o bastante e a conversão de volta devolve canais abaixo de 0 ou acima de 1. Trave o resultado com clamp ou baixe o croma até caber. Uma conversão custa dois produtos de matriz e uma raiz cúbica (ou um cubo) por canal em cada sentido: barato o bastante para todo pixel, mas quando as cores são constantes vale converter uma vez só.