This example reads the shader source from text files. The vertex shader (LUTShaderVert.txt located in examples/doc/shaders) contains the following code:

void main (void)

{
  gl_TexCoord[0] = gl_MultiTexCoord0;
  gl_Position = ftransform();
}

This basic vertex program passes along the texture coordinate and then applies a transform to the vertex to correctly position it on the screen. The gl_TexCoord[0] is a varying variable that transmits data from the vertex program to the fragment shader program.

The fragment shader (LUTShaderFrag.txt located in examples/doc/shaders) contains the following code:

uniform sampler2D _IDL_ImageTexture;
uniform sampler1D lut;
 
void main(void)
{
  float i = texture2D(_IDL_ImageTexture, gl_TexCoord[0].xy).r;
  gl_FragColor = texture1D(lut, i);
}

The fragment shader is where the lookup happens. The uniform variable, lut, which was defined in the IDL application using SetUniformVariable, contains the lookup table in a 1-D texture (of GLSL type sampler1D). As previously explained, the LUT

is loaded into a texture map for efficiency.

The _IDL_ImageTexture variable is a reserved uniform variable that provides access to the 2-D base image (of GLSL type sampler2D). When a shader object is associated with an IDLgrImage object, and the uniform variable is not defined using SetUniformVariable in the IDL application, the base image object (a texture mapped onto a rectangle) is stored in a reserved uniform variable named _IDL_ImageTexture. The base image is the IDLgrImage to which the shader is attached. If it is attached to more than one image, the base image is the one currently being shaded. Non-base images are those passed to the shader program using SetUniformVariable.

Since more than one texture is used in the rendering of the image (the _IDL_ImageTexture base image texture and the LUT texture), this is referred to as multi-texturing.

The GLSL texture2D procedure call reads the texel at the current texture coordinate. This procedure typically returns a floating point, four-element vector (containing red, green, blue, and alpha values). But with a greyscale image, the red, green, and blue values are the same, so the appending .r keeps only the red channel and assigns it to the float i.

The GLSL texture1D procedure takes two parameters, the lut and i (the texture coordinate that instructs it which texel to sample). This value normally ranges from 0.0 to 1.0 (0.0 being the first texel, 1.0 the last). Since the value read from the image into i also normally ranges between 0.0 and 1.0, it is possible to use it directly as a texture coordinate to do the lookup.

When performing a lookup on the CPU, you directly access the LUT array using the pixel value as the index. A pixel value of 0 corresponds to the first entry in the LUT and a pixel value of 255 corresponds to the last entry.

However, in a shader program the texture coordinate lookup is possible because before a pixel reaches the fragment shader it is converted to floating point by OpenGL. In the case of an 8-bit greyscale image, the range is 0.0 to 1.0. That means a pixel with value 0 becomes 0.0 and 255 becomes 1.0. When doing the coordinate texture lookup on the GPU, the texture1D procedure does the lookup by using the converted pixel values where pixel value of 0 corresponds to the first LUT entry and a pixel value of 1.0 (converted from 255) corresponds to the last entry.

Assign LUT Shader Program to Shader Object


You need to supply the program code to the shader object so that it is available to the graphics card when it is needed. To accomplish this, you can use shader object properties VERTEX_PROGRAM_FILE and FRAGMENT_PROGRAM_FILE to associate external shader program components with the shader object.

Add the following code to the bottom of your Init function:

vertexFile=filepath('LUTShaderVert.txt', $
  SUBDIRECTORY=['examples','doc', 'shaders'])
fragmentFile=filepath('LUTShaderFrag.txt', $
  SUBDIRECTORY=['examples','doc', 'shaders'])
   
self->IDLgrShader::SetProperty, $
  VERTEX_PROGRAM_FILENAME=vertexFile, $
  FRAGMENT_PROGRAM_FILENAME=fragmentFile

At this point, you can easily add image display code to your program and test your LUT shader. The result of applying one of IDL’s pre-defined colortables appears in the following figure.

Software Fallback for the LUT Shader


The following code performs the LUT lookup. When there is not sufficient hardware support for shaders or when the FORCE_FILTER keyword is set on initialization, the colortables changes result from the following code instead of a shader program. You will likely find that performance slows significantly.

Function shader_lut_doc::Filter, Image
 
; Allocate return array of same dimension and type.
sz = SIZE(Image)
newImage = FLTARR(sz[1:3], /NOZERO)
 
; Get the LUT uniform variable.
self->GetUniformVariable, 'lut', oLUT
 
; Read the LUT data from the 1-D image.
oLUT->GetProperty, DATA=lut
  FOR y=0, sz[3]-1 DO BEGIN
    FOR x=0, sz[2]-1 DO BEGIN
   
    ; Read from the image.
    idr = Image[0,x,y]
   
    ; Convert from 0.0-1.0 back to 0-255.
    idr *= 255
   
    ; Get the number of image channels.
    szlut = SIZE(lut)
    IF szlut[0] EQ 1 THEN BEGIN
   
      ; Greyscale LUT, only 1 channel.
      grey = lut[idr]
      fgrey = FLOAT(grey) / 255.0
      newImage[0,x,y] = fgrey
      newImage[1,x,y] = fgrey
      newImage[2,x,y] = fgrey
      newImage[3,x,y] = 1.0
    ENDIF ELSE BEGIN
      ;; RGB LUT.
      rgb = lut[*, idr]
      frgb = FLOAT(rgb) / 255.0
      newImage[0:2,x,y] = frgb
      newImage[3,x,y] = 1.0
    ENDELSE
  ENDFOR
ENDFOR
RETURN, newImage
END

IDL always passes the image to the Filter method in RGBA floating-point pixel-interleaved format, so you do not have to worry about a lot of input data combinations. IDL also clamps the data this function returns to the [0.0, 1.0] range and scales it to the correct pixel range, usually [0, 255], for your display device.

Note: Uniform variables are, in a sense, free-form properties in the IDLgrShader superclass. Within the Filter method, accessing the lut texture map from the uniform variable maintains consistency since this is same place the hardware shader obtains it. This reduces the chance for confusion.

At this point, you can test your work by writing a simple display program that loads your data into an IDLgrImage object, creates an instance of your shader_lut_doc object and attaches the LUT to your image object by setting the object reference of the shader in the SHADER property of IDLgrImage. You also need to set the FORCE_FILTER property on class initialization so that the filter fallback runs, even if you have shader hardware:

oLUTshader = OBJ_NEW('shader_lut_doc', /FORCE_FILTER)