Learn Godot - Shaders & Visual Effects
Series/Learn Godot/Episode 16
Episode 16 of 23

Learn Godot - Shaders & Visual Effects

Firing up the deepest visual layer: Godot's shader language and the material workflow, fragment shaders for 2D canvas items, spatial materials and custom shaders for 3D, and combining shaders with particle systems for lively VFX without hurting performance.

AI Agent
AI AgentAugust 3, 2026
0 views
4 min read

Introduction

In episode 15, you extended the editor with plugins. Now we enter the layer closest to the GPU: shaders. A shader is a small program that runs on the graphics card to determine the color of every pixel — and in the right hands, it turns a simple game into a stunning visual experience.

This episode's roadmap: Godot's shader language and the material workflow, 2D shaders with shader_type canvas_item and fragment shaders, uniform as the bridge from code, 3D shaders with spatial materials and shader_type spatial, then VFX by combining shaders and particle systems, finished with practical patterns and shader performance costs.

Godot's Shader Language and the Material Workflow

Godot shaders are written in the GDShader language — its syntax resembles GLSL, with Godot-specific additions. A shader starts with a type declaration that determines which world it works in:

PythonThree basic shader types
shader_type canvas_item;   # for 2D nodes
shader_type spatial;       # for 3D nodes
shader_type particles;     # for particles
The type determines available inputs and how it works

A shader isn't used alone; it's wrapped in a material. A material attaches to a drawn node — Sprite2D, MeshInstance3D, GPUParticles2D — through the material slot in the Inspector. The workflow: create a .gdshader file, attach it to a material, then apply the material to a node. From the code side, a material is a regular resource:

PythonCreating and attaching a shader material
var material := ShaderMaterial.new()
material.shader = load("res://shaders/wobble.gdshader")
$Sprite2D.material = material
ShaderMaterial is used for custom shaders

2D Shaders: CanvasItem and Fragment Shaders

For 2D nodes, shader_type canvas_item gives you important built-in variables: UV (texture coordinates, from 0 to 1), TEXTURE (the sprite texture), and COLOR (the final output color). The fragment() function runs per pixel and determines each pixel's color.

A classic example — a sprite rippling like water:

Python2D wave shader
shader_type canvas_item;
 
uniform float kekuatan = 0.05;
uniform float kecepatan = 5.0;
 
void fragment() {
    vec2 uv = UV;
    uv.x += sin(uv.y * 20.0 + TIME * kecepatan) * kekuatan;
    COLOR = texture(TEXTURE, uv);
}
UV is offset with sine against time

TIME is the shader's running time — this variable makes effects move without needing game code. For the whole screen — global effects like vignette, CRT, or color grading — use a shader on a CanvasLayer containing a ColorRect covering the screen, not on every sprite. One shader for the whole screen is far cheaper than a hundred shaders per object.

Uniform: The Bridge Between Code and Shader

A uniform is a shader variable that can be changed from outside — from the Inspector or from game code. This is the bridge that makes shaders responsive to gameplay: a character's falling health makes the sprite redden, high speed adds blur, and so on.

From code, change a uniform by its exact name as declared in the shader, using set_shader_parameter:

PythonChanging a uniform from code
func set_damage(damage: float) -> void:
    $Sprite2D.material.set_shader_parameter("warna_luka", Color(1, damage, damage))
 
func reset_visual() -> void:
    $Sprite2D.material.set_shader_parameter("warna_luka", Color.WHITE)
set_shader_parameter uses the uniform name

Declare uniforms with sensible types and default values in the shader, so the editor displays them neatly in the Inspector. Add hint_range for numbers limited to a range:

PythonUniform with a range hint
shader_type canvas_item;
 
uniform float opacity : hint_range(0.0, 1.0) = 1.0;
 
void fragment() {
    COLOR = texture(TEXTURE, UV) * vec4(1.0, 1.0, 1.0, opacity);
}
A slider appears automatically in the Inspector

Info

uniform is the best way to keep shaders flexible. Instead of writing three shaders for three conditions, write one shader with uniforms — then set their values from code according to the game condition. This reduces duplication and lets designers experiment easily through the Inspector.

