Color space conversions

From Second Life Wiki
Jump to navigation Jump to search

A collection of "Wikipedia logo"color space conversion functions. Although all colors in Second Life are RGB, other color spaces may offer more comfortable ways of specifying colors and generating more pleasing gradients than straight interpolations between RGB colors.

Hue-based color spaces are commonly used in color pickers. Second Life viewers usually provide a HSL color selector, and most other programs provide one of either HSL or HSV/HSB, with other possible color spaces available.

HSV/HSB

More detail: "Wikipedia logo"HSL and HSV.

HSV and HSB are synonyms, standing for Hue, Saturation and Value or Brightness.

  • Hue is the perceived hue and wraps around in its range, forming a color circle.
  • Saturation is the "grayness" of the color: maximum saturation gives the most intense color, minimum gives grays.
  • Value/brightness determine the "blackness" of the color: maximum value gives the brightest color, minimum gives black.
// Takes a Hue-Saturation-Value(/Brightness) vector
// H in range [0, 1[
// S, V in range [0, 1]
// Returns a LSL RGB vector
vector hsv2rgb(vector hsv) {
    if(hsv.y) {
        float ht = 6*hsv.x; integer i = (integer)ht; float f = ht-i;
        if(i == 0) return <hsv.z, hsv.z*(1-hsv.y*(1-f)), hsv.z*(1-hsv.y)>;
        else if(i == 1) return <hsv.z*(1-hsv.y*f), hsv.z, hsv.z*(1-hsv.y)>;
        else if(i == 2) return <hsv.z*(1-hsv.y), hsv.z, hsv.z*(1-hsv.y*(1-f))>;
        else if(i == 3) return <hsv.z*(1-hsv.y), hsv.z*(1-hsv.y*f), hsv.z>;
        else if(i == 4) return <hsv.z*(1-hsv.y*(1-f)), hsv.z*(1-hsv.y), hsv.z>;
        else return <hsv.z, hsv.z*(1-hsv.y), hsv.z*(1-hsv.y*f)>;
    }
    return <hsv.z, hsv.z, hsv.z>;
}

// Takes a LSL RGB vector
// Returns a HSV vector
// H in range [0, 1[
// S, V in range [0, 1]
vector rgb2hsv(vector c) {
    float min = c.x; if(c.y < min) min = c.y; if(c.z < min) min = c.z;
    float max = c.x; if(c.y >= max) max = c.y; if(c.z >= max) max = c.z;
    if(min == max) return <0, 0, max>;
    float d; float h;
    if(min == c.x) { d = c.y-c.z; h = 3; }
    else if(min == c.z) { d = c.x-c.y; h = 1; }
    else { d = c.z-c.x; h = 5; }
    return <(h-d/(max-min))*0.166666666667, (max-min)/max, max>;
}

HSL

More detail: "Wikipedia logo"HSL and HSV.

HSL stands for Hue, Saturation and Lightness or Luminance. HSL is available in the SL viewer color picker.

  • Hue is the perceived hue and wraps around in its range, forming a color circle.
  • Saturation is the "grayness" of the color: maximum saturation gives the most intense color, minimum gives grays.
  • Lightness/luminance determine the "whiteness" and "blackness" of the color: maximum value gives white, minimum gives black and the middle point gives the most intense color.

See uHSL2RGB and uRGB2HSL. Note that the linked functions may take differently styled inputs compared to ones in this page.

HSLuv/HPLuv

More detail: "Wikipedia logo"HSLuv and HSLuv.org.

HSLuv is a color space that attempts to combine the human-friendliness of HSV/HSL with the perceived luminous/chromatic stability of the "Wikipedia logo"CIE LCh(uv) color space. The conversion is multi-step and rather convoluted, containing many "magic numbers" that are not explained here; see HSLuv math and other implementations.

  • Hue is the perceived hue and wraps around in its range, forming a color circle.
  • Saturation is the "grayness" of the color: maximum saturation gives the most intense color, minimum gives grays. Using saturation over 1.0 may also produce usable colors, but the resulting RGB value may not be within spec.
  • Luminance determine the "whiteness" and "blackness" of the color: maximum value gives white, minimum gives black and the middle point gives the most intense color.
  • Although the parameters are like HSL, the results are different.
  • Using S and L outside the [0, 1] range may also produce usable colors, but the resulting RGB values won't be within spec.
  • There is currently no LSL implementation for RGB->HSLuv on this page.

