Browse Source

Add Linear SRGB and OKLab color spaces to Gradient.

JoNax97 2 years ago
parent
commit
c07b2fcf4d

+ 14 - 1
doc/classes/Gradient.xml

@@ -80,8 +80,12 @@
 		<member name="colors" type="PackedColorArray" setter="set_colors" getter="get_colors" default="PackedColorArray(0, 0, 0, 1, 1, 1, 1, 1)">
 			Gradient's colors returned as a [PackedColorArray].
 		</member>
+		<member name="interpolation_color_space" type="int" setter="set_interpolation_color_space" getter="get_interpolation_color_space" enum="Gradient.ColorSpace" default="0">
+			The color space used to interpolate between points of the gradient. It does not affect the returned colors, which will always be in sRGB space. See [enum ColorSpace] for available modes.
+			[b]Note:[/b] This setting has no effect when [member interpolation_mode] is set to [constant GRADIENT_INTERPOLATE_CONSTANT].
+		</member>
 		<member name="interpolation_mode" type="int" setter="set_interpolation_mode" getter="get_interpolation_mode" enum="Gradient.InterpolationMode" default="0">
-			Defines how the colors between points of the gradient are interpolated. See [enum InterpolationMode] for available modes.
+			The algorithm used to interpolate between points of the gradient. See [enum InterpolationMode] for available modes.
 		</member>
 		<member name="offsets" type="PackedFloat32Array" setter="set_offsets" getter="get_offsets" default="PackedFloat32Array(0, 1)">
 			Gradient's offsets returned as a [PackedFloat32Array].
@@ -97,5 +101,14 @@
 		<constant name="GRADIENT_INTERPOLATE_CUBIC" value="2" enum="InterpolationMode">
 			Cubic interpolation.
 		</constant>
