Last modified on 17 December 2014, at 21:21

Cg Programming/Unity/RGB Cube

An RGB cube: the x, y, z coordinates are mapped to red, green, and blue color components.

This tutorial discusses vertex output parameters and fragment input parameters. 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.)

PreparationsEdit

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”.

The Shader CodeEdit

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

Shader "Cg shader for RGB cube" { 
   SubShader { 
      Pass { 
         CGPROGRAM 
 
         #pragma vertex vert // vert function is the vertex shader 
         #pragma fragment frag // frag function is the fragment shader
 
         // for multiple vertex output parameters an output structure 
         // is defined:
         struct vertexOutput {
            float4 pos : SV_POSITION;
            float4 col : TEXCOORD0;
         };
 
         vertexOutput vert(float4 vertexPos : POSITION) 
            // vertex shader 
         {
            vertexOutput output; // we don't need to type 'struct' here
 
            output.pos =  mul(UNITY_MATRIX_MVP, vertexPos);
            output.col = vertexPos + float4(0.5, 0.5, 0.5, 0.0);
               // Here the vertex shader writes output data
               // to the output structure. 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. 
            return output;
         }
 
         float4 frag(vertexOutput input) : COLOR // fragment shader
         {
            return input.col; 
               // Here the fragment shader returns the "col" input 
               // parameter with semantic TEXCOORD0 as nameless
               // output parameter with semantic COLOR.
         }
 
         ENDCG  
      }
   }
}

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.

Communication between Vertex and Fragment ShadersEdit

The main task of our shader is to set the fragment output color (i.e. the fragment output parameter with semantic COLOR) in the fragment shader to the vertex position that is available in the vertex shader. Actually, this is not quite true: the coordinates in the vertex input parameter with semantic POSITION 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: vertexPos + float4(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 pairs of vertex output parameters and fragment input parameters with the same semantics (TEXCOORD0 in this case). In fact, it is only the semantics that are used to determine which vertex output parameters correspond to which fragment input parameters. Instead of the semantic TEXCOORD0 we could also use another semantic, e.g. COLOR, it doesn't really matter here, except that parameters with the semantic COLOR are often clamped to values between 0 and 1 (which would be OK in this case). It is, however, common to use the semantics TEXCOORD0, TEXCOORD1, TEXCOORD2, etc. for all kinds of parameters.

The next problem is to specify multiple vertex output parameters. Since the return instruction can only return one value, it is common to define a structure for all the required vertex output parameters. Here, this structure is called vertexOutput:

         struct vertexOutput {
            float4 pos : SV_POSITION;
            float4 col : TEXCOORD0;
         };

By using this structure as an argument of the fragment shader function, we make sure that the semantics match. Note that in Cg (in contrast to C), we don't have to write struct vertexOutput when defining variables of this type but we can just use the name vertexOutput (without struct) for the same type.

The out QualifierEdit

An alternative to the use of an output structure would be to use arguments of the vertex shader function with the out qualifier, e.g.:

Shader "Cg shader for RGB cube" { 
   SubShader { 
      Pass { 
         CGPROGRAM 
 
         #pragma vertex vert // vert function is the vertex shader 
         #pragma fragment frag // frag function is the fragment shader
 
         void vert(float4 vertexPos : POSITION,
            out float4 pos : SV_POSITION,
            out float4 col : TEXCOORD0)  
         {
            pos =  mul(UNITY_MATRIX_MVP, vertexPos);
            col = vertexPos + float4(0.5, 0.5, 0.5, 0.0);
            return;
         }
 
         float4 frag(float4 pos : SV_POSITION, 
            float4 col : TEXCOORD0) : COLOR 
         {
            return col; 
         }
 
         ENDCG 
      }
   }
}

However, the use of an output structure is more common in practice and it makes sure that vertex output parameters and fragment input parameters have matching semantics.

Variations of this ShaderEdit

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 could compute 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 brightness. 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(atan2(z, x)) in Cg 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 Vertex Output ParametersEdit

The story about vertex output parameters and fragment input parameters 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 vertex output parameters. However, there are many more colors on the cube. How did that happen?

In fact, the vertex shader is only called for each vertex of each triangle. However, the different values of the vertex output parameters for the different vertices 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 vertex output parameters as fragment input parameters. 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 vertex output parameters for all vertices of a triangle.

SummaryEdit

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

  • What an RGB cube is.
  • What an output structure is and how to define it.
  • How output structures are used to make sure that vertex output parameters have the same semantics as fragment input parameters.
  • How the values written to vertex output parameters are interpolated across a triangle before they are received as input parameters by the fragment shader.

Further ReadingEdit

If you want to know more


< Cg Programming/Unity

Unless stated otherwise, all example source code on this page is granted to the public domain.