Skip to content Skip to sidebar Skip to footer

Widget HTML #1

Godot 4 Shaders: Write 2D shaders for your game from scratch


Godot 4 Shaders: Write 2D shaders for your game from scratch

Learn to create visual effects (VFX) with 2D shaders via GDShader language of Godot 4. PART 1: Beginner to intermediate.

Enroll Now

The Godot Engine, renowned for its flexibility and open-source nature, continues to evolve, and Godot 4 brings a wealth of new features and improvements. Among these are enhancements to the shader language and shader capabilities, making it an exciting time for developers looking to push the visual boundaries of their games. In this guide, we'll delve into writing 2D shaders in Godot 4 from scratch, exploring the basics and then moving on to more advanced techniques.

Understanding Shaders

Shaders are small programs that run on the GPU, allowing for real-time image processing and rendering effects. In Godot, shaders are written in a language similar to GLSL (OpenGL Shading Language) but tailored to the Godot environment. Shaders can be used for various effects, such as changing colors, creating patterns, or generating complex visual effects like water reflections, lighting, and more.

There are three primary types of shaders in Godot:

  1. CanvasItem Shader: Used for 2D rendering.
  2. Spatial Shader: Used for 3D rendering.
  3. Particle Shader: Used for customizing particle systems.

In this guide, we'll focus on CanvasItem shaders, which are used for 2D elements.

Getting Started with a Basic Shader

To start, let's create a simple shader that changes the color of a sprite. This will help us understand the basic structure and syntax of Godot shaders.

  1. Create a New Shader: In your Godot project, create a new ShaderMaterial and attach it to a 2D node, such as a Sprite.

  2. Open the Shader Editor: Click on the ShaderMaterial to open the shader editor.

  3. Write the Shader Code:

glsl
shader_type canvas_item; void fragment() { // Set the color of the pixel to red COLOR = vec4(1.0, 0.0, 0.0, 1.0); }

This simple shader sets every pixel of the sprite to red. Let's break down the code:

  • shader_type canvas_item; tells Godot that this is a 2D shader.
  • The fragment() function runs for each pixel (or fragment) and determines its final color. The COLOR variable is a built-in that represents the pixel's color.

Adding Uniforms

Uniforms are variables that can be passed from your game to the shader, allowing you to control shader properties from the editor or code. Let's modify our shader to change the color dynamically.

glsl
shader_type canvas_item; uniform vec4 modulate_color : hint_color; void fragment() { COLOR = modulate_color; }

Here, modulate_color is a uniform variable of type vec4 (a vector with four components, representing RGBA). The hint_color tells Godot to show a color picker in the editor for this uniform.

Adding Textures

Textures are essential in shaders for adding details and patterns. Let's create a shader that uses a texture and applies a grayscale effect.

  1. Attach a Texture: Ensure your ShaderMaterial has a texture applied in the Godot editor.

  2. Modify the Shader Code:

glsl
shader_type canvas_item; uniform sampler2D texture : hint_albedo; void fragment() { vec4 tex_color = texture(texture, FRAGCOORD.xy / SCREEN_PIXEL_SIZE); float gray = dot(tex_color.rgb, vec3(0.299, 0.587, 0.114)); COLOR = vec4(vec3(gray), tex_color.a); }
  • sampler2D texture is a uniform for the texture.
  • texture(texture, FRAGCOORD.xy / SCREEN_PIXEL_SIZE) samples the texture at the current pixel position.
  • dot(tex_color.rgb, vec3(0.299, 0.587, 0.114)) calculates the grayscale value using a standard formula.
  • COLOR = vec4(vec3(gray), tex_color.a) sets the final color, preserving the alpha value.

Creating Patterns and Animations

Shaders can also be used to create dynamic patterns and animations. Let's create a simple shader that generates a moving wave pattern.

glsl
shader_type canvas_item; uniform float time; uniform vec2 wave_frequency = vec2(10.0, 10.0); uniform float wave_amplitude = 0.1; void fragment() { vec2 uv = FRAGCOORD.xy / SCREEN_PIXEL_SIZE; float wave = sin(uv.x * wave_frequency.x + time) * wave_amplitude; wave += sin(uv.y * wave_frequency.y + time) * wave_amplitude; COLOR = vec4(vec3(0.5 + wave), 1.0); }
  • time is a uniform that you can update from your game code to animate the wave.
  • wave_frequency and wave_amplitude control the wave's frequency and amplitude.
  • The sin functions generate the wave pattern, and uv is the normalized screen coordinates.

In your game code, update the time uniform to animate the wave:

gd
func _process(delta): $Sprite.material.set_shader_param("time", OS.get_time())

Advanced Techniques

Normal Mapping

Normal mapping can add depth to 2D surfaces by simulating lighting effects. To implement normal mapping, you'll need a normal map texture.

glsl
shader_type canvas_item; uniform sampler2D texture : hint_albedo; uniform sampler2D normal_map : hint_normal; uniform vec2 light_dir = vec2(1.0, -1.0); void fragment() { vec2 uv = FRAGCOORD.xy / SCREEN_PIXEL_SIZE; vec4 tex_color = texture(texture, uv); vec3 normal = texture(normal_map, uv).rgb * 2.0 - 1.0; float light = max(dot(normalize(normal.xy), normalize(light_dir)), 0.0); COLOR = vec4(tex_color.rgb * light, tex_color.a); }

Procedural Textures

Procedural textures are generated by the shader itself. Here’s a shader that generates a simple checkerboard pattern.

glsl
shader_type canvas_item; uniform vec2 resolution; uniform vec2 checker_size = vec2(10.0, 10.0); void fragment() { vec2 uv = FRAGCOORD.xy / resolution * checker_size; float checker = mod(floor(uv.x) + floor(uv.y), 2.0); COLOR = vec4(vec3(checker), 1.0); }
  • resolution is the size of the screen or texture.
  • checker_size controls the size of the checker squares.

Post-Processing Effects

Shaders can also be used for post-processing effects, such as blurring or edge detection. Here’s a simple blur shader.

glsl
shader_type canvas_item; uniform sampler2D texture : hint_albedo; uniform float blur_size = 1.0 / 512.0; void fragment() { vec2 uv = FRAGCOORD.xy / SCREEN_PIXEL_SIZE; vec4 color = vec4(0.0); for (int x = -2; x <= 2; x++) { for (int y = -2; y <= 2; y++) { color += texture(texture, uv + vec2(x, y) * blur_size); } } COLOR = color / 25.0; }
  • blur_size controls the blur intensity.
  • The nested loops sample the surrounding pixels and average their colors.

Conclusion

Writing 2D shaders in Godot 4 opens up a world of creative possibilities for your game. By understanding the basics and gradually incorporating more advanced techniques, you can create stunning visual effects that enhance your game's aesthetic and immerse players in your world. Whether you're modulating colors, generating procedural patterns, or implementing complex lighting effects, the power of shaders in Godot 4 is at your fingertips. Happy coding!