Get an 20% OFF using the RELEASE code on your book purchase. For a limited time.

Introduction to the shader programming language

4.0.4. Structure of a function in HLSL

This post is also available in…

As in C#, in HLSL we can declare empty functions (void) or functions that return a value (return). 

We have to use “declarations” that depend on the function type; these determine if a value corresponds to an input (in), output (out), global variable (uniform) or a constant value (const). 

We will start by analyzing an empty function using the following syntax:

void functionName_precision (declaration type arg)
{
    float value = 0;
    arg = value;
}

To declare an empty function, we start with the void nomenclature, then the name of the function accompanied by precision and final arguments. As we can see in the previous example, inside the function field we write the algorithm or operation to be performed. Generally, in the arguments, we must define whether these will be inputs or outputs. 

How can we know if they have a declaration? Everything will depend on the functions that we want to pass as arguments. To understand this concept, let’s suppose we want to create a function to calculate the illumination in an object, for this, one of the properties that we need are the normals, with this we can identify from which direction the light source will be illuminating our object. Therefore, the normals would be an “input to calculate” inside our empty function.

void FakeLight_float (in float3 Normal, out float3 Out)
{
    float[n] operation = Normal;
     Out = operation; 
}

This function does not fulfil any specific functionality, but we will use it to understand the concepts mentioned before it. 

The function is called “FakeLight” and “_float” corresponds to its precision, this can be of float or half type since, as we know, these are HLSL compatible formats. 

We must always add precision to an empty function, otherwise, it cannot be compiled within our program. Then in the arguments, we can see that the object’s normals (float3 Normal) have been declared as input through the “in” declaration, likewise, there is an output called “out” which will be the operation’s final value. 

To use this type of function within another, we must declare both inputs and outputs in our code before the function itself. 

Let’s simulate our FakeLight function inside the fragment shader stage as if it will actually operate:

// create our function
void FakeLight_float (in float3 Normal, out float3 Out)
{
    float[n] operation = Normal;
    Out = operation; 
}

half4 frag (v2f i) : SV_Target
{
    float3 n = i.normal;      // declare the normals. 
    float3 col = 0;           // declare the output. 
    FakeLight_float (n, col); // pass both values ​​as arguments.

    return float4(col.rgb, 1);
}

In the example above there are several situations that are occurring. First, the “FakeLight” function has been declared before the “frag” function because the GPU reads our code from top to bottom. Then in the fragment shader stage, we have created a three-dimensional vector called “n” and another three-dimensional vector called “col”. In this case, both are three-dimensional vector types, this is because we will use both vectors as arguments in the FakeLight_float function, which asks for three-dimensional input and output vectors. Then, the first argument corresponds to the normal input of the object and the second, to the result of the operation that is being carried out within the FakeLight_float function. 

The col vector has been started at “zero”, this means that it has “0” for the red, green and blue (RGB) color, which corresponds to black by default, however, since it has been declared as output, it is now taking place inside the FakeLight_float function. 

Finally, we return to a four-dimensional vector where the first three values ​​correspond to the col vector in RGB and “one” to the Alpha. 

Why are we returning a four-dimensional vector? This is because the function frag is a half4 type, that is, a four-dimensional vector. 

Now we will analyze the structure of a function that returns a value. In essence, they are very similar, with the difference that in this case, it will not be necessary to add the precision function. To illustrate this, we are going to use the same FakeLight function, but this time it will return a value.

// create our function 
half3 FakeLight (float3 Normal)
{
    float[n] operation = Normal;
    return operation; 
}

half4 frag (v2f i) : SV_Target
{
    // declare the normals. 
    float3 n = i.normal;        
    float3 col = FakeLight_float (n);    

    return float4(col.rgb, 1);
}

Unlike the empty function, we just add the Normal argument given that it does not require output, likewise, in the fragment shader stage we made the vector “col” equal to this function because it returns the same number of dimensions that this vector possesses.

Follow us to stay informed about all the latest news, updates, and more.

Join the group to share your experiences with other developers.

Subscribe to our channel and keep learning game dev!

jettelly-logo

Jettelly Team

We are a team of indie developers with more than 9 years of experience in video games. As an independent studio, we have developed Nom Noms in which we published with Hyperbeard in 2019. We are currently developing The Unity Shader Bible.

Follow us on our social networks.