Color space conversions

From Second Life Wiki
Revision as of 07:09, 2 November 2023 by Frionil Fang (talk | contribs) (example images)
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.
  • There is currently no LSL implementation for RGB->HSLuv.

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 over 1.0 may also produce usable colors, but the resulting RGB value may not be within spec and the luminous uniformity degrades.
  • Currently no LSL implementation for RGB->HPLuv.
// Takes a Hue-Saturation-Luminance vector
// H in range [0, 1[
// S, V in range [0, 1], S 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, V in range [0, 1], P 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;
}

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. Hue ranges from 0 to 1 horizontally, saturation from 0 to 1 vertically. Value/lightness/luminosity depend on the image.