Altering RGB Levels Using a Shader
This shader program example lets you interactively apply color level correction to an image when you view it. This does not modify the image data itself. This example places the original image data in an IDLgrImage object and attaches the custom shader object using the SHADER property. It then creates a simple user interface that lets you alter the color levels and passes these values to the shader program in a named uniform variable. The Filter method implements the software fallback. When the correct graphics hardware is unavailable, IDL automatically calls the Filter method.
Example Code
See shader_rgb_doc__define.pro, located in the examples/doc/shaders subdirectory of the IDL distribution, for the complete, working example. Run the example by creating an instance of the object at the IDL command prompt using orgbshader=OBJ_NEW('shader_rgb_doc') or view the file in an IDL Editor window by entering .EDIT shader_rgb_doc__define.pro.
The example code differs slightly from that presented here for the sake of clarity. Whereas the working example includes code needed to support user interface interaction, the following sections leave out such modifications to highlight the shader program components.
Basic RGB Shader Object Class
First, create a basic object class that inherits from IDLgrShader:
; Initialize object. FUNCTIONshader_rgb_doc::Init, _EXTRA=_extra IF NOT self->IDLgrShader::Init(_EXTRA=_extra) THEN $ RETURN, 0 RETURN, 1 END ; Clean up. PROshader_rgb_doc::Cleanup self->IDLgrShader::Cleanup END ; Filter method for software fallback option. FUNCTIONshader_rgb_doc::Filter, Image RETURN, Image END ; Class definition. PROshader_rgb_doc__define COMPILE_OPT hidden struct = {shader_rgb_doc, $ INHERITS IDLgrShader $ } END
Uniform Variable for RGB Values
In this example, a uniform variable contains the values of the red, green and blue levels. You can set or change uniform variables anytime before you draw the scene and their values will remain in effect until you change them again. These types of variables are perfect for making minor adjustments to the image filter and then viewing the image to see if the result is satisfactory.
First set the uniform variable to a reasonable default value such as [1,1,1] before you start, otherwise the shader program defaults of [0,0,0] will make the image look dim. Add the following line to your Init function:
self->SetUniformVariable, 'scl', [1.0, 1.0, 1.0]Warning
The uniform variable name is case-sensitive, unlike most variable names in IDL.
This example lets you change color levels using sliders. You can read the slider values from your GUI, and modify the uniform variable at any time. Assuming that the instance of your shader_rgb_doc object is called oShaderRGB and red, green and blue are floating point values, update the value of the uniform variable as follows:
Once the needed elements are defined, associate your shader object with oImage, an image object (that has been previously defined).
Once the shader object is associated with the image, shader program display updates are activated any time the SetUniformVariable method is called.
Software Fallback for RGB Shader
IDL calls the Filter method when shader functionality is not supported by the graphics hardware. Providing a software-based fallback is never a requirement and you may choose not to if you know sufficient hardware will always be available. However, it is good practice to write this method just in case the application is ever executed on a machine without suitable hardware.
In the Filter method, retrieve the uniform variable values using GetUniformVariable, and then return a modified copy of the image data.
Function shader_rgb_doc::Filter, Image newImage=Image self->GetUniformVariable, 'scl', s newImage[0,*,*] *= s[0] newImage[1,*,*] *= s[1] newImage[2,*,*] *= s[2] RETURN, newImage END
IDL always passes the image to the Filter method in RGBA floating-point pixel-interleaved format, so you don't 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 scale vector 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_rgb_doc object, and attaches the filter 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. You can force use of the fallback either when creating the shader object:
or explicitly in the shader object's Init method:
FUNCTION shader_rgb_doc::Init, _EXTRA=_extra IF NOT self->IDLgrShader::Init(_EXTRA=_extra, /FORCE_FILTER) $ THEN $ RETURN, 0 ...
Hardware Shader Program for RGB Shader
The OpenGL Shading Language (GLSL) is a vast subject that requires extensive study to develop an expert level of programming, a subject that is impossible to cover here. However, this example is relatively simple and you can likely easily follow along with the code required for the vertex and fragment shader portions of the shader program. All shader programs need a vertex program and a fragment program.
RGB Vertex Shader Program
The following vertex shader is fairly common among image filtering shader programs. Add the following code to the bottom of your Init function:
vertexProgram = $ [ $ 'void main (void) {', $ ' gl_TexCoord[0] = gl_MultiTexCoord0;', $ ' gl_Position = ftransform();', $ '}' ]
The first line after main() transfers the texture coordinate from OpenGL's Texture Unit 0 into the current texture coordinate predefined variable. Remember that IDL draws its images with texture maps applied to rectangles, so you need to pass along the texture coordinate. IDL always uses Texture Unit 0 when drawing images. The gl_TexCoord[0] is a varying variable that transmits data from the vertex program to the fragment shader program.
The next line in the program simply applies the current OpenGL ModelViewProjection transform to the vertex, so that it ends up in the right place on the screen.
RGB Fragment Shader Program
The fragment program of an image filtering shader program is where all the work happens. Add the following to the Init function as well:
fragmentProgram = $ [ $ 'uniform sampler2D _IDL_ImageTexture;', $ 'uniform vec3 scl;', $ 'void main(void) {', $ 'vec4 c = texture2D(_IDL_ImageTexture, gl_TexCoord[0].xy);', $ ' c.rgb *= scl;', $ ' gl_FragColor = c;', $ '}' ]
This GLSL code can be translated relatively easily. IDLgrImage uses textures to draw image data. Access the texture map associated with the base image's data in the IDL reserved uniform variable, _IDL_ImageTexture, which is automatically created for the base image. The sixth line in the program above fetches the image pixel (a texture texel) from the image texture and stores it in c, which is a 4-element vector that represents the RGBA channel data. Modify the color of the texel in the next tile using the uniform variable, scl, declared on line four. Finally, tell OpenGL about the new color for this particular pixel on the screen by setting gl_FragColor. OpenGL clamps the pixel color values to the appropriate range for your display.
This fragment program runs once for every pixel (fragment) on your screen that is covered by the image.
Assign RGB 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_STRING and FRAGMENT_PROGRAM_STRING to associate inline shader program components with the shader object.
Note
With more complicated (longer) shader programs, it may be easier to keep the shader program components in separate files. In such a case, associate the shader program elements with a shader object using the VERTEX_PROGRAM_FILE and FRAGMENT_PROGRAM_FILE properties.
Add the following code to the bottom of your Init function.
self->IDLgrShader::SetProperty, $ VERTEX_PROGRAM_STRING=STRJOIN(vertexProgram, STRING(10B)), $ FRAGMENT_PROGRAM_STRING=STRJOIN(fragmentProgram, STRING(10B))
Add newlines (STRING(10B)) so that the shader program compiler sees your program as a single long string containing many source code lines, instead of one long line. If you ever get a compile-time error, the shader compiler can tell you on what line the error occurred when you insert the newlines.
Tip
Remove the FORCE_FILTER keyword from the initialization function if you have been testing your software fallback.
The following image shows the result of modifying the RGB levels of an image of a rose.
