Last active
May 21, 2025 00:27
-
-
Save partybusiness/c59460fc2d475672c8a09399b4853b1d to your computer and use it in GitHub Desktop.
Trying out different ways of blending colours in different colour spaces.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
shader_type canvas_item; | |
uniform sampler2D textureA:source_color; | |
uniform sampler2D textureB:source_color; | |
uniform float blend:hint_range(0.0, 1.0, 0.01) = 0.5; | |
// lowers saturation for colours that pass through the centre of hue space | |
uniform float pow_comp:hint_range(0.0, 10.0, 0.01) = 1.0; | |
// https://github.com/scanberg/viamd/blob/da84469033c799e0a0035fbf96919f1eb84fa808/src/color_utils.h#L67 | |
vec3 hcl_to_rgb(vec3 HCL) { | |
if (HCL.r < 0.0) | |
HCL.r += 1.0; | |
const float HCLgamma = 3.0; | |
const float HCLy0 = 100.0; | |
const float HCLmaxL = 0.530454533953517f; // == exp(HCLgamma / HCLy0) - 0.5 | |
vec3 RGB = vec3(0.0); | |
if (HCL.z != 0.0) { | |
float H = HCL.x; | |
float C = HCL.y; | |
float L = HCL.z * HCLmaxL; | |
float Q = exp((1.0 - C / (2.0 * L)) * (HCLgamma / HCLy0)); | |
float U = (2.0 * L - C) / (2.0 * Q - 1.0); | |
float V = C / Q; | |
float T = tan((H + min(fract(2.0 * H) / 4.0, fract(-2.0 * H) / 8.0)) * PI * 2.0); | |
H *= 6.0; | |
if (H <= 1.0) { | |
RGB.r = 1.0; | |
RGB.g = T / (1.0 + T); | |
} else if (H <= 2.0) { | |
RGB.r = (1.0 + T) / T; | |
RGB.g = 1.0; | |
} else if (H <= 3.0) { | |
RGB.g = 1.0; | |
RGB.b = 1.0 + T; | |
} else if (H <= 4.0) { | |
RGB.g = 1.0 / (1.0 + T); | |
RGB.b = 1.0; | |
} else if (H <= 5.0) { | |
RGB.r = -1.0 / T; | |
RGB.b = 1.0; | |
} else { | |
RGB.r = 1.0; | |
RGB.b = -T; | |
} | |
RGB = RGB * V + U; | |
} | |
return RGB; | |
} | |
vec3 rgb_to_hcl(vec3 rgb) { | |
const float HCLgamma = 3.0; | |
const float HCLy0 = 100.0; | |
const float HCLmaxL = 0.530454533953517f; // == exp(HCLgamma / HCLy0) - 0.5 | |
vec3 HCL; | |
float H = 0.0; | |
float U = min(rgb.x, min(rgb.g, rgb.b)); | |
float V = max(rgb.x, max(rgb.g, rgb.b)); | |
float Q = HCLgamma / HCLy0; | |
HCL.y = V - U; | |
if (HCL.y != 0.0) { | |
H = atan(rgb.g - rgb.b, rgb.x - rgb.g) / 3.1515926535f; | |
Q *= U / V; | |
} | |
Q = exp(Q); | |
HCL.x = fract(H / 2.0 - min(fract(H), fract(-H)) / 6.0); | |
HCL.y *= Q; | |
HCL.z = mix(-U, V, Q) / (HCLmaxL * 2.0); | |
return HCL; | |
} | |
vec2 hue_to_pos(float hue) { | |
return (vec2(cos(hue * TAU), sin(hue * TAU))); | |
} | |
float pos_to_hue(vec2 pos) { | |
return atan(pos.y, pos.x) / TAU; | |
} | |
void fragment() { | |
vec3 sampleA = rgb_to_hcl(texture(textureA, UV).rgb); | |
vec3 sampleB = rgb_to_hcl(texture(textureB, UV).rgb); | |
vec3 blended; | |
blended.rgb = mix(sampleA.rgb, sampleB.rgb, blend); | |
// handle hue blending as special | |
vec2 mix_pos = mix(hue_to_pos(sampleA.r), hue_to_pos(sampleB.r), blend); | |
blended.r = pos_to_hue(mix_pos); | |
//blended.rgb = sampleA.rgb; | |
// desaturate when hue is traveling far | |
blended.g = blended.g * pow(length(mix_pos), pow_comp);//blended.g * pow(length(mix_pos), 2.0); | |
//blended.r = sampleA.r; | |
COLOR.rgb = hcl_to_rgb(blended); | |
} | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
shader_type canvas_item; | |
uniform sampler2D textureA:source_color; | |
uniform sampler2D textureB:source_color; | |
uniform float blend:hint_range(0.0, 1.0, 0.01); | |
// lowers saturation for colours that pass through the centre of hue space | |
uniform float pow_comp:hint_range(0.0, 50.0, 0.01) = 1.0; | |
float hue_to_rgb(float p, float q, float t){ | |
if(t < 0.0) t += 1.0; | |
if(t > 1.0) t -= 1.0; | |
if(t < 1.0/6.0) return p + (q - p) * 6.0 * t; | |
if(t < 1.0/2.0) return q; | |
if(t < 2.0/3.0) return p + (q - p) * (2.0/3.0 - t) * 6.0; | |
return p; | |
} | |
vec3 hsl_to_rgb(vec3 hsl) { | |
float h = hsl.x; | |
float s = hsl.y; | |
float l = hsl.z; | |
vec3 result; | |
if (s == 0.0) { | |
result.rgb = vec3(l); | |
} else { | |
float q, p; | |
if (l < 0.5) { | |
q = l * (1.0 + s); | |
} else { | |
q = l + s - l * s; | |
} | |
p = 2.0 * l - q; | |
result.r = hue_to_rgb(p, q, h + 1.0 / 3.0); | |
result.g = hue_to_rgb(p, q, h); | |
result.b = hue_to_rgb(p, q, h - 1.0 / 3.0); | |
} | |
return result; | |
} | |
vec3 rgb_to_hsl(vec3 col){ | |
float c_max = max(col.r, max(col.g, col.b)); | |
float c_min = min(col.r, min(col.g, col.b)); | |
float h, s, l = (c_max + c_min) / 2.0; | |
if(c_max == c_min) { | |
h = s = 0.0; // achromatic | |
} else { | |
float d = c_max - c_min; | |
s = l > 0.5 ? d / (2.0 - c_max - c_min) : d / (c_max + c_min); | |
if (c_max == col.r) { | |
h = (col.g - col.b) / d + (col.g < col.b ? 6.0 : 0.0); | |
} else if (c_max == col.g) { | |
h = (col.b - col.r) / d + 2.0; | |
} else { | |
h = (col.r - col.g) / d + 4.0; | |
} | |
h /= 6.0; | |
} | |
return vec3(h, s, l); | |
} | |
vec2 hue_to_pos(float hue) { | |
return (vec2(cos(hue * TAU), sin(hue * TAU))); | |
} | |
float pos_to_hue(vec2 pos) { | |
return atan(pos.y, pos.x) / TAU; | |
} | |
void fragment() { | |
vec3 sampleA = rgb_to_hsl(texture(textureA, UV).rgb); | |
vec3 sampleB = rgb_to_hsl(texture(textureB, UV).rgb); | |
vec3 blended; | |
blended.gb = mix(sampleA.gb, sampleB.gb, blend); | |
// handle hue blending as special | |
vec2 mix_pos = mix(hue_to_pos(sampleA.r), hue_to_pos(sampleB.r), blend); | |
blended.r = pos_to_hue(mix_pos); | |
// desaturate when hue is traveling far | |
blended.g = blended.g * pow(length(mix_pos), pow_comp);//blended.g * pow(length(mix_pos), 2.0); | |
//blended.r = sampleA.r; | |
COLOR.rgb = hsl_to_rgb(blended); | |
} | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
shader_type canvas_item; | |
uniform sampler2D textureA:source_color; | |
uniform sampler2D textureB:source_color; | |
uniform float blend:hint_range(0.0, 1.0, 0.01) = 0.5; | |
// lowers saturation for colours that pass through the centre of hue space | |
uniform float pow_comp:hint_range(0.0, 10.0, 0.01) = 1.0; | |
float hue_to_rgb(float p, float q, float t){ | |
if(t < 0.0) t += 1.0; | |
if(t > 1.0) t -= 1.0; | |
if(t < 1.0/6.0) return p + (q - p) * 6.0 * t; | |
if(t < 1.0/2.0) return q; | |
if(t < 2.0/3.0) return p + (q - p) * (2.0/3.0 - t) * 6.0; | |
return p; | |
} | |
vec3 hsv_to_rgb(vec3 hsl) { | |
float h = hsl.x; | |
if (h < 0.0) | |
h += 1.0; | |
if (h > 1.0) | |
h -= 1.0; | |
float s = hsl.y; | |
float v = hsl.z; | |
vec3 result; | |
float i = floor(h * 6.0); | |
float f = h * 6.0 - i; | |
float p = v * (1.0 - s); | |
float q = v * (1.0 - f * s); | |
float t = v * (1.0 - (1.0 - f) * s); | |
if (abs(i) < 0.1) result.rgb = vec3(v,t,p); | |
else if (abs(i - 1.0) < 0.1) result.rgb = vec3(q, v, p); | |
else if (abs(i - 2.0) < 0.1) result.rgb = vec3(p, v, t); | |
else if (abs(i - 3.0) < 0.1) result.rgb = vec3(p, q, v); | |
else if (abs(i - 4.0) < 0.1) result.rgb = vec3(t, p, v); | |
else result.rgb = vec3(v, p, q); | |
// result.rgb = vec3(1.0, 1.0, 1.00); | |
return result; | |
} | |
vec3 rgb_to_hsv(vec3 col){ | |
float c_max = max(col.r, max(col.g, col.b)); | |
float c_min = min(col.r, min(col.g, col.b)); | |
float h, s, v = c_max; | |
float d = c_max - c_min; | |
s = c_max == 0.0 ? 0.0 : d / c_max; | |
if(c_max == c_min) { | |
h = 0.0; // achromatic | |
} else { | |
if (c_max == col.r) { | |
h = (col.g - col.b) / d + (col.g < col.b ? 6.0 : 0.0); | |
} else if (c_max == col.g) { | |
h = (col.b - col.r) / d + 2.0; | |
} else { | |
h = (col.r - col.g) / d + 4.0; | |
} | |
h /= 6.0; | |
} | |
return vec3(h, s, v); | |
} | |
vec2 hue_to_pos(float hue) { | |
return (vec2(cos(hue * TAU), sin(hue * TAU))); | |
} | |
float pos_to_hue(vec2 pos) { | |
return atan(pos.y, pos.x) / TAU; | |
} | |
void fragment() { | |
vec3 sampleA = rgb_to_hsv(texture(textureA, UV).rgb); | |
vec3 sampleB = rgb_to_hsv(texture(textureB, UV).rgb); | |
vec3 blended; | |
blended.gb = mix(sampleA.gb, sampleB.gb, blend); | |
// handle hue blending as special | |
vec2 mix_pos = mix(hue_to_pos(sampleA.r), hue_to_pos(sampleB.r), blend); | |
blended.r = pos_to_hue(mix_pos); | |
//blended.rgb = sampleA.rgb; | |
// desaturate when hue is traveling far | |
blended.g = blended.g * pow(length(mix_pos), pow_comp);//blended.g * pow(length(mix_pos), 2.0); | |
//blended.r = sampleA.r; | |
COLOR.rgb = hsv_to_rgb(blended); | |
} | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
shader_type canvas_item; | |
uniform sampler2D textureA:source_color; | |
uniform sampler2D textureB:source_color; | |
uniform float blend:hint_range(0.0, 1.0, 0.01); | |
void fragment() { | |
vec3 sampleA = texture(textureA, UV).rgb; | |
vec3 sampleB = texture(textureB, UV).rgb; | |
COLOR.rgb = mix(sampleA, sampleB, blend); | |
} | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
shader_type canvas_item; | |
uniform sampler2D textureA:source_color; | |
uniform sampler2D textureB:source_color; | |
uniform float blend:hint_range(0.0, 1.0, 0.01) = 0.5; | |
// lowers saturation for colours that pass through the centre of hue space | |
uniform float pow_comp:hint_range(0.0, 10.0, 0.01) = 1.0; | |
//https://github.com/joeedh/pigment-painter/blob/c358be306967e53d5a8582e990611d88b3a54d50/wasm/color.h#L62 | |
vec3 rgb_to_xyz(vec3 rgb) { | |
float var_R = rgb.r; | |
float var_G = rgb.g; | |
float var_B = rgb.b; | |
// ignor gamma for now | |
/*if (!noGamma) { | |
if (var_R > 0.04045) | |
var_R = powf((var_R + 0.055) / 1.055, 2.4); | |
else | |
var_R = var_R / 12.92; | |
if (var_G > 0.04045) | |
var_G = powf((var_G + 0.055) / 1.055, 2.4); | |
else | |
var_G = var_G / 12.92; | |
if (var_B > 0.04045) | |
var_B = powf((var_B + 0.055) / 1.055, 2.4); | |
else | |
var_B = var_B / 12.92; | |
}*/ | |
/* | |
on factor; | |
off period; | |
f1 := var_R * 0.4124 + var_G * 0.3576 + var_B * 0.1805 - X; | |
f2 := var_R * 0.2126 + var_G * 0.7152 + var_B * 0.0722 - Y; | |
f3 := var_R * 0.0193 + var_G * 0.1192 + var_B * 0.9505 - Z; | |
f := solve({f1, f2, f3}, {var_r, var_g, var_b}); | |
*/ | |
// Observer. = 2��, Illuminant = D65 | |
float X = var_R * 0.4124 + var_G * 0.3576 + var_B * 0.1805; | |
float Y = var_R * 0.2126 + var_G * 0.7152 + var_B * 0.0722; | |
float Z = var_R * 0.0193 + var_G * 0.1192 + var_B * 0.9505; | |
return vec3(X, Y, Z); | |
} | |
vec3 xyz_to_rgb(vec3 xyz) { | |
float var_X = xyz.x; // X from 0 to 95.047 (Observer = 2��, Illuminant = D65) | |
float var_Y = xyz.y; // Y from 0 to 100.000 | |
float var_Z = xyz.z; // Z from 0 to 108.883 | |
float var_R = var_X * 3.240625 + var_Y * -1.53720797 + var_Z * -0.498628; | |
float var_G = var_X * -0.9689307 + var_Y * 1.87575606 + var_Z * 0.04151752; | |
float var_B = var_X * 0.0557101 + var_Y * -0.204021 + var_Z * 1.05699; | |
/* if (!noGamma) { | |
if (var_R > 0.003130807) | |
var_R = 1.055 * (powf(var_R, 1.0 / 2.4)) - 0.055; | |
else | |
var_R = 12.92 * var_R; | |
if (var_G > 0.003130807) | |
var_G = 1.055 * (powf(var_G, 1.0 / 2.4)) - 0.055; | |
else | |
var_G = 12.92 * var_G; | |
if (var_B > 0.003130807) | |
var_B = 1.055 * (powf(var_B, 1.0 / 2.4)) - 0.055; | |
else | |
var_B = 12.92 * var_B; | |
}*/ | |
return vec3(var_R, var_G, var_B); | |
} | |
void fragment() { | |
vec3 sampleA = rgb_to_xyz(texture(textureA, UV).rgb); | |
vec3 sampleB = rgb_to_xyz(texture(textureB, UV).rgb); | |
vec3 blended; | |
blended.rgb = mix(sampleA.rgb, sampleB.rgb, blend); | |
// handle hue blending as special | |
//blended.r = sampleA.r; | |
COLOR.rgb = xyz_to_rgb(blended); | |
} | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
shader_type canvas_item; | |
uniform sampler2D textureA:source_color; | |
uniform sampler2D textureB:source_color; | |
uniform float blend:hint_range(0.0, 1.0, 0.01); | |
//based on https://www.shadertoy.com/view/3lycWz | |
vec3 rgb_to_yuv(vec3 rgb) { | |
float y = 0.299 * rgb.r + 0.587 * rgb.g + 0.114 * rgb.b; | |
return vec3(y, 0.493 * (rgb.b - y), 0.877 * (rgb.r - y)); | |
} | |
vec3 yuv_to_rgb(vec3 yuv) { | |
float y = yuv.x; | |
float u = yuv.y; | |
float v = yuv.z; | |
vec3 rgb = vec3( | |
y + 1.0 / 0.877 * v, | |
y - 0.39393 * u - 0.58081 * v, | |
y + 1.0 / 0.493 * u | |
); | |
return rgb; | |
} | |
void fragment() { | |
vec3 sampleA = rgb_to_yuv(texture(textureA, UV).rgb); | |
vec3 sampleB = rgb_to_yuv(texture(textureB, UV).rgb); | |
vec3 blended = mix(sampleA, sampleB, blend); | |
COLOR.rgb = yuv_to_rgb(blended); | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
I found that the main difference is between colour spaces that use a hue and ones that don't.
There isn't much point in blending in YUV or XYZ over RGB unless you're doing something else with it.
The hsl, hcl, hsv all rely on hue as one property, where I would convert to a 2D vector and then convert back. So, rather than evenly spaced movement from one hue to the other, it spends more time on either end. And also allowed me to try desaturating during the transition based on the magnitude of the vector. There might be some effects where it might looks good to play with hue in these ways, but for most cases of blending between two images, it ends up feeling oversaturated.