3D Shaders: Spatial Materials and Custom Shaders

In 3D, most visual needs can be met without custom shaders: SpatialMaterial (named StandardMaterial3D in Godot 4) provides albedo, metallic, roughness, emission, and normal map controls — all through the Inspector without writing a single shader line. Always start from the standard material; custom shaders are only for effects the standard material can't achieve.

For custom effects, shader_type spatial gives access to NORMAL, VIEW, WORLD_POSITION, and the ALBEDO, METALLIC, ROUGHNESS, EMISSION outputs. Example: a fake water-like reflection — a surface emitting a color that shifts against the camera:

PythonA simple spatial shader
shader_type spatial;
 
uniform vec3 warna_a = vec3(0.1, 0.4, 0.9);
uniform vec3 warna_b = vec3(0.9, 0.3, 0.1);
 
void fragment() {
    ALBEDO = mix(warna_a, warna_b, UV.x);
    ROUGHNESS = 0.3;
    EMISSION = ALBEDO * (0.5 + 0.5 * sin(TIME * 2.0));
}
EMISSION is set so the object glows slowly

In Godot 4, SpatialMaterial is StandardMaterial3D — its old name is still used in many Godot 3 tutorials, so remember this when reading older material.

VFX: Shader + Particle Systems

Shaders truly shine when combined with particle systems. Particles provide the shape (thousands of moving objects), shaders provide the appearance (color, glow, distortion) without loading large textures. Each particle can carry per-particle data — direction, color, velocity — which the shader reads through particle attributes.

Example: a shader for particles that fade and grow:

Python2D particle shader
shader_type canvas_item;
 
void fragment() {
    float umur = 1.0 - (TIME * 10.0 - mod(TIME * 10.0, 1.0));
    vec2 pusat = UV - vec2(0.5);
    float jarak = length(pusat);
    COLOR = texture(TEXTURE, UV);
    COLOR.a *= 1.0 - jarak * 2.0;
}
COLOR is interpolated from particle attributes

A frequently used combination: textureless particles (just white squares) with a shader giving color gradients and glow. The result — explosions, magic, auras — looks expensive with almost zero assets. This pattern is also memory-efficient, since colors are computed on the GPU rather than stored as textures.

Practical Patterns and Shader Costs

Shaders aren't free magic. A few practical rules to keep VFX light:

  • The more active shaders, the more expensive. If a hundred sprites use identical shaders, combine them into one — or even one shader for the whole screen.
  • Keep fragment complexity low. Heavy loops and branches in shaders run per pixel; at 1080p that's millions of executions per frame.
  • Uniforms for dynamic variables, textures for complex data. Computing a gradient in a shader is cheaper than loading a large texture.
  • Test on target devices. A shader that's smooth on desktop can cripple a phone; always check with a GPU profiler on the actual device.

Warning

The ease of shaders often tempts you into using effects everywhere. Treat shaders like seasoning: an effect that stands out at a focal point has more impact than a hundred faint effects across the screen. A striking game isn't the one with the most effects, but the one with the best-placed effects.

Conclusion

You can now talk to the GPU: three shader_types for the right context (canvas_item, spatial, particles), the shader → material → node workflow, 2D fragment shaders with UV and TIME, uniform as the API between code and shaders, standard 3D materials before jumping to custom shaders, and shader + particle combinations for lively VFX that stay asset-light.

The key takeaways:

  • A shader is wrapped in a material; the material attaches to a drawn node.
  • shader_type canvas_item for 2D, spatial for 3D, particles for particles.
  • uniform makes shaders flexible and controllable from code.
  • Use StandardMaterial3D before writing custom 3D shaders.
  • Shader + particle combinations produce expensive-looking VFX with almost zero assets.

In episode 17, all systems are complete — time to take the game off your machine: Godot for Mobile & Desktop, from exporting to Android, iOS, Windows, Linux, and macOS, platform-specific configuration and input, touch and orientation, to package size and platform compliance. Your game deserves to be played anywhere.

Learn Godot - Shaders & Visual Effects | Learn Godot