Color space conversions
A collection of 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: 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.
These functions are optimized for performance and don't do input verification. See clamping functions if you need to ensure the inputs are in the correct range. |
// 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: 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: 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 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.
These functions don't verify their inputs. See clamping functions if you need to ensure the inputs are in the correct range. |
// 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 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.