i made a shader that interprets the RGB color channels als OKLCH:

RGB channel | interpreted as
---|---
Red, 0 – 255 | Lightness, 0.0 – 1.0
Green, 0 – 255 | Chroma, 0.0 – 0.34
Blue, 0 – 255 | Hue, 0 – 360w

but the result looks less consistent than it shoud:

  • the Lightness is not as consistent as expected
  • the Chroma is brighter and less consistent than expected

the strange thing is that the GDScript version of the converter is accurate despite using the exact same formulas in the exact same order:
if i ask the shader to translate oklch(0.7 0.1 248), i get this:
a bit too saturated;
but if i ask the GDScript to do it, i get rgb(103.8397, 164.4775, 217.0177, 255), the same as this trusted OKLCH picker.

rgb-as-oklch.zip
2MB

here are these 2 files in the project:

file | path
---|---
Shader | res://srgb_as_oklch.gdshader
GDScript | res://oklch_calculator.tscn::GDScript

  • Sosasees replied to this.
  • cybereality Your issue is that the code in GDScript and the code in the shader is using completely different functions.

    Sosasees ¿what are these differences that i missed?

    cybereality It's not differences. It's that the functions are completely different. Totally different names, logic, code.

    now that i have the right result, i know that the strange result was from not understanding shader language good enough.
    in my second attempt i rewrote the shader from scratch, doing the same thing i did in my first attempt — copying the logic from the GDScript.

    rgb-as-oklch.zip
    2MB

    You can upload pictures. What format do you have them in?

    Shaders use sRGB for some textures, but the calculations are linear. Depending on how you are using it you might have to add a hint like hint_albedo.

      @Sosasees
      A bit offtopic. I didn't know about OKLCH. It sounds great. I do a lot of stuff with procedurally generated palettes where perceptual uniformity is of utmost importance. The best results I had with radial CIELAB (LCH) but stumbled upon the exact issues Bjorn is mentioning in the article (problems with hue wonkiness). Had an idea to deduce a better perceptual remap from observational data, but figured it'd be too much work. Bjorn did exactly that and it looks like a success.

      You should perhaps "officially" release a GDScript conversion library if you have it implemented. I'd most certainly use it.

        Sosasees (sorry, Godot Forums didn't let me upload the pictures. please fix this.)

        cybereality You can upload pictures. What format do you have them in?

        Sosasees these pictures are in WEBP format

        cybereality I just added support. Try to upload the images again.

        Thank you Very much!
        now i could add the pictures in the post itself — not just linking to them on Google Drive.

        cybereality Shaders use sRGB for some textures, but the calculations are linear. Depending on how you are using it you might have to add a hint like hint_albedo.

        i couldn't find how to add a hint to texture(TEXTURE, UV);.
        i want to use the Sprite2D's sprite as the shader texture — and later i might want a variation that uses the screen texture.

        So for shaders (especially 2D) I think everything is in linear space. If there is any conversion, it is automatic at the end and shouldn't affect the calculations.

        Your issue is that the code in GDScript and the code in the shader is using completely different functions.

          cybereality Your issue is that the code in GDScript and the code in the shader is using completely different functions.

          ¿what are these differences that i missed?

            Sosasees ¿what are these differences that i missed?

            It's not differences. It's that the functions are completely different. Totally different names, logic, code.

              xyz Try to disable sRGB flag in texture's import options.

              ¿do you mean "HDR as sRGB"?
              it was disabled, and enabling it did not make the colors look better:
              lightness is more consistent but not by much — chroma is too desaturated and sometimes the hue becomes wrong
              (i did not take a photo, i was too busy fixing the colors)

              cybereality Your issue is that the code in GDScript and the code in the shader is using completely different functions.

              Sosasees ¿what are these differences that i missed?

              cybereality It's not differences. It's that the functions are completely different. Totally different names, logic, code.

              now that i have the right result, i know that the strange result was from not understanding shader language good enough.
              in my second attempt i rewrote the shader from scratch, doing the same thing i did in my first attempt — copying the logic from the GDScript.

              rgb-as-oklch.zip
              2MB

              xyz A bit offtopic. I didn't know about OKLCH. It sounds great. I do a lot of stuff with procedurally generated palettes where perceptual uniformity is of utmost importance. The best results I had with radial CIELAB (LCH) but stumbled upon the exact issues Bjorn is mentioning in the article (problems with hue wonkiness). Had an idea to deduce a better perceptual remap from observational data, but figured it'd be too much work. Bjorn did exactly that and it looks like a success.

              You should perhaps "officially" release a GDScript conversion library if you have it implemented. I'd most certainly use it.

              this sounds great.
              i think that the best implementation of a Godot OKLCH library would be an add-on with these things, please correct me:

              • OklchConverter node (autoload)
                can convert colors between sRGB and OKLCH
              • ColorOklch custom resource type
                for storing 1 OKLCH color, just like Color stores 1 RGB color
                (i don't know anything about custom resources yet, but i just feel like this is right)

                Sosasees It doesn't need to be a resource. It can be a regular class/object or even just conversion functions that take/return LCH values in an array.

                  xyz ¿which is the best way? please only answer this if you know.

                  • xyz replied to this.

                    Sosasees There's no best way 🙂
                    I'd start with just making static conversion functions in a regular script file. If file is added to the project the functions become available everywhere in the project.

                    class_name LCH
                    
                    static func to_rgb(lch: Array) -> Color:
                    	# code here
                    	
                    static func from_rgb(rgb: Color) -> Array:
                    	# code here

                    The usage would then be:

                    LCH.to_rgb([1.0, 1.0, 1.0])
                    LCH.from_rgb(Color(1.0, 1.0, 1.0))

                    I'd also maybe add a convenience function to blend/interpolate two LCH colors. So:

                    static func blend(lch1: Array, lch2: Array, factor: float) -> Array

                    Just to add some more thoughts on the topic. CIELAB in great for two-color blending. The gradients it produces all look perceptually "natural". However, in procedural generation there is a lot of need for intuitive color shifts - i.e. changing the value of one of the luma/chroma/hue components but keeping other two components perceptually the same. This is problematic in all CIEXYZ based spaces including LAB. Hue shifts in particular can give unpredictable results. Not as bad as HSL/HSV but still not ideal.

                    I even think that ensuring both - perceptually uniform blending and perceptually uniform shifts - is not entirely possible inside a single space. At least not without some transformation black magic. But that then is equivalent of using an alternate space. It'd be good to see some tests on how OKLCH does with shifts.

                    Ideally I'd like to have a library that lets you do perceptually uniform blends/shifts, but uses rgb at input/output. It can internally convert to whatever space is best for a particular operation. The library user need not care about it. So interface might look something like this:

                    class_name Perceptual
                    func blend(rgb1: Color, rgb2: Color, factor: float) -> Color
                    func shift_hue(rgb: Color, shift: float) -> Color
                    func shift_luma(rgb: Color, shift: float) -> Color
                    func shift_chroma(rgb: Color, shift: float) -> Color

                    now i'm only a licensing away from releasing an OKLCH addon as an open-source project.

                    rgb-as-oklch.zip
                    2MB

                    my original plan was to have 2 things in this addon:

                    Sosasees

                    • OklchConverter node (autoload)
                      can convert colors between sRGB and OKLCH
                    • ColorOklch custom resource type
                      for storing 1 OKLCH color, just like Color stores 1 RGB color
                      (i don't know anything about custom resources yet, but i just feel like this is right)

                    in the real addon, i have 2 other things:

                    • ColorOKLCH custom resource type
                      • stores a color in OKLCH format
                      • can convert a color from OKLCH to sRGB
                      • can convert a color from sRGB to OKLCH
                    • sRGB as OKLCH shader, also known as OKLCH color profile shader
                      • interprets the sRGB pixels as OKLCH and converts them back to sRGB