+		<constant name="GRADIENT_COLOR_SPACE_SRGB" value="0" enum="ColorSpace">
+			sRGB color space.
+		</constant>
+		<constant name="GRADIENT_COLOR_SPACE_LINEAR_SRGB" value="1" enum="ColorSpace">
+			Linear sRGB color space.
+		</constant>
+		<constant name="GRADIENT_COLOR_SPACE_OKLAB" value="2" enum="ColorSpace">
+			[url=https://bottosson.github.io/posts/oklab/]Oklab[/url] color space. This color space provides a smooth and uniform-looking transition between colors.
+		</constant>
 	</constants>
 </class>

+ 13 - 0
editor/plugins/gradient_editor.cpp

@@ -41,6 +41,7 @@ void GradientEditor::set_gradient(const Ref<Gradient> &p_gradient) {
 	gradient->connect("changed", callable_mp(this, &GradientEditor::_gradient_changed));
 	set_points(gradient->get_points());
 	set_interpolation_mode(gradient->get_interpolation_mode());
+	set_interpolation_color_space(gradient->get_interpolation_color_space());
 }
 
 void GradientEditor::reverse_gradient() {
@@ -93,6 +94,7 @@ void GradientEditor::_gradient_changed() {
 	Vector<Gradient::Point> grad_points = gradient->get_points();
 	set_points(grad_points);
 	set_interpolation_mode(gradient->get_interpolation_mode());
+	set_interpolation_color_space(gradient->get_interpolation_color_space());
 	queue_redraw();
 	editing = false;
 }
@@ -104,9 +106,11 @@ void GradientEditor::_ramp_changed() {
 	undo_redo->add_do_method(gradient.ptr(), "set_offsets", get_offsets());
 	undo_redo->add_do_method(gradient.ptr(), "set_colors", get_colors());
 	undo_redo->add_do_method(gradient.ptr(), "set_interpolation_mode", get_interpolation_mode());
+	undo_redo->add_do_method(gradient.ptr(), "set_interpolation_color_space", get_interpolation_color_space());
 	undo_redo->add_undo_method(gradient.ptr(), "set_offsets", gradient->get_offsets());
 	undo_redo->add_undo_method(gradient.ptr(), "set_colors", gradient->get_colors());
 	undo_redo->add_undo_method(gradient.ptr(), "set_interpolation_mode", gradient->get_interpolation_mode());
+	undo_redo->add_undo_method(gradient.ptr(), "set_interpolation_color_space", gradient->get_interpolation_color_space());
 	undo_redo->commit_action();
 	editing = false;
 }
@@ -171,6 +175,14 @@ Gradient::InterpolationMode GradientEditor::get_interpolation_mode() {
 	return interpolation_mode;
 }
 
+void GradientEditor::set_interpolation_color_space(Gradient::ColorSpace p_color_space) {
+	interpolation_color_space = p_color_space;
+}
+
+Gradient::ColorSpace GradientEditor::get_interpolation_color_space() {
+	return interpolation_color_space;
+}
+
 ColorPicker *GradientEditor::get_picker() {
 	return picker;
 }
@@ -385,6 +397,7 @@ void GradientEditor::_notification(int p_what) {
 			// Draw color ramp.
 			gradient_cache->set_points(points);
 			gradient_cache->set_interpolation_mode(interpolation_mode);
+			gradient_cache->set_interpolation_color_space(interpolation_color_space);
 			preview_texture->set_gradient(gradient_cache);
 			draw_texture_rect(preview_texture, Rect2(handle_width / 2, 0, total_w, h));
 

+ 4 - 0
editor/plugins/gradient_editor.h

@@ -45,6 +45,7 @@ class GradientEditor : public Control {
 	int grabbed = -1;
 	Vector<Gradient::Point> points;
 	Gradient::InterpolationMode interpolation_mode = Gradient::GRADIENT_INTERPOLATE_LINEAR;
+	Gradient::ColorSpace interpolation_color_space = Gradient::GRADIENT_COLOR_SPACE_SRGB;
 
 	bool editing = false;
 	Ref<Gradient> gradient;
@@ -84,6 +85,9 @@ public:
 	void set_interpolation_mode(Gradient::InterpolationMode p_interp_mode);
 	Gradient::InterpolationMode get_interpolation_mode();
 
+	void set_interpolation_color_space(Gradient::ColorSpace p_color_space);
+	Gradient::ColorSpace get_interpolation_color_space();
+
 	ColorPicker *get_picker();
 	PopupPanel *get_popup();
 

+ 25 - 0
scene/resources/gradient.cpp

@@ -69,7 +69,12 @@ void Gradient::_bind_methods() {
 	ClassDB::bind_method(D_METHOD("set_interpolation_mode", "interpolation_mode"), &Gradient::set_interpolation_mode);
 	ClassDB::bind_method(D_METHOD("get_interpolation_mode"), &Gradient::get_interpolation_mode);
 
+	ClassDB::bind_method(D_METHOD("set_interpolation_color_space", "interpolation_color_space"), &Gradient::set_interpolation_color_space);
+	ClassDB::bind_method(D_METHOD("get_interpolation_color_space"), &Gradient::get_interpolation_color_space);
+
+	ADD_GROUP("Interpolation", "interpolation_");
 	ADD_PROPERTY(PropertyInfo(Variant::INT, "interpolation_mode", PROPERTY_HINT_ENUM, "Linear,Constant,Cubic"), "set_interpolation_mode", "get_interpolation_mode");
+	ADD_PROPERTY(PropertyInfo(Variant::INT, "interpolation_color_space", PROPERTY_HINT_ENUM, "sRGB,Linear sRGB,Oklab"), "set_interpolation_color_space", "get_interpolation_color_space");
 
 	ADD_GROUP("Raw Data", "");
 	ADD_PROPERTY(PropertyInfo(Variant::PACKED_FLOAT32_ARRAY, "offsets"), "set_offsets", "get_offsets");
@@ -78,6 +83,16 @@ void Gradient::_bind_methods() {
 	BIND_ENUM_CONSTANT(GRADIENT_INTERPOLATE_LINEAR);
 	BIND_ENUM_CONSTANT(GRADIENT_INTERPOLATE_CONSTANT);
 	BIND_ENUM_CONSTANT(GRADIENT_INTERPOLATE_CUBIC);
+
+	BIND_ENUM_CONSTANT(GRADIENT_COLOR_SPACE_SRGB);
+	BIND_ENUM_CONSTANT(GRADIENT_COLOR_SPACE_LINEAR_SRGB);
+	BIND_ENUM_CONSTANT(GRADIENT_COLOR_SPACE_OKLAB);
+}
+
+void Gradient::_validate_property(PropertyInfo &p_property) const {
+	if (p_property.name == "interpolation_color_space" && interpolation_mode == GRADIENT_INTERPOLATE_CONSTANT) {
+		p_property.usage = PROPERTY_USAGE_NO_EDITOR;
+	}
 }
 
 Vector<float> Gradient::get_offsets() const {
@@ -101,12 +116,22 @@ Vector<Color> Gradient::get_colors() const {
 void Gradient::set_interpolation_mode(Gradient::InterpolationMode p_interp_mode) {
 	interpolation_mode = p_interp_mode;
 	emit_signal(CoreStringNames::get_singleton()->changed);
+	notify_property_list_changed();
 }
 
 Gradient::InterpolationMode Gradient::get_interpolation_mode() {
 	return interpolation_mode;
 }
 
+void Gradient::set_interpolation_color_space(Gradient::ColorSpace p_color_space) {
+	interpolation_color_space = p_color_space;
+	emit_signal(CoreStringNames::get_singleton()->changed);
+}
+
+Gradient::ColorSpace Gradient::get_interpolation_color_space() {
+	return interpolation_color_space;
+}
+
 void Gradient::set_offsets(const Vector<float> &p_offsets) {
 	points.resize(p_offsets.size());
 	for (int i = 0; i < points.size(); i++) {

+ 88 - 21
scene/resources/gradient.h

@@ -33,6 +33,8 @@
 
 #include "core/io/resource.h"
 
+#include "thirdparty/misc/ok_color.h"
+
 class Gradient : public Resource {
 	GDCLASS(Gradient, Resource);
 	OBJ_SAVE_TYPE(Gradient);
@@ -44,6 +46,12 @@ public:
 		GRADIENT_INTERPOLATE_CUBIC,
 	};
 
+	enum ColorSpace {
+		GRADIENT_COLOR_SPACE_SRGB,
+		GRADIENT_COLOR_SPACE_LINEAR_SRGB,
+		GRADIENT_COLOR_SPACE_OKLAB,
+	};
+
 	struct Point {
 		float offset = 0.0;
 		Color color;
@@ -56,6 +64,7 @@ private:
 	Vector<Point> points;
 	bool is_sorted = true;
 	InterpolationMode interpolation_mode = GRADIENT_INTERPOLATE_LINEAR;
+	ColorSpace interpolation_color_space = GRADIENT_COLOR_SPACE_SRGB;
 
 	_FORCE_INLINE_ void _update_sorting() {
 		if (!is_sorted) {
@@ -64,8 +73,55 @@ private:
 		}
 	}
 
+	_FORCE_INLINE_ Color transform_color_space(const Color p_color) const {
+		switch (interpolation_color_space) {
+			case GRADIENT_COLOR_SPACE_SRGB:
+			default:
+				return p_color;
+
+			case GRADIENT_COLOR_SPACE_LINEAR_SRGB:
+				return p_color.srgb_to_linear();
+
+			case GRADIENT_COLOR_SPACE_OKLAB:
+				Color linear_color = p_color.srgb_to_linear();
+				ok_color::RGB rgb{};
+				rgb.r = linear_color.r;
+				rgb.g = linear_color.g;
+				rgb.b = linear_color.b;
+
+				ok_color ok_color;
+				ok_color::Lab lab_color = ok_color.linear_srgb_to_oklab(rgb);
+
+				// Constructs an RGB color using the Lab values directly. This allows reusing the interpolation code.
+				return { lab_color.L, lab_color.a, lab_color.b, linear_color.a };
+		}
+	}
+
+	_FORCE_INLINE_ Color inv_transform_color_space(const Color p_color) const {
+		switch (interpolation_color_space) {
+			case GRADIENT_COLOR_SPACE_SRGB:
+			default:
+				return p_color;
+
+			case GRADIENT_COLOR_SPACE_LINEAR_SRGB:
+				return p_color.linear_to_srgb();
+
+			case GRADIENT_COLOR_SPACE_OKLAB:
+				ok_color::Lab lab{};
+				lab.L = p_color.r;
+				lab.a = p_color.g;
+				lab.b = p_color.b;
+
+				ok_color new_ok_color;
+				ok_color::RGB ok_rgb = new_ok_color.oklab_to_linear_srgb(lab);
+				Color linear{ ok_rgb.r, ok_rgb.g, ok_rgb.b, p_color.a };
+				return linear.linear_to_srgb();
+		}
+	}
+
 protected:
 	static void _bind_methods();
+	void _validate_property(PropertyInfo &p_property) const;
 
 public:
 	Gradient();
@@ -92,6 +148,9 @@ public:
 	void set_interpolation_mode(InterpolationMode p_interp_mode);
 	InterpolationMode get_interpolation_mode();
 
+	void set_interpolation_color_space(Gradient::ColorSpace p_color_space);
+	ColorSpace get_interpolation_color_space();
+
 	_FORCE_INLINE_ Color get_color_at_offset(float p_offset) {
 		if (points.is_empty()) {
 			return Color(0, 0, 0, 1);
@@ -134,16 +193,22 @@ public:
 		if (first < 0) {
 			return points[0].color;
 		}
-		const Point &pointFirst = points[first];
-		const Point &pointSecond = points[second];
+		const Point &point1 = points[first];
+		const Point &point2 = points[second];
+		float weight = (p_offset - point1.offset) / (point2.offset - point1.offset);
 
 		switch (interpolation_mode) {
-			case GRADIENT_INTERPOLATE_LINEAR: {
-				return pointFirst.color.lerp(pointSecond.color, (p_offset - pointFirst.offset) / (pointSecond.offset - pointFirst.offset));
-			} break;
 			case GRADIENT_INTERPOLATE_CONSTANT: {
-				return pointFirst.color;
-			} break;
+				return point1.color;
+			}
+			case GRADIENT_INTERPOLATE_LINEAR:
+			default: { // Fallback to linear interpolation.
+				Color color1 = transform_color_space(point1.color);
+				Color color2 = transform_color_space(point2.color);
+
+				Color interpolated = color1.lerp(color2, weight);
+				return inv_transform_color_space(interpolated);
+			}
 			case GRADIENT_INTERPOLATE_CUBIC: {
 				int p0 = first - 1;
 				int p3 = second + 1;
@@ -153,20 +218,21 @@ public:
 				if (p0 < 0) {
 					p0 = first;
 				}
-				const Point &pointP0 = points[p0];
-				const Point &pointP3 = points[p3];
-
-				float x = (p_offset - pointFirst.offset) / (pointSecond.offset - pointFirst.offset);
-				float r = Math::cubic_interpolate(pointFirst.color.r, pointSecond.color.r, pointP0.color.r, pointP3.color.r, x);
-				float g = Math::cubic_interpolate(pointFirst.color.g, pointSecond.color.g, pointP0.color.g, pointP3.color.g, x);
-				float b = Math::cubic_interpolate(pointFirst.color.b, pointSecond.color.b, pointP0.color.b, pointP3.color.b, x);
-				float a = Math::cubic_interpolate(pointFirst.color.a, pointSecond.color.a, pointP0.color.a, pointP3.color.a, x);
-
-				return Color(r, g, b, a);
-			} break;
-			default: {
-				// Fallback to linear interpolation.
-				return pointFirst.color.lerp(pointSecond.color, (p_offset - pointFirst.offset) / (pointSecond.offset - pointFirst.offset));
+				const Point &point0 = points[p0];
+				const Point &point3 = points[p3];
+
+				Color color0 = transform_color_space(point0.color);
+				Color color1 = transform_color_space(point1.color);
+				Color color2 = transform_color_space(point2.color);
+				Color color3 = transform_color_space(point3.color);
+
+				Color interpolated;
+				interpolated[0] = Math::cubic_interpolate(color1[0], color2[0], color0[0], color3[0], weight);
+				interpolated[1] = Math::cubic_interpolate(color1[1], color2[1], color0[1], color3[1], weight);
+				interpolated[2] = Math::cubic_interpolate(color1[2], color2[2], color0[2], color3[2], weight);
+				interpolated[3] = Math::cubic_interpolate(color1[3], color2[3], color0[3], color3[3], weight);
+
+				return inv_transform_color_space(interpolated);
 			}
 		}
 	}
@@ -175,5 +241,6 @@ public:
 };
 
 VARIANT_ENUM_CAST(Gradient::InterpolationMode);
+VARIANT_ENUM_CAST(Gradient::ColorSpace);
 
 #endif // GRADIENT_H