Color curves

This shader adjusts colors with curves, similar to the filter found in art/photo editing software such as Krita and GIMP. Supports RGB, Red, Green, Blue, Alpha, Hue, Saturation and Value channels. It takes a CurveTexture as a uniform sampler2D for each channel.

For the Godot 3 GLES2 version, see: https://godotshaders.com/shader/color-curves/

Artwork in the cover image is made by Erevoid, with Pixelorama.

Shader code
shader_type canvas_item;

// CurveTexture(s)
uniform sampler2D rgb_curve;
uniform sampler2D red_curve;
uniform sampler2D green_curve;
uniform sampler2D blue_curve;
uniform sampler2D alpha_curve;
uniform sampler2D hue_curve;
uniform sampler2D sat_curve;
uniform sampler2D value_curve;


vec3 rgb2hsb(vec3 c) {
	vec4 K = vec4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0);
	vec4 p = mix(vec4(c.bg, K.wz),
				vec4(c.gb, K.xy),
				step(c.b, c.g));
	vec4 q = mix(vec4(p.xyw, c.r),
				vec4(c.r, p.yzx),
				step(p.x, c.r));
	float d = q.x - min(q.w, q.y);
	float e = 1.0e-10;
	return vec3(abs(q.z + (q.w - q.y) / (6.0 * d + e)),
				d / (q.x + e),
				q.x);
}

vec3 hsb2rgb(vec3 c)
{
	vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
	vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);
	return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
}


void fragment() {
	COLOR = texture(TEXTURE, UV);

	float red_curve_color = texture(red_curve, vec2(COLOR.r, 0.0)).r;
	float green_curve_color = texture(green_curve, vec2(COLOR.g, 0.0)).r;
	float blue_curve_color = texture(blue_curve, vec2(COLOR.b, 0.0)).r;
	float alpha_curve_color = texture(alpha_curve, vec2(COLOR.a, 0.0)).r;
	COLOR.r = red_curve_color;
	COLOR.g = green_curve_color;
	COLOR.b = blue_curve_color;
	COLOR.a = alpha_curve_color;

	vec3 hsb = rgb2hsb(COLOR.rgb);
	float hue_curve_color = texture(hue_curve, vec2(hsb.r, 0.0)).r;
	float sat_curve_color = texture(sat_curve, vec2(hsb.g, 0.0)).r;
	float value_curve_color = texture(value_curve, vec2(hsb.b, 0.0)).r;
	hsb.r = hue_curve_color;
	hsb.g = sat_curve_color;
	hsb.b = value_curve_color;

	COLOR.rgb = hsb2rgb(hsb);

	float rgb_curve_color_r = texture(rgb_curve, vec2(COLOR.r, 0.0)).r;
	float rgb_curve_color_g = texture(rgb_curve, vec2(COLOR.g, 0.0)).r;
	float rgb_curve_color_b = texture(rgb_curve, vec2(COLOR.b, 0.0)).r;
	COLOR.rgb = vec3(rgb_curve_color_r, rgb_curve_color_g, rgb_curve_color_b);
}
Live Preview
Tags
color adjustment, color correction, color curves, color filter, filter
The shader code and all code snippets in this post are under CC0 license and can be used freely without the author's permission. Images and videos, and assets depicted in those, do not fall under this license. For more info, see our License terms.

More from Overloaded

Related shaders

guest

1 Comment
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
HORRDEV
1 month ago

Freaking Awesome!!! Thanks so much. This is a Game Changer. It enables grading after having the world environment already filled with a LUT, or independent of it entirely.

Showed me an opaque white rect first, but I corrected the code to work, orienting on this issue: https://github.com/godotengine/godot/issues/69885

This happened in some other shaders for me as well. I then recalled the suggestion by clayjohn in the issue worked for me in those cases, and also does here for me:

shader_type canvas_item;

// CurveTexture(s)
uniform sampler2D rgb_curve;
uniform sampler2D red_curve;
uniform sampler2D green_curve;
uniform sampler2D blue_curve;
uniform sampler2D alpha_curve;
uniform sampler2D hue_curve;
uniform sampler2D sat_curve;
uniform sampler2D value_curve;
uniform sampler2D SCREEN_TEXTURE : hint_screen_texture, filter_linear_mipmap;

vec3 rgb2hsb(vec3 c) {
  vec4 K = vec4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0);
  vec4 p = mix(vec4(c.bg, K.wz),
        vec4(c.gb, K.xy),
        step(c.b, c.g));
  vec4 q = mix(vec4(p.xyw, c.r),
        vec4(c.r, p.yzx),
        step(p.x, c.r));
  float d = q.x – min(q.w, q.y);
  float e = 1.0e-10;
  return vec3(abs(q.z + (q.w – q.y) / (6.0 * d + e)),
        d / (q.x + e),
        q.x);
}

vec3 hsb2rgb(vec3 c)
{
  vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
  vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 – K.www);
  return c.z * mix(K.xxx, clamp(p – K.xxx, 0.0, 1.0), c.y);
}

void fragment() {
  COLOR = texture(SCREEN_TEXTURE, SCREEN_UV);

  float red_curve_color = texture(red_curve, vec2(COLOR.r, 0.0)).r;
  float green_curve_color = texture(green_curve, vec2(COLOR.g, 0.0)).r;
  float blue_curve_color = texture(blue_curve, vec2(COLOR.b, 0.0)).r;
  //float alpha_curve_color = texture(alpha_curve, vec2(COLOR.a, 0.0)).r;
  COLOR.r = red_curve_color;
  COLOR.g = green_curve_color;
  COLOR.b = blue_curve_color;
  //COLOR.a = alpha_curve_color;

  vec3 hsb = rgb2hsb(COLOR.rgb);
  float hue_curve_color = texture(hue_curve, vec2(hsb.r, 0.0)).r;
  float sat_curve_color = texture(sat_curve, vec2(hsb.g, 0.0)).r;
  float value_curve_color = texture(value_curve, vec2(hsb.b, 0.0)).r;
  hsb.r = hue_curve_color;
  hsb.g = sat_curve_color;
  hsb.b = value_curve_color;

  COLOR.rgb = hsb2rgb(hsb);

  float rgb_curve_color_r = texture(rgb_curve, vec2(COLOR.r, 0.0)).r;
  float rgb_curve_color_g = texture(rgb_curve, vec2(COLOR.g, 0.0)).r;
  float rgb_curve_color_b = texture(rgb_curve, vec2(COLOR.b, 0.0)).r;
  COLOR.rgb = vec3(rgb_curve_color_r, rgb_curve_color_g, rgb_curve_color_b);
}

Last edited 1 month ago by HORRDEV