Cg Programming/Unity/RGB Cube
This tutorial discusses vertex output parameters and fragment input parameters. It is assumed that you are familiar with 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 has the color . For example, the point is mapped to the color , i.e. pure blue. (This is the blue corner in the lower right of the figure to the left.)
Preparations
editSince 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 > 3D Object > 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 Code
editHere 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 = UnityObjectToClipPos(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 > General > 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 Shaders
editThe 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 Qualifier
editAn 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 Shader
editThe 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. , 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 . 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 you could subtract from a pure white an amount of red that is proportional to in order to produce cyan. Furthermore, you would subtract an amount of green in proportion to the component to produce magenta and also an amount of blue in proportion to to produce yellow.
If you really want to get fancy, you could compute an HSV (hue, saturation, value) cylinder. For and coordinates between -0.5 and +0.5, you can get an angle between 0 and 360° with 180.0+degrees(atan2(z, x))
in Cg and a distance between 0 and 1 from the axis with 2.0 * sqrt(x * x + z * z)
. The coordinate for Unity's built-in cylinder is between -1 and 1 which can be translated to a value between 0 and 1 by . The computation of RGB colors from HSV coordinates is described in the article on HSV in Wikipedia.
Interpolation of Vertex Output Parameters
editThe 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 (so-called flat shading), you can either make sure that the vertex shader writes the same value to the vertex output parameters for all vertices of a triangle, or you can use the HLSL storage-class modifier nointerpolation
. In our example, you could use this struct:
struct vertexOutput {
float4 pos : SV_POSITION;
nointerpolation float4 col : TEXCOORD0;
};
Summary
editAnd 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 reading
editIf you want to know more
- about the data flow in and out of vertex and fragment shaders, you should read the description in Section “Programmable Graphics Pipeline”.
- about vector and matrix operations (e.g. the expression
vertexPos + float4(0.5, 0.5, 0.5, 0.0)
), you should read Section “Vector and Matrix Operations”. - about the interpolation of vertex output parameters, you should read Section “Rasterization”.
- about Unity's official documentation of writing vertex shaders and fragment shaders in Unity's ShaderLab, you should read Unity's ShaderLab reference about “Writing vertex and fragment shaders”.