HPLuv is a variation of HSLuv that maintains a very stable perceived luminosity, but produces only desaturated/pastel colors. Its parameters are the same as HSLuv.

  • No info for what P stands for.
  • Using P and L outside the [0, 1] range may also produce usable colors, but the resulting RGB value won't be within spec and the luminous uniformity degrades.
  • There is currently no LSL implementation for RGB->HPLuv on this page.
// Takes a Hue-Saturation-Luminance vector
// H in range [0, 1[
// S, L in range [0, 1], can also be overdriven
// Returns a LSL RGB vector
vector hsluv2rgb(vector col) {
    // HSLuv to LCh(uv), except in hCL order
    float hcos = llCos(TWO_PI*col.x); float hsin = llSin(TWO_PI*col.x);
    float l = (col.z+0.16); l = l*l*l/1.560896; if(l < 0.0088564516) l = col.z/9.032962962;
    if(l) {
        float temp; list intercepts;
        temp = col.z*-6.3705630182411/(hsin+8.0217391304349*hcos);
        if(temp >= 0) intercepts += temp;
        temp = col.z*((l-1)*769860.0)/(l*-120846.46173275+126452)/
               (hsin-hcos*l*969398.79085627/(l*-120846.46173275+126452));
        if(temp >= 0) intercepts += temp;
        temp = col.z*-3.6495554177681/(hsin-1.3259649910233*hcos);
        if(temp >= 0) intercepts += temp;
        temp = col.z*((l-1)*769860.0)/(l*-210946.24190439+126452)/
               (hsin-hcos*l*-279707.33175316/(l*-210946.24190439+126452));
        if(temp >= 0) intercepts += temp;
        temp = col.z*1.1091899201577/(hsin+0.12162162162162*hcos);
        if(temp >= 0) intercepts += temp;
        temp = col.z*((l-1)*769860.0)/(l*694074.10400063+126452)/
               (hsin-hcos*l*-84414.418054130/(l*694074.10400063+126452));
        if(temp >= 0) intercepts += temp;
        col.y *= llListStatistics(LIST_STAT_MIN, intercepts);
    } else return <0, 0, 0>;
    // hCL to Luv
    col = <col.z, hcos*col.y, hsin*col.y>;
    // Luv to XYZ, except in YXZ order
    float u = col.y/(13*col.x)+0.19783000664283;
    float v = col.z/(13*col.x)+0.46831999493879;
    if(col.x > 0.08) { col.x = (col.x+0.16)/1.16; col.x *= col.x*col.x; } else col.x /= 9.032962962;
    col.y = -9*col.x*u/(-4*v);
    col.z = (9*col.x-15*v*col.x-v*col.y)/(3*v);
    // YXZ to linear RGB
    col = <col.y*3.240969941904521+col.x*-1.537383177570093+col.z*-0.498610760293,
           col.y*-0.96924363628087+col.x*1.87596750150772+col.z*0.041555057407175,
           col.y*0.055630079696993+col.x*-0.20397695888897+col.z*1.056971514242878>;
    // linear RGB to sRGB
    if(col.x > 0.0031308) col.x = 1.055*llPow(col.x, 0.41666666666667)-0.055; else col.x = 12.92*col.x;
    if(col.y > 0.0031308) col.y = 1.055*llPow(col.y, 0.41666666666667)-0.055; else col.y = 12.92*col.y;
    if(col.z > 0.0031308) col.z = 1.055*llPow(col.z, 0.41666666666667)-0.055; else col.z = 12.92*col.z;
    return col;
}

