Colorspaces in XSI

January 6th, 2007 by Harry Bardak - Viewed 96758 times -

The graphics pipeline from source art to final output is complicated, and requires the artist to work in several different colour spaces along the way. In this article I’ll give a brief overview of colour spaces, and then detail a commonly overlooked area in the texture pipeline where gamma is important.

The sRGB Standard

The sRGB colour space is based on the monitor characteristics expected in a dimly lit office, and has been standardised by the IEC (as IEC 61966-1-2). This colour space has been widely adopted by the industry, and is used universally for CRT, LCD and projector displays. Modern 8-bit image file formats (such as JPEG 2000 or PNG) default to the sRGB colour space.

A value in the sRGB colour space is a floating-point triple, with each value between 0.0 and 1.0. Values outside of this range are clipped. An sRGB colour from this [0, 1] interval is commonly encoded as an 8-bit unsigned integer between 0 and 255.

The pivotal fact to remember about sRGB is that it is non-linear. It roughly follows the curve y = x 2.2, although the actual standard curve is slightly more complicated (and will be listed at the end of this article). A graph of sRGB against gamma 2.2 looks as follows:

This mapping has the nice property that more resolution is given to low-luminance RGB values, which fits the human visual model well.

The Gamma Function As An Approximation

As can be seen by the above graph, the sRGB standard is very close to the gamma 2.2 curve. For this reason, the full sRGB conversion function is often approximated with the much simpler gamma function.

Please note that the value associated with the word gamma is the power value used in the function y = xp. Unfortunately gamma is often associated with brightness, which is not exactly what it is doing. The full [0, 1] interval is always mapped back onto the full [0, 1] interval.

What Maths Work In This Colour Space?

In general your lighting pipeline should be done in linear space, so that all lighting is accumulated linearly. This is the approach taken in many film pipeline, and is the only way to ensure that you are being physically correct.

However, assuming that the gamma function approximation is good enough, you can still perform modulate operations. In this case we have some constant A that we wish to modulate our sRGB source data x with, and store the result in sRGB as y. In linear space this would be written as:

y2.2= A x2.2 = ( A1/2.2 x )2.2

Since we are working only in the [0, 1] interval, we can remove the power from both sides and work in the sRGB space itself. In which case:

y = A 1/2.2 x

So if we convert our constants into sRGB, then modulate operations can still be performed. However, there are only very few operations that work this way. Additive operations (which are used in additive lighting models, or for alpha-blending) cannot be reformulated to work in a gamma 2.2 space, simply because the space is non-linear. If you wish to have a correct additive lighting model, then you have to work in a linear space, which will mean that you need a higher-precision framebuffer to at least match the low-luminance granularity of sRGB.

sRGB to linear RGB: rgb (sRGB), RGB (linear RGB)

R = r / 12.92 for r <= 0.04045
( (r + 0.055)/1.055 )2.4 for r > 0.04045
G = g / 12.92 for g <= 0.04045
( (g + 0.055)/1.055 )2.4 for g > 0.04045
B = b / 12.92 for b <= 0.04045
( (b + 0.055)/1.055 )2.4 for b > 0.04045

This is commonly approximated as X = x 2.2 for all channels.

linear RGB to sRGB: RGB (linear RGB), rgb (sRGB)

r = 12.92 R for R <= 0.0031308
1.055 R 1.0 / 2.4 – 0.055 for R > 0.0031308
g = 12.92 G for G <= 0.0031308
1.055 G 1.0 / 2.4 – 0.055 for G > 0.0031308
b = 12.92 B for B <= 0.0031308
1.055 B 1.0 / 2.4 – 0.055 for B > 0.0031308

This is commonly approximated as x = X 1/2.2 for all channels.

Ok But How To Do That in XSI ?

Before the release of XSI v6, we could change the ouput gamma of Mental ray and match approximately the sRGB by using a gamma of 2.2 ( put 1/2.2 in the active effect tab). In version 6 this option is missing unfortunately but don’t worry there are others third party addon that can do the job for you.

I wrote one of them and you can find them here.

This addon include two shaders. A texture node that convert sRGB image in linear space and a lens shader that convert linear render to sRGB color space.

These shader are intended to be used in a context where you don’t output anything in float format. I didn’t try it in this context so you don’t have any warranty.

The idea is to convert every texture that you create in Lin space and convert every render in sRGB space. By texture I mean any images that are not used to drive data such as displacement map, bump map or normal map. I exclude also floating point image which are considered linear defacto.

Let’s see these shader in action

I will first focus on the lens shader and then on the texture node.

As I said we need to watch our result according to the monitor profile (sRGB) so to convert our render we need to apply the lens shader. It’s really easy use add it on the lens shader stack.

