mapbox / mapbox-gl-js

Interactive, thoroughly customizable maps in the browser, powered by vector tiles and WebGL
https://docs.mapbox.com/mapbox-gl-js/
Other
11.25k stars 2.23k forks source link

Consider support for interpolate expressions in Oklab color space #11348

Open mbullington opened 2 years ago

mbullington commented 2 years ago

Motivation

I was recently investigating Oklab for a personal project (after seeing now the default for gradients in Photoshop: https://twitter.com/bjornornorn/status/1453069681082896394) and while I am not a color theorist, this article claims it has some perceptual improvements over CIELAB, especially in terms of blue hue. https://raphlinus.github.io/color/2021/01/18/oklab-critique.html

I wonder if what the effort is to support an interpolate-oklab expression to go with interpolate-lab, and if that effort is worth having more perceptually accurate color blending.

Design Alternatives

I am unaware of the complexity of adding interpolate-oklab, so we could just do nothing and have access to the CIELAB color space.

Design

https://docs.mapbox.com/mapbox-gl-js/style-spec/expressions/#interpolate-lab

We already have an interpolate-lab expression, so from my perspective copying that seems fine, but if there's any drawbacks to that design we should rethink for an interpolate-oklab expression I'd assume that's ok as well.

Mock-Up

["interpolate-oklab", ["linear"], ["get", "step"], 0, "red", 1, "blue"]

Concepts

Implementation

asheemmamoowala commented 2 years ago

Optimizing Oklab gradients from @aras-p 🙇🏼

mbullington commented 2 years ago

Looking at the OKLab introduction blog post (https://bottosson.github.io/posts/oklab/), the code for the color space is here:

struct Lab {float L; float a; float b;};
struct RGB {float r; float g; float b;};

Lab linear_srgb_to_oklab(RGB c) 
{
    float l = 0.4122214708f * c.r + 0.5363325363f * c.g + 0.0514459929f * c.b;
    float m = 0.2119034982f * c.r + 0.6806995451f * c.g + 0.1073969566f * c.b;
    float s = 0.0883024619f * c.r + 0.2817188376f * c.g + 0.6299787005f * c.b;

    float l_ = cbrtf(l);
    float m_ = cbrtf(m);
    float s_ = cbrtf(s);

    return {
        0.2104542553f*l_ + 0.7936177850f*m_ - 0.0040720468f*s_,
        1.9779984951f*l_ - 2.4285922050f*m_ + 0.4505937099f*s_,
        0.0259040371f*l_ + 0.7827717662f*m_ - 0.8086757660f*s_,
    };
}

RGB oklab_to_linear_srgb(Lab c) 
{
    float l_ = c.L + 0.3963377774f * c.a + 0.2158037573f * c.b;
    float m_ = c.L - 0.1055613458f * c.a - 0.0638541728f * c.b;
    float s_ = c.L - 0.0894841775f * c.a - 1.2914855480f * c.b;

    float l = l_*l_*l_;
    float m = m_*m_*m_;
    float s = s_*s_*s_;

    return {
        +4.0767416621f * l - 3.3077115913f * m + 0.2309699292f * s,
        -1.2684380046f * l + 2.6097574011f * m - 0.3413193965f * s,
        -0.0041960863f * l - 0.7034186147f * m + 1.7076147010f * s,
    };
}

Each of these functions are just two matrix multiplies (with a pow cube). Would it make sense to just do this entirely on the GPU?

For my personal project I wrote code in Metal that did this but lost it (weirdly, my use-case looks better in sRGB! shows this is an art, not a science. so I deleted it 😭).