// Takes a Hue-P-Luminance vector
// H in range [0, 1[
// P, L in range [0, 1], can also be overdriven
// Returns a LSL RGB vector
vector hpluv2rgb(vector col) {
    // HPLuv to LCh(uv), except in hCL order
    float l = (col.z+0.16); l = l*l*l/1.560896; if(l < 0.0088564516) l = col.z/9.032962962;
    if(l) {
        float nom; float den; list intercepts;
        intercepts += col.z*0.78806252071086;
        nom = col.z*((l-1)*769860.0)/(l*-120846.46173275+126452); if(nom<0) nom = -nom;
        den = (l*969398.79085627/(l*-120846.46173275+126452)); den = llSqrt(den*den+1);
        intercepts += nom/den;
        intercepts += col.z*2.1974976111232;
        nom = col.z*((l-1)*769860.0)/(l*-210946.24190439+126452); if(nom<0) nom = -nom;
        den = (l*-279707.33175316/(l*-210946.24190439+126452)); den = llSqrt(den*den+1);
        intercepts += nom/den;
        intercepts += col.z*1.1010763526520;
        nom = col.z*((l-1)*769860.0)/(l*694074.10400063+126452); if(nom<0) nom = -nom;
        den = (l*-84414.418054130/(l*694074.10400063+126452)); den = llSqrt(den*den+1);
        intercepts += nom/den;
        col.y *= llListStatistics(LIST_STAT_MIN, intercepts);
    } else return <0, 0, 0>;
    // hCL to Luv
    col = <col.z, llCos(TWO_PI*col.x)*col.y, llSin(TWO_PI*col.x)*col.y>;
    // Luv to XYZ, except in YXZ order
    float u = col.y/(13*col.x)+0.19783000664283;
    float v = col.z/(13*col.x)+0.46831999493879;
    if(col.x > 0.08) { col.x = (col.x+0.16)/1.16; col.x *= col.x*col.x; } else col.x /= 9.032962962;
    col.y = -9*col.x*u/(-4*v);
    col.z = (9*col.x-15*v*col.x-v*col.y)/(3*v);
    // YXZ to linear RGB
    col = <col.y*3.240969941904521+col.x*-1.537383177570093+col.z*-0.498610760293,
           col.y*-0.96924363628087+col.x*1.87596750150772+col.z*0.041555057407175,
           col.y*0.055630079696993+col.x*-0.20397695888897+col.z*1.056971514242878>;
    // linear RGB to sRGB
    if(col.x > 0.0031308) col.x = 1.055*llPow(col.x, 0.41666666666667)-0.055; else col.x = 12.92*col.x;
    if(col.y > 0.0031308) col.y = 1.055*llPow(col.y, 0.41666666666667)-0.055; else col.y = 12.92*col.y;
    if(col.z > 0.0031308) col.z = 1.055*llPow(col.z, 0.41666666666667)-0.055; else col.z = 12.92*col.z;
    return col;
}
The HSLuv LSL code is heavily modified from the Javascript version, MIT licensed:
Copyright (c) 2012-2022 Alexei Boronine

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

LSL port 2023 Frionil Fang.

Linear RGB

Typically colors shown on screen are in the "Wikipedia logo"sRGB color space, but some features, such as point lights expect linear RGB color instead.

See the built in functions llsRGB2Linear and llLinear2sRGB.

Clamping Functions

Hue-based colors are often specified with hue as degrees (0-360°) and saturation/value/lightness/luminance as a value from 0 to 100. Likewise, RGB colors are often given as integers in the range 0-255, as opposed to the LSL colors in the range 0-1. The conversion into the range expected on this page requires just simple division.

// Scales a hue-style color vector into [0, 1] range
vector hxx_scale(vector hxx) {
    return <hxx.x/360.0, hxx.y/100.0, hxx.z/100.0>;
}

// Scales an integer color vector into [0, 1] LSL color range
vector rgb_scale(vector rgb) {
    return rgb/255.0;
}

To ensure color vectors are in the proper range before calling a conversion, you can clamp/wrap them with the following helper functions. If necessary, scale the colors first.

// Wraps & clamps a hue-style color vector
// Hue wrapped to the range [0, 1[
// Others clamped to [0, 1]
vector hxx_clamp(vector hxx) {
    hxx.x -= (integer)hxx.x;
    if(hxx.x < 0) hxx.x = 1-hxx.x;
    if(hxx.y < 0) hxx.y = 0; else if(hxx.y > 1) hxx.y = 1;
    if(hxx.z < 0) hxx.z = 0; else if(hxx.z > 1) hxx.z = 1;
    return hxx;
}

// Clamps a RGB vector to the [0, 1] range
vector rgb_clamp(vector rgb) {
    if(rgb.x < 0) rgb.x = 0; else if(rgb.x > 1) rgb.x = 1;
    if(rgb.y < 0) rgb.y = 0; else if(rgb.y > 1) rgb.y = 1;
    if(rgb.z < 0) rgb.z = 0; else if(rgb.z > 1) rgb.z = 1;
    return rgb;
}

Visual Examples

These images show a visual comparison of some of the colorspaces described, rendered inworld with above functions on a 64x24 grid. Hue ranges from 0 to 1 horizontally, saturation from 0 to 1 vertically. Value/lightness/luminosity depend on the image.