Rampa RGB vs OKLab
As mesmas duas cores misturadas de três jeitos, empilhadas para você ver por que rampas misturadas em RGB ficam barrentas ou desbotadas.
- 01ObjetivoCor
- 02TécnicaDegradês
- 03ExecuçãoRampa RGB vs OKLab
Parâmetros
Copie para a sua plataforma
- GLSLCopiar
- GodotCopiar
Cadeia de nós
Montada passo a passo, no estilo do GLSL, com os nomes de nós dele.
Montada passo a passo, no estilo do Godot, com os nomes de nós dele.
Este card ainda não tem versão para Unreal Engine 5, então os nós mostram os nomes do GLSL.
Este card ainda não tem versão para Unity, então os nós mostram os nomes do GLSL.
Este card ainda não tem versão para Blender, então os nós mostram os nomes do GLSL.
i.uv.xUV + VectorDecomposei.uv.xi.uv.xi.uv.x UV01 Posição na horizontal
mixColorFunc + Mix + ColorFuncmixmixmix Entrada02 Misturar os valores sRGB
mixMixmixmixmix Entrada03 Misturar luz linear
mixExpressionmixmixmix Entrada04 Misturar em OKLab
?:Compare + Switch?:?:?: Entrada05 Empilhar as três faixas
toOklab().xExpressiontoOklab().xtoOklab().xtoOklab().x Entrada06 Luminosidade percebida
Como funciona
Três rampas têm as mesmas duas pontas, azul à esquerda e amarelo à direita. Em cima: os valores sRGBA codificação padrão das cores de tela. Os valores guardados seguem uma curva próxima de uma potência 1/2.2 da luz, então precisam ser decodificados antes de iluminar ou mesclar. sRGB A codificação padrão das cores de tela. Os valores guardados seguem uma curva próxima de uma potência 1/2.2 da luz, então precisam ser decodificados antes de iluminar ou mesclar. 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.linear = ((c + 0.055) / 1.055)^2.4 for c > 0.04045mix acontece.
- Posição.
t = i.uv.xvai de 0 na borda esquerda a 1 na direita; todas as faixas misturam por ele. - Misturar os valores sRGB. Os parâmetros chegam lineares, então, para mostrar o mix ingênuo, o shader codifica as duas cores para sRGB, mistura os números codificados e decodifica o resultado de novo. Azul e amarelo são quase opostos, então no meio do caminho os três canais quase se anulam num cinza sem graça, e a faixa fica escura e suja ali: o meio barrento.
- Misturar luz linear.
mix(p_left, p_right, t)nos valores lineares é o que duas luzes sobrepostas fazem, então é o fisicamente correto. Só que o olho não responde à luz de forma linear: um pouco de luz somado a uma cor escura parece uma mudança grande. A rampa clareia rápido demais, e o meio dela é um lilás desbotado. - Misturar em OKLab.
toOklab()transforma cada cor em luminosidade L e dois eixos opostos, a (do verde ao vermelho) e b (do azul ao amarelo), escolhidos para que distâncias iguais pareçam diferenças iguais. O mix acontece ali, efromOklab()volta para RGB linear. Agora L muda num ritmo constante, e o meio é um cinza-azulado claro e limpo. Uma reta entre duas cores sRGB no OKLab pode sair um pouco do gamutO conjunto de cores que um espaço de cor ou uma tela consegue mostrar. Uma cor fora do gamut do sRGB tem um canal abaixo de 0 ou acima de 1 e precisa ser limitada ou trazida de volta para dentro.FundamentoGamut
O conjunto de cores que um espaço de cor ou uma tela consegue mostrar. Uma cor fora do gamut do sRGB tem um canal abaixo de 0 ou acima de 1 e precisa ser limitada ou trazida de volta para dentro.
Fontesdo sRGB, então o resultado é limitado.- Gamut (abre em uma nova aba)Referência
- CSS Color Module Level 4 (abre em uma nova aba)Referência
- Empilhar as faixas. Duas comparações com
i.uv.yescolhem qual mix cada terço do quad mostra, e uma emenda fina e escura separa as faixas. - Luminosidade percebida.
toOklab(ramp).xmede o quanto cada pixel parece claro. Com “Mostrar só a luminosidade” ligado, o shader desenha um cinza com essa mesma luminosidade (o L do OKLab de um cinza é a raiz cúbica do valor linear dele, então o cinza é L ao cubo). A faixa de baixo vira uma rampa uniforme de ponta a ponta; a um quarto do caminho, ela percorreu exatamente um quarto da diferença de luminosidade, enquanto a faixa sRGB percorreu cerca de um oitavo e a linear mais de um terço.
Nenhum espaço ganha em tudo. O linear é o certo para luz: brilhos, neblina, desfoque e mesclagem. O OKLab é o certo para escalas de cor, em que cada passo deve parecer a mesma quantidade de mudança. E matizes opostos passam perto do cinza em qualquer espaço quando se vai em linha reta; para manter a cor saturada, misture o ângulo do matiz, em OKLCH.
A comparação calcula as três rampas e duas conversões extras em cada pixel, por isso a contagem é alta. Uma rampa de
verdade converte as duas pontas uma vez, fora do shader ou como constantes, e faz por pixel só o mix, o fromOklab() e
o clamp.
Monte no GLSL
O código copiado é um shader de fragmento GLSL ES 3.00 completo para WebGL2. Desenhe-o num quad cujo shader de vértice
passe v_uv (e v_position, v_normal, v_view, que este card não lê), envie p_left e p_right como vec3
lineares e p_showLightness como bool, e desenhe. O main() limita o resultado e o codifica para sRGB.
Para uma rampa sua você só precisa de duas funções auxiliares, quatro matrizes e uma linha. Converta as duas pontas uma vez, na CPU ou como constantes, e então, por pixel:
vec3 color = clamp(fromOklab(mix(labA, labB, t)), 0.0, 1.0);
Fique com toOklab(), fromOklab() e as quatro matrizes; tire srgbEncode(), srgbDecode(), a escolha das faixas e
a vista de luminosidade, que existem só para a comparação. Para uma rampa com várias paradas, guarde cada parada em
OKLab e misture entre as duas paradas em volta de t.
Monte no Godot
O shader supõe o HDR 2D desligado (o padrão do projeto), então ele mesmo decodifica os uniforms de cor e codifica o resultado. Com Rendering > Viewport > HDR 2D ligado, faça u_toLinear() e u_toSrgb() devolverem c sem mudança.
A exportação é um shader canvas_item.
- Adicione um ColorRect do tamanho da comparação (ela fica melhor mais larga do que alta).
- Em Material, crie um ShaderMaterial com um Shader novo e cole o código.
- Em Shader Parameters, escolha as cores da esquerda e da direita e ligue ou desligue Show Lightness.
O mat3() do Godot recebe três colunas vec3, como o GLSL, então as matrizes e os produtos v * M passam sem mudança;
diferente do GLSL, ele recusa nove floats soltos, e por isso as matrizes estão escritas como vec3. Para uma rampa de
produção muitas vezes você nem precisa do shader: um recurso Gradient com o espaço de cor de interpolação em OKLab,
gravado numa GradientTexture1D e lido por t, dá a faixa de baixo por uma leitura de textura.
Ainda não há tutorial de Unreal Engine 5 para este card.
Ainda não há tutorial de Unity para este card.
Ainda não há tutorial de Blender para este card.
Código
#version 300 es// Gradients → RGB vs OKLab ramp, web GLSL (GLSL ES 3.00). From Creative Center, MIT license.precision highp float;uniform float u_time;uniform vec2 u_resolution; // this pass's output size in pixelsuniform vec3 p_left; // Left color: vec3(0.1, 0.25, 0.95), sRGB; upload it decoded to linearuniform vec3 p_right; // Right color: vec3(1.0, 0.85, 0.1), sRGB; upload it decoded to linearuniform bool p_showLightness; // Show lightness only: falsein vec2 v_uv;in vec3 v_position;in vec3 v_normal;in vec3 v_view;out vec4 fragColor;struct Inputs { vec2 uv; vec3 position; vec3 normal; vec3 view; float time; vec2 fragCoord; vec2 resolution;};vec3 u_toSrgb(vec3 c) { return mix(c * 12.92, 1.055 * pow(c, vec3(1.0 / 2.4)) - 0.055, step(0.0031308, c));}// OKLab's matrices, from Björn Ottosson's definition (linear sRGB in). Each vec3 is one row of his matrix, but mat3()// takes columns, so the vector multiplies from the left. Godot accepts only vec3 columns here, not nine floats.const mat3 RGB_TO_LMS = mat3( vec3(0.4122214708, 0.5363325363, 0.0514459929), vec3(0.2119034982, 0.6806995451, 0.1073969566), vec3(0.0883024619, 0.2817188376, 0.6299787005));const mat3 LMS_TO_LAB = mat3( vec3(0.2104542553, 0.7936177850, -0.0040720468), vec3(1.9779984951, -2.4285922050, 0.4505937099), vec3(0.0259040371, 0.7827717662, -0.8086757660));const mat3 LAB_TO_LMS = mat3( vec3(1.0, 0.3963377774, 0.2158037573), vec3(1.0, -0.1055613458, -0.0638541728), vec3(1.0, -0.0894841775, -1.2914855480));const mat3 LMS_TO_RGB = mat3( vec3(4.0767416621, -3.3077115913, 0.2309699292), vec3(-1.2684380046, 2.6097574011, -0.3413193965), vec3(-0.0041960863, -0.7034186147, 1.7076147010));// The input is a color between 0 and 1, so its cone responses are never negative and pow() needs no sign fixvec3 toOklab(vec3 rgb) { return pow(rgb * RGB_TO_LMS, vec3(1.0 / 3.0)) * LMS_TO_LAB;}vec3 fromOklab(vec3 lab) { vec3 lms = lab * LAB_TO_LMS; return (lms * lms * lms) * LMS_TO_RGB;}vec3 srgbEncode(vec3 c) { return mix(c * 12.92, 1.055 * pow(c, vec3(1.0 / 2.4)) - 0.055, step(0.0031308, c));}vec3 srgbDecode(vec3 c) { return mix(c / 12.92, pow((c + 0.055) / 1.055, vec3(2.4)), step(0.04045, c));}vec4 effect(Inputs i) { float t = i.uv.x; // What mixing two hex codes does: blend the encoded numbers, then decode, since everything here is linear vec3 encodedMix = srgbDecode(mix(srgbEncode(p_left), srgbEncode(p_right), t)); vec3 linearMix = mix(p_left, p_right, t); // sRGB's gamut isn't convex in OKLab, so a straight line between two of its colors can poke slightly outside it vec3 oklabMix = clamp(fromOklab(mix(toOklab(p_left), toOklab(p_right), t)), 0.0, 1.0); vec3 ramp = i.uv.y > 2.0 / 3.0 ? encodedMix : (i.uv.y > 1.0 / 3.0 ? linearMix : oklabMix); // A gray has OKLab lightness L when its linear value is L cubed, so this gray looks as bright as the ramp does float lightness = toOklab(ramp).x; vec3 shown = p_showLightness ? vec3(lightness * lightness * lightness) : ramp; float seam = min(abs(i.uv.y - 1.0 / 3.0), abs(i.uv.y - 2.0 / 3.0)); return vec4(shown * step(0.006, seam), 1.0);}void main() { Inputs i; i.uv = v_uv; i.position = v_position; // Interpolation shortens the vectors between vertices i.normal = normalize(v_normal); i.view = normalize(v_view); i.time = u_time; i.fragCoord = gl_FragCoord.xy; i.resolution = u_resolution; vec4 frag_out = effect(i); // The output is sRGB: clamped and encoded once, here, with no tone mapping; opaque, so alpha is 1 fragColor = vec4(u_toSrgb(clamp(frag_out.rgb, 0.0, 1.0)), 1.0);}// Gradients → RGB vs OKLab ramp, Godot 4. From Creative Center, MIT license.shader_type canvas_item;// Unshaded shows the effect's color as is, like the site's preview; scene lights don't touch itrender_mode unshaded;uniform vec3 left : source_color = vec3(0.1, 0.25, 0.95);uniform vec3 right : source_color = vec3(1.0, 0.85, 0.1);uniform bool showLightness = false;struct Inputs { vec2 uv; vec3 position; vec3 normal; vec3 view; float time; vec2 fragCoord; vec2 resolution;};// Godot's 2D renderer works in sRGB (HDR 2D off, the default), so the frame decodes color params and sRGB// textures to linear itself and encodes the result. With HDR 2D on, make these return c unchanged.vec3 u_toLinear(vec3 c) { return mix(c / 12.92, pow((c + 0.055) / 1.055, vec3(2.4)), step(0.04045, c));}vec3 u_toSrgb(vec3 c) { return mix(c * 12.92, 1.055 * pow(c, vec3(1.0 / 2.4)) - 0.055, step(0.0031308, c));}// OKLab's matrices, from Björn Ottosson's definition (linear sRGB in). Each vec3 is one row of his matrix, but mat3()// takes columns, so the vector multiplies from the left. Godot accepts only vec3 columns here, not nine floats.const mat3 RGB_TO_LMS = mat3( vec3(0.4122214708, 0.5363325363, 0.0514459929), vec3(0.2119034982, 0.6806995451, 0.1073969566), vec3(0.0883024619, 0.2817188376, 0.6299787005));const mat3 LMS_TO_LAB = mat3( vec3(0.2104542553, 0.7936177850, -0.0040720468), vec3(1.9779984951, -2.4285922050, 0.4505937099), vec3(0.0259040371, 0.7827717662, -0.8086757660));const mat3 LAB_TO_LMS = mat3( vec3(1.0, 0.3963377774, 0.2158037573), vec3(1.0, -0.1055613458, -0.0638541728), vec3(1.0, -0.0894841775, -1.2914855480));const mat3 LMS_TO_RGB = mat3( vec3(4.0767416621, -3.3077115913, 0.2309699292), vec3(-1.2684380046, 2.6097574011, -0.3413193965), vec3(-0.0041960863, -0.7034186147, 1.7076147010));// The input is a color between 0 and 1, so its cone responses are never negative and pow() needs no sign fixvec3 toOklab(vec3 rgb) { return pow(rgb * RGB_TO_LMS, vec3(1.0 / 3.0)) * LMS_TO_LAB;}vec3 fromOklab(vec3 lab) { vec3 lms = lab * LAB_TO_LMS; return (lms * lms * lms) * LMS_TO_RGB;}vec3 srgbEncode(vec3 c) { return mix(c * 12.92, 1.055 * pow(c, vec3(1.0 / 2.4)) - 0.055, step(0.0031308, c));}vec3 srgbDecode(vec3 c) { return mix(c / 12.92, pow((c + 0.055) / 1.055, vec3(2.4)), step(0.04045, c));}vec4 effect(Inputs i) { float t = i.uv.x; // What mixing two hex codes does: blend the encoded numbers, then decode, since everything here is linear vec3 encodedMix = srgbDecode(mix(srgbEncode(u_toLinear(left)), srgbEncode(u_toLinear(right)), t)); vec3 linearMix = mix(u_toLinear(left), u_toLinear(right), t); // sRGB's gamut isn't convex in OKLab, so a straight line between two of its colors can poke slightly outside it vec3 oklabMix = clamp(fromOklab(mix(toOklab(u_toLinear(left)), toOklab(u_toLinear(right)), t)), 0.0, 1.0); vec3 ramp = i.uv.y > 2.0 / 3.0 ? encodedMix : (i.uv.y > 1.0 / 3.0 ? linearMix : oklabMix); // A gray has OKLab lightness L when its linear value is L cubed, so this gray looks as bright as the ramp does float lightness = toOklab(ramp).x; vec3 shown = showLightness ? vec3(lightness * lightness * lightness) : ramp; float seam = min(abs(i.uv.y - 1.0 / 3.0), abs(i.uv.y - 2.0 / 3.0)); return vec4(shown * step(0.006, seam), 1.0);}void fragment() { Inputs i; // UV starts at the top-left; the effect expects GLSL's bottom-left i.uv = vec2(UV.x, 1.0 - UV.y); // A flat quad facing the viewer, the same as the site's 2D preview i.position = vec3(i.uv * 2.0 - 1.0, 0.0); i.normal = vec3(0.0, 0.0, 1.0); i.view = vec3(0.0, 0.0, 1.0); i.time = TIME; // FRAGCOORD's origin is the top-left too i.resolution = 1.0 / SCREEN_PIXEL_SIZE; i.fragCoord = vec2(FRAGCOORD.x, i.resolution.y - FRAGCOORD.y); vec4 frag_out = effect(i); COLOR = vec4(u_toSrgb(clamp(frag_out.rgb, 0.0, 1.0)), 1.0);}Ainda não há código de Unreal Engine 5 para este card.
Ainda não há código de Unity para este card.
Ainda não há código de Blender para este card.