Godot Custom Shader Built-ins. Functions (part 2/3)

DivideByZero
4 min readMay 22, 2023

--

I assume that you already read my previous part about Custom Built-in Variables and ready to dive deeper in Godot’s Shader Language implementation.

I have both good and bad news for you. Let’s start with the bad news: unlike UE or Unity3D, Godot does not provide a built-in function that allows us to sample shadow information at specific coordinates. Currently, we can only have shadow information for the current fragment. However, here’s the good news: today, we will discuss how to implement custom functions in GSL (Godot Shader Language). As an example, we will create a function for sampling shadows. You can find a complete list of available built-in functions here.

You might be wondering why we even need this? The answer is quite simple: to make beautiful and stylized SHADOWS. To accomplish this, we need to be able to read shadow values with a some offset (typically achieved through noise) to the current fragment position.

Custom Built-in Function

Now, let’s create a function to sample shadows from a directional light. While it will not be very complicated to create a function for positional lights (omni and spot), for the sake of simplicity in this tutorial, we will focus on the directional light only.

Let’s define our function as follows: float sample_directional_shadow(uint light_index, vec3 vertex). This function will take two arguments: the index of the light currently being processed (which will be explained later) and the coordinates of the specific fragment we want to sample for shadows. The function will return a value between 0 and 1, where 1.0 represents no shadow and 0.0 represents full shadow.

Firstly we will need to define our new function. All built-in functions are defined in shader_language.cpp

const ShaderLanguage::BuiltinFuncDef ShaderLanguage::builtin_func_defs[] = {

...

{ "frexp", TYPE_VEC4, { TYPE_VEC4, TYPE_IVEC4, TYPE_VOID }, { "x", "exp" }, TAG_GLOBAL, true },

// add define of our function at the end of constructor
{ "sample_directional_shadow", TYPE_FLOAT, { TYPE_UINT, TYPE_VEC3, TYPE_VOID }, { "light_idx", "vertex" }, TAG_GLOBAL, true },


{ nullptr, TYPE_VOID, { TYPE_VOID }, { "" }, TAG_GLOBAL, false }

}

In order to define our function, we need to follow this format:

  • Specify the name of the function;
  • Data type that the function will return;
  • Data type of the function arguments. The last argument in the list should always be the extra pseudo TYPE_VOID, which is used as a break flag in engine loop;
  • Names for the function arguments;
  • Specify the parameter TAG_GLOBAL or TAG_ARRAY. In our case, we will use TAG_GLOBAL;
  • Specify the HIGH_END flag (described here), for our case we will use “true”, but also “false” can be used.

If your function contains arguments that are used as “out” in built-ins, you will need to define them here. The format for this will be: { "name of your function", { number of "out" arguments; index of the first one, starting with 0 } }. Similar for const arguments.

So now we can move on to write our custom function. The calculation for shadows in directional light is defined in scene_forward_clustered.glsl (L1554-L1839).

Spot, omni, and directional lights are stored in separate arrays. During the rendering of the scene, to calculate the light and shadows for each pixel, all of these lights are called in a loop. First, all directional lights (up to 8) are processed, followed by omni and spot lights. To optimize this process and avoid calculating light for pixels that are out of light range, Godot utilizes a clustering optimization technique.

This means that for each fragment/pixel, a special mask is used to filter lights that should be used for shading. This optimization only applies to positional lights, as directional lights are calculated for each fragment/pixel.

Since we plan to call our custom function from the light() shader function, we need to determine the array index of the current light. To achieve this, we will create a new custom variable called LIGHT_INDEX and store the current light's index there. I won't go into detail on how to create this variable, as we have already covered it in a previous post. I will only mention that for omni and spot lights, we need to pass the unit light_index, for directional lights we will use the current loop index i.

Just like all other light functions defined in scene_forward_lights_inc.glsl, we will include the definition of our custom function there as well:

float sample_directional_shadow(uint idx, vec3 vertex) {

uint shadow0 = 0;
uint shadow1 = 0;

float shadow = 1.0; // no shadow

light_process_directional_shadow(idx, vertex, scene_data_block.data.directional_shadow_pixel_size, shadow0, shadow1);

if (idx < 4) {
shadow = float(shadow0 >> (idx * 8u) & 0xFFu) / 255.0;
} else {
shadow = float(shadow1 >> ((idx - 4u) * 8u) & 0xFFu) / 255.0;
}

return shadow;

}

The extra function light_process_directional_shadow() will contain the code for calculating shadows extracted from scene_forward_clustered.glsl (L1558-L1765)

void light_process_directional_shadow(uint idx, vec3 vertex, highp vec2 directional_shadow_pixel_size, inout uint shadow0, inout uint shadow1)

And that’s it! After compiling the engine, we will have the full power of the Godot engine at our disposal :)

It’s time for magic— shader time.

The main concept of this shader is straightforward and derived from @chrisloop:

  • Use a triplanar mapping to sample noise texture.
  • In the fragment function, we offset the current vertex (fragment position) by blending it with a noise value.
  • Sample the shadow value at this modified vertex and store it for our case in diffuse_light.
  • To have 2 layers of shadows, get 2 vertex mixed with different noise (with offset or different scale) and mix it together.
  • To have two layers of shadows, we store two vertex blended with different noise values (with different offsets or varying scales), and combine them together.

Full shader code you can find here.

--

--

Responses (2)