GLSL Programming/Unity/RGB Cube

This tutorial introduces varying variables. It is based on Section “Minimal Shader”.

In this tutorial we will write a shader to render an RGB cube similar to the one shown to the left. The color of each point on the surface is determined by its coordinates; i.e., a point at position $(x,y,z)$ has the color $({\text{red}},{\text{green}},{\text{blue}})=(x,y,z)$ . For example, the point $(x,y,z)=(0,0,1)$ is mapped to the color $({\text{red}},{\text{green}},{\text{blue}})=(0,0,1)$ , i.e. pure blue. (This is the blue corner in the lower right of the figure to the left.)

Preparations

Since we want to create an RGB cube, you first have to create a cube game object. As described in Section “Minimal Shader” for a sphere, you can create a cube game object by selecting GameObject > Create Other > Cube from the main menu. Continue with creating a material and a shader object and attaching the shader to the material and the material to the cube as described in Section “Minimal Shader”.

Here is the shader code, which you should copy & paste into your shader object:

Shader "GLSL shader for RGB cube" {
Pass {
GLSLPROGRAM

#ifdef VERTEX // here begins the vertex shader

varying vec4 position;
// this is a varying variable in the vertex shader

void main()
{
position = gl_Vertex + vec4(0.5, 0.5, 0.5, 0.0);
// Here the vertex shader writes output data
// to the varying variable. We add 0.5 to the
// x, y, and z coordinates, because the
// coordinates of the cube are between -0.5 and
// 0.5 but we need them between 0.0 and 1.0.
gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
}

#endif // here ends the vertex shader

#ifdef FRAGMENT // here begins the fragment shader

varying vec4 position;
// this is a varying variable in the fragment shader

void main()
{
gl_FragColor = position;
// Here the fragment shader reads intput data
// from the varying variable. The red, gree, blue,
// and alpha component of the fragment color are
// set to the values in the varying variable.
}

#endif // here ends the fragment shader

ENDGLSL
}
}
}

If your cube is not colored correctly, check the console for error messages (by selecting Window > Console from the main menu), make sure you have saved the shader code, and check whether you have attached the shader object to the material object and the material object to the game object.

Varying Variables

The main task of our shader is to set the output fragment color (gl_FragColor) in the fragment shader to the position (gl_Vertex) that is available in the vertex shader. Actually, this is not quite true: the coordinates in gl_Vertex for Unity's default cube are between -0.5 and +0.5 while we would like to have color components between 0.0 and 1.0; thus, we need to add 0.5 to the x, y, and z component, which is done by this expression: gl_Vertex + vec4(0.5, 0.5, 0.5, 0.0).

The main problem, however, is: how do we get any value from the vertex shader to the fragment shader? It turns out that the only way to do this is to use varying variables (or varyings for short). Output of the vertex shader can be written to a varying variable and then it can be read as input by the fragment shader. This is exactly what we need.

To specify a varying variable, it has to be defined with the modifier varying (before the type) in the vertex and the fragment shader outside of any function; in our example: varying vec4 position;. And here comes the most important rule about varying variables:

This is required to avoid ambiguous cases where the GLSL compiler cannot figure out which varying variable of the vertex shader should be matched to which varying variable of the fragment shader.

A Neat Trick for Varying Variables in Unity

The requirement that the definitions of varying variables in the vertex and fragment shader match each other often results in errors, for example if a programmer changes a type or name of a varying variable in the vertex shader but forgets to change it in the fragment shader. Fortunately, there is a nice trick in Unity that avoids the problem. Consider the following shader:

Shader "GLSL shader for RGB cube" {
Pass {
GLSLPROGRAM // here begin the vertex and the fragment shader

varying vec4 position;
// this line is part of the vertex and the fragment shader

#ifdef VERTEX
// here begins the part that is only in the vertex shader

void main()
{
position = gl_Vertex + vec4(0.5, 0.5, 0.5, 0.0);
gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
}

#endif
// here ends the part that is only in the vertex shader

#ifdef FRAGMENT
// here begins the part that is only in the fragment shader

void main()
{
gl_FragColor = position;
}

#endif
// here ends the part that is only in the fragment shader

ENDGLSL // here end the vertex and the fragment shader
}
}
}