My first example is simple. A grid with a phong shader and a light. An area light with a realistic falloff. To do this I am using the D2S_light with a temperature of 6500 K (White). I set the light intensity to get the same result in the red area.

The left picture is rendered as-is in XSI while the other one is rendered with Lin_to_sRGB Lens shader. As you can see the left picture is overlighted. (intensity around 4000) While the right one behave nicely (with an intensity of 750 only).

Note that even the specular from phong shader looks correct. It looks like the reflection of the area light.

My second example is a simple FG scene with 2 spheres and a plane. The plane and one of the sphere got a DGS shader with only a diffuse term set to a neutral gray. The second sphere is fully reflective to see the environment map.

I used an HDR image from Paul Debevec’s website called beach.hdr to light my scene with Final Gathering.

The first test is a render as-is:

As you can see the color of the environement map are more contrast / darker than what we can see in HDR shop. To correct this naturally we will change the exposure but the color will be affected and we will got a more saturated image which is wrong.

If we use the lens shader we go this result:

The result this time is what we expected. The color match to what we can see in HDR shop. We can start to work safely because we got the right illumination.

On my next example I am introducing a texture on the grey plane by plug to the diffuse slot an image node. I am keeping the same HDR illumination and the lens shader.

As you can see the texture is washed out. This doesn’t come from illumination. This wood texture has been generated in sRGB color space. With the lens shader we simply apply an another time the sRGB convertion. What we need is to convert this texture in linear space before the render. It is the purpose of the sRGB_to_Lin texture node. Just plug it between your image node and the diffuse slot and re-render.

As you can see now our texture is corrected and looks natural.


If you need to change your setup and include these two node to your existing setup your can use the existing script written by Guillaume Laforge and included in the first archive. It will insert a sRGB_to_Lin node after your image node.

If you have any questions, critics or correction please don’t hesitate :

Edit : Gradient example removed because it seams to confuse more people than it helps. Shaders source code fixed I didn’t realised that this article was read by non XSI users.