As the comments in this shader explain, the line #ifdef VERTEX doesn't actually mark the beginning of the vertex shader but the beginning of a part that is only in the vertex shader. Analogously, #ifdef FRAGMENT marks the beginning of a part that is only in the fragment shader. In fact, both shaders begin with the line GLSLPROGRAM. Therefore, any code between GLSLPROGRAM and the first #ifdef line will be shared by the vertex and the fragment shader. (If you are familiar with the C or C++ preprocessor, you might have guessed this already.)

This is perfect for definitions of varying variables because it means that we may type the definition only once and it will be put into the vertex and the fragment shader; thus, matching definitions are guaranteed! I.e. we have to type less and there is no way to produce compiler errors because of mismatches between the definitions of varying variables. (Of course, the cost is that we have to type all these #ifdef and #end lines.)

Variations of this Shader

The RGB cube represents the set of available colors (i.e. the gamut of the display). Thus, it can also be used show the effect of a color transformation. For example, a color to gray transformation would compute either the mean of the red, green, and blue color components, i.e. $({\text{red}}+{\text{green}}+{\text{blue}})/3$ , and then put this value in all three color components of the fragment color to obtain a gray value of the same intensity. Instead of the mean, the relative luminance could also be used, which is $0.21{\text{ red}}+0.72{\text{ green}}+0.07{\text{ blue}}$ . Of course, any other color transformation (changing saturation, contrast, hue, etc.) is also applicable.

Another variation of this shader could compute a CMY (cyan, magenta, yellow) cube: for position $(x,y,z)$ you could subtract from a pure white an amount of red that is proportional to $x$ in order to produce cyan. Furthermore, you would subtract an amount of green in proportion to the $y$ component to produce magenta and also an amount of blue in proportion to $z$ to produce yellow.

If you really want to get fancy, you could compute an HSV (hue, saturation, value) cylinder. For $x$ and $z$ coordinates between -0.5 and +0.5, you can get an angle $H$ between 0 and 360° with 180.0+degrees(atan(z, x)) in GLSL and a distance $S$ between 0 and 1 from the $y$ axis with 2.0 * sqrt(x * x + z * z). The $y$ coordinate for Unity's built-in cylinder is between -1 and 1 which can be translated to a value $V$ between 0 and 1 by $(y+1.0)/2.0$ . The computation of RGB colors from HSV coordinates is described in the article on HSV in Wikipedia.

Interpolation of Varying Variables

The story about varying variables is not quite over yet. If you select the cube game object, you will see in the Scene View that it consists of only 12 triangles and 8 vertices. Thus, the vertex shader might be called only eight times and only eight different outputs are written to the varying variable. However, there are many more colors on the cube. How did that happen?

The answer is implied by the name varying variables. They are called this way because they vary across a triangle. In fact, the vertex shader is only called for each vertex of each triangle. If the vertex shader writes different values to a varying variable for different vertices, the values are interpolated across the triangle. The fragment shader is then called for each pixel that is covered by the triangle and receives interpolated values of the varying variables. The details of this interpolation are described in Section “Rasterization”.

If you want to make sure that a fragment shader receives one exact, non-interpolated value by a vertex shader, you have to make sure that the vertex shader writes the same value to the varying variable for all vertices of a triangle.

Summary

And this is the end of this tutorial. Congratulations! Among other things, you have seen:

• What an RGB cube is.
• What varying variables are good for and how to define them.
• How to make sure that a varying variable has the same name and type in the vertex shader and the fragment shader.
• How the values written to a varying variable by the vertex shader are interpolated across a triangle before they are received by the fragment shader.