39 Responses to “Colorspaces in XSI”

  1. anthony barritault says:

    Thank you for this Harry, very informative and helpful!


  2. Bullit says:

    Many thanks. Small note the link to srgb doesnt work.

  3. Harry Bardak says:

    I will fix the link tonight.

    In the meantime you can find it here :

    Sorry about that.

  4. The download for is fixed. Sorry for the mishap.

  5. Bullit says:

    Thanks for the fix.

  6. Erklaerbar says:

    oh dear, didnt know it was that tricky. Many thanks for the article and shaders, makes all sense now

  7. Bill spitzak did a page about this a while back and it’s a huge topic on the vray forums for getting more natural predictable renders – here’s his page –

  8. Brian Meanley says:

    Nice explanation, and thanks for the shaders, they will certainly come in handy. One question I have though is if you are required to convert your image maps to linear space using your supplied shader, are you also required to use this shader on a flat shaded object, say like the non-reflective sphere, or the non-image mapped ground plane (or even on any procedurally based texture)? Because it seems to me that even though these colors are selected directly in xsi, they still seem to get washed out at rendertime. Sorry for the long question, if someone would rather answer this in one of the main forums instead of here, that’d be fine with me (just point me in the direction). Thanks again.

  9. Harry Bardak says:

    Actually you don’t have to do this on non textured object. Because the color are generated in a linear space.
    If you select let say a middle grey (value .5 ), it would be displayed correctly to a 18-20% grey ( which is the middle grey in photography ).

    Make sure that you don’t apply this correction on displacement, bump, or normal maps. Theses maps contain data and by “correcting” these map you will bias the data. We don’t want that.

  10. Brian Meanley says:

    Thanks for the quick response. Alright, it all makes sense when it is explained…however…how come when I use 2 middle greys (one picked in xsi’s diffuse color, and one 50% grey image from photoshop each mapped to a separate sphere) i get two different results? The one from photoshop is converted to linear space using your shader, while the one from xsi is left as is. The one from photoshop returns 50% grey when rendered, however the one from xsi with no image returns a much lighter grey. It seems to me like I may be doing something that I shouldn’t be…should I be selecting colors/creating textures in photoshop in some sort of adjusted colorspace to match the colors from xsi or perhaps I shouldn’t be using the rgbToLin shader at all in the case? Again, sorry about the confusion, just trying to sort out the workflow for using this. Thanks!

  11. Harry Bardak says:

    I think I have answer a bit too quickly.
    I didn’t test it in XSI as you did, I just took the F-curve to answer and unfortunetly it was the wrong one. Sorry my bad.

    So what I should have say is :

    “If you select let say a 18-20% grey ( which is the middle grey in photography ), it would be displayed correctly to middle grey (value .5 ). ”

    Your result makes perfectly sense and you didn’t do anything wrong.

    It’s quite rare to get just a shade without the texture but if you need this you have to apply the conversion too.

  12. Brian Meanley says:

    Alright, makes perfect sense, just wanted to double check to make sure I was using it correctly. Thanks!

  13. gfxman says:

    ” It’s quite rare to get just a shade without the texture but if you need this you have to apply the conversion too. ”

    So it’s correct to say that also the basic shaders, like phong or lambert, are wrong and mus be corrected with this node if i’m not using any texture to drive it’s color ???

  14. Chris_TC says:

    You don’t need to apply the conversion to flat colors, but you can. Personally, I never bother with that. Just keep in mind that the color you select in XSI will end up considerably brighter in the render.
    A 0.18 gray will be rendered as a 50% gray.

    Therefore, if I try to create e.g. a dark gray plastic I won’t give it a 0.15 gray value because I know that this does not equal a dark gray once converted to gamma 2.2.
    Instead, I’ll give it maybe a 0.05 gray.

    As Harry said, non-textured diffuse values are quite rare, so it’s not a huge issue.

  15. Kris Rivel says:

    Any chance your conversion shaders will be published for Win64 anytime soon?

  16. Chris says:

    When you say “linear” do you mean “perceptually linear” or “radiometrically linear” ?

    I am looking into this very issue and there seems to be different uses of the term linear .

    Anyone talking about cineon files or film work uses “linear” to describe images that “look right” on a monitor . A grayscale “looks right” . As opposed to a log image which looks washed out . In this case – .18 looks like a 50% grey to the human eye .

    Many peoples talking about lighting theory and science use “linear” to describe even increases in energy being emitted . The standard grayscale would not be linear . But it seems to me , that this the space the lighting calculations should be done . This would be “radiometrically” or “photometrically” linear

  17. Luc-Eric says:

    the article is explaining that calculations need to be in the mathematically correct linear color space, and the viewing in sRGB. This linear space being “mathematically correct” answers your question as to whether it’s perceptual or not.

    it’s pretty much only in the case of cineon’s log2lin that the word “linear” has been bastardized, because historically it was used to convert the log space data into linear voltage values required for proper video display.

  18. Arman Sernaz says:

    great article ! THANKs a LOT !!! :)

    linear space rullaz !!!

  19. Jeromee says:

    Hi Harry,

    Looking at IEC 61966-2-1 where conversions between sRGB and linear RGB are defined, I have the feeling that in LIN_to_sRGB.cpp:

    result->r = pow(Ci.r *1.055, 1/2.4)- 0.055;

    should read instead:

    result->r = 1.055 * pow(Ci.r, 1/2.4)- 0.055;

  20. Szabolcs Matefy says:

    Anyone has a 64 bit version for XSI6.5? It would be really nice to have it…

  21. harry bardak says:

    Hi Jeromee.

    I have double checked in IEC 61966-2-1 specification.
    No that the formula for sRGB to LIN not LIN to sRGB.


  22. Hi everyone, i would share whith you a publication oh this shaders for win 64.

    Well, it’s a test for me because i’m not a programmer and it’s the first time for me to create a dll with a IDE.

    I hope i dont make any mistakes.

    So the archive is here:

    Enjoy and thank you to the author.

  23. szadam says:

    where can i find the lens shader? i mean i go to camera/lens shaders/ but i cant find that lin2srgb stuff
    i installed the spdl, so what am i doing wrong?

  24. Jeromee says:

    Hi Harry,

    I still tend to differ on the formula implemented in your source file LIN_to_sRGB.cpp. As specified in IEC 61966-2-1, the “1.055″ must be outside the parenthesis, not inside. Otherwise the sRGB LIN conversions would not be bijective.

    Let’s take the simple example of LIN=1.0 converted to sRGB. If we use your formula:

    result->r = pow(Ci.r *1.055, 1/2.4)- 0.055;

    then the result is 0.968.

    If we use what I read from IEC 61966-2-1:

    result->r = 1.055 * pow(Ci.r, 1/2.4)- 0.055;

    then the result is 1.0 (and obviously the 0.055 is intended to exactly compensate the 1.055).

    The error on output sRGB value is at most 3.2 %, so from a render standpoint it is not so perceptible.


  25. mac_apple says:

    very go0d!! but alike photoshop I can do nice ..thankss

  26. harry bardak says:

    oh crap you are totally right. I have even put the correct formula in my explanation but not in the shader.
    I am going to fix that.
    thanks !

  27. Splin says:

    The sRGB to LIN script seems not to be working in 6.5?

  28. Gene Crucean says:

    Hey Harry, have you updated the code on this?

  29. harry bardak says:

    Use a gamma 1/2.2 node instead and the simple_tone_mapping shader.
    That would do the trick.

    There is no point to change that now.

  30. Harry Bardak says:

    Obviously with XSI 7 you don’t need to bother with theses shaders. But the concept stay the same.

  31. Mako says:

    Hey Harry,

    We just had lunch earlier today and I just realize now that I had read your explanations while I was doing my tests using the spheron camera.

    Well done. I’d be happy to discuss even longer about all this with you one day.

  32. Adrian P says:

    I don’t think your images in “sRGB Example: Gray gradient” are correct.

    If you take the linear-light ramp image on the left and pass it through the linear-light to sRGB function, the image should appear lighter, not darker like the image on the right.

  33. James Rowell says:

    Adrian – I was just jumping on this site to make a comment about the same thing. I think Harry has it wrong in the “sRGB Example: Gray Gradient” section. There are a few confusing red-herrings across the web regarding linear colorspace workflow, and this was one that took me a while to straighten out for myself as I was trying to learn this stuff. Since so many people link to this site, it’s worth fixing.

    So Harry – if one is looking at a zero to one ramp in shake, with NO LUT on the viewer AND assuming that you are using a gamma 2.2 monitor, THEN the ramp on the left should look correct to your eye. You are wrong to think that your example on the left is too bright. Here’s how you can convince yourself – cut and paste the following nodes into shake.

    Ramp1 = Ramp(21, 486, 1, 1, 0.5, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0);
    Bytes1 = Bytes(Ramp1, 4);
    Resize1 = Resize(Bytes1, 720, height, “impulse”, 0);
    linRGB_to_sRGB_1 = ColorX(Resize1, r<=0.0031308 ? r*12.92 : pow(r*1.055, 1.0/2.4) – 0.055,
    g<=0.0031308 ? g*12.92 : pow(g*1.055, 1.0/2.4) – 0.055, b<=0.0031308 ? b*12.92 : pow(b*1.055, 1.0/2.4) – 0.055,
    a<=0.0031308 ? a*12.92 : pow(a*1.055, 1.0/2.4) – 0.055, z);
    sRGB_to_linRGB_1 = ColorX(Resize1, r<=0.04045? r/12.92 : pow((r+0.055)/1.055, 2.4),
    g<=0.04045? g/12.92 : pow((g+0.055)/1.055, 2.4), b<=0.04045? b/12.92 : pow((b+0.055)/1.055, 2.4),
    a<=0.04045? a/12.92 : pow((a+0.055)/1.055, 2.4), z);

    So – if you examine the output of my “resize” node – the ramp looks perfect on a shake viewer with NO LUT on a gamma 2.2 monitor. That is part of the whole point of gamma monitors – that equal steps in value look like equal steps up in value to our eye. If you look at the sRGB_to_linRGB node output, it’s FAR too dark in the blacks, i.e.; the steps do NOT look like equal steps up the scale. (Clearly previwing the linRGB_to_sRGB is way off.)

    Hope you can fix your post Harry – it’s a little confusing to the newly indoctrinated to sort this out. I hope my commnt helps! Feel free to use my shake snippet if it helps you rework the writeup.

    Thanks for posting this though – it is very helpful.

  34. James Rowell says:

    ooops – that’s an old copy of my sRGB_to_linRGB, and visa-versa, nodes. If anyone is thinking of using my code there (for use elsewhere), please change the colorx alpha channel to just be “a”, i.e.; NO correction on alpha. Sorry! :-)

  35. James Rowell says:

    Another mistake in my little post – when posting a comment here the blog translates the ascii double quote character into something a little fancier. When you paste the code above into shake – it breaks the “resize” node. Just be sure to select “filter” type “impulse” to see what I’m talking about. Thanks – and sorry for the multiple posts in a row.

  36. James Rowell says:

    Ok – fourth time is a charm? Corrected shake nodes as per Jeromee’s points above.

    linRGB_to_sRGB_1 = ColorX(0,
    r<=0.0031308 ? r*12.92 : (1.055 * pow(r, 1.0/2.4)) – 0.055,
    g<=0.0031308 ? g*12.92 : (1.055 * pow(g, 1.0/2.4)) – 0.055,
    b<=0.0031308 ? b*12.92 : (1.055 * pow(b, 1.0/2.4)) – 0.055,
    a, z);
    sRGB_to_linRGB_1 = ColorX(0,
    r<=0.04045? r/12.92 : pow((r+0.055)/1.055, 2.4),
    g<=0.04045? g/12.92 : pow((g+0.055)/1.055, 2.4),
    b<=0.04045? b/12.92 : pow((b+0.055)/1.055, 2.4),
    a, z);

  37. Harry Bardak says:

    it s all fixed now

  38. [...] Softimage Blog » Blog Archive » Colorspaces in XSI [...]