Introduction: Why Learn Shaders
When developing games in Godot Engine, shaders are powerful tools you can't avoid. Shaders are small programs executed on the Graphics Processing Unit (GPU), controlling object appearance, colors, light reflections, and screen-wide effects at the pixel level.
Why are shaders important? Because they can dramatically enhance visual appeal while achieving high performance. By maximizing GPU parallel processing capability instead of performing complex calculations on the CPU, you can efficiently achieve effects difficult to realize with GDScript alone—fire, water, custom lighting, unique screen transitions, and more.
This article focuses on Fragment Shaders, which handle visual expression, explaining fundamental knowledge and the first steps to creating color changes and effects with concrete code examples.
Basic Structure of Godot Shaders
Godot Engine shaders use a proprietary shading language similar to GLSL ES 3.0. When creating a shader file (.gdshader), you first need to define what the shader applies to.
shader_type canvas_item; // Apply to 2D objects
// shader_type spatial; // Apply to 3D objects
canvas_item applies to 2D sprites and UI elements, while spatial applies to 3D meshes.
Godot shaders consist primarily of three functions (entry points):
| Function | Role | Execution Timing |
|---|---|---|
vertex | Manipulates object vertices (positions). Used for deformation, waves, rotation, etc. | Once per vertex |
fragment | Calculates pixel (fragment) colors. Core for texture sampling, color adjustment, effects. | Once per pixel |
light | Calculates light influence on objects. Used for custom lighting. | Once per light source and pixel combination |
The fragment function—the star of this article—executes for every pixel on screen, responsible for determining each pixel's final color.
Practice 1: Basic Color Operations with Fragment Shader
Fragment Shader's most basic role is determining pixel color. Here we learn basic color manipulation using the COLOR output variable and texture() function.
Code Example 1: Solid Color Fill
The simplest shader that completely fills an object with a specific color.
shader_type canvas_item;
void fragment() {
// Directly assign a new color to COLOR variable in vec4(R, G, B, A) format.
// Each component ranges from 0.0 to 1.0.
COLOR = vec4(0.8, 0.2, 0.3, 1.0); // Wine red
}
Code Example 2: Texture Color Inversion
Let's process while utilizing the original texture's colors. Get the current pixel's color with texture() and invert it.
shader_type canvas_item;
void fragment() {
// 1. Get texture color at current UV coordinates.
vec4 original_color = texture(TEXTURE, UV);
// 2. Invert colors by subtracting RGB components from 1.0.
vec3 inverted_rgb = vec3(1.0) - original_color.rgb;
// 3. Set inverted RGB with original alpha as final output color.
COLOR = vec4(inverted_rgb, original_color.a);
}
Code Example 3: Sepia Filter
As a more practical example, let's apply a classic sepia filter to an image.
shader_type canvas_item;
void fragment() {
vec4 original_color = texture(TEXTURE, UV);
vec3 c = original_color.rgb;
// Convert to grayscale (luminance calculation)
float gray = dot(c, vec3(0.299, 0.587, 0.114));
// Apply sepia color tone
vec3 sepia_color = vec3(
gray * 1.07, // Boost red
gray * 0.74, // Reduce green
gray * 0.43 // Reduce blue further
);
COLOR = vec4(sepia_color, original_color.a);
}
Practice 2: Dynamic Effects Using UV Coordinates and Time
Fragment Shader's true value lies in using built-in variables like UV (texture coordinates) and TIME to transform static images into dynamic effects.
Code Example 4: Circular Mask Using UV Coordinates
Process UV coordinates to create a spotlight-like circular mask.
shader_type canvas_item;
void fragment() {
// 1. Adjust UV coordinates to center at (0,0).
vec2 centered_uv = UV - vec2(0.5);
// 2. Calculate distance from center.
float dist = length(centered_uv);
// 3. Use smoothstep for smooth boundary rendering.
float mask = 1.0 - smoothstep(0.3, 0.4, dist);
vec4 original_color = texture(TEXTURE, UV);
// 4. Apply mask by multiplying with original alpha.
COLOR = vec4(original_color.rgb, original_color.a * mask);
}
Code Example 5: Scroll Animation Using Time
Adding TIME variable to UV coordinates automatically scrolls the texture. Essential for flowing water surfaces and clouds.
shader_type canvas_item;
uniform float scroll_speed = 0.1;
void fragment() {
// 1. Add time to UV's x component to shift UVs.
vec2 scrolled_uv = UV + vec2(TIME * scroll_speed, 0.0);
// 2. Use fract() to loop UV coordinates in 0.0-1.0 range.
scrolled_uv = fract(scrolled_uv);
// 3. Sample texture with new UV coordinates.
COLOR = texture(TEXTURE, scrolled_uv);
}
Using fract() makes the texture scroll infinitely.
Code Example 6: Dissolve Effect Using Noise
Using noise textures enables more organic, complex effects.
shader_type canvas_item;
// Noise texture set externally
uniform sampler2D noise_texture;
// Control dissolve progress from GDScript
uniform float dissolve_threshold : hint_range(0.0, 1.0) = 0.5;
void fragment() {
// Get value from noise texture
float noise_value = texture(noise_texture, UV).r;
// If noise value is below threshold, discard pixel (make transparent)
if (noise_value < dissolve_threshold) {
discard; // discard prevents pixel from being drawn
}
COLOR = texture(TEXTURE, UV);
}
Varying dissolve_threshold from 0.0 to 1.0 via GDScript achieves smooth dissolve animation.
Performance and Optimization
Shaders are powerful, but careless implementation can become performance bottlenecks. Since Fragment Shaders execute per pixel, slight inefficiencies lead to significant load.
- Avoid
ifstatements: Due to GPU parallel processing nature, branching withifis costly. Replace logic with branch-free functions likestep(),smoothstep(),mix()where possible. - Heavy calculations in Vertex Shader: Calculations that don't change per pixel (e.g., sin/cos using
TIME) should be done in Vertex Shader, passing results to Fragment Shader viavaryingvariables to significantly reduce computation. - Reduce texture sampling:
texture()function calls are relatively heavy. When sampling the same texture multiple times, save results to variables for reuse.
Common Mistakes and Best Practices
Here's a summary of common pitfalls when working with Fragment Shaders and best practices to avoid them.
| Common Mistake | Best Practice |
|---|---|
Heavy use of if statements | Use step(), smoothstep(), mix() functions to replace conditional branching with arithmetic operations. |
| Hardcoding UV coordinates | UV isn't always in vec2(0.0, 1.0) range. Develop the habit of normalizing UVs with fract() or mod(). |
Excessive use of discard | While convenient, discard can inhibit depth test optimization on some GPUs. Consider setting alpha to 0 as an alternative. |
| Over-sampling textures | Sampling the same texture multiple times with texture() increases load. Save results to variables for reuse. |
| Doing in shaders what GDScript can do | Shaders specialize in per-pixel operations. Simple color changes to entire objects may be simpler and faster using Sprite2D's modulate property. |
Summary: Next Steps with Fragment Shaders
This article covered the basic role, structure, and methods for achieving color changes and dynamic effects with Fragment Shaders in Godot Engine.
Fragment Shaders are powerful tools for calculating per-pixel colors, and by leveraging built-in variables like UV coordinates and TIME, you can create infinite visual effects.
Key Points:
- Shaders execute on the GPU, controlling visual expression with high performance.
- The
fragmentfunction determines per-pixel color, outputting to theCOLORvariable. UVcoordinates indicate position on textures, essential for gradients and coordinate-based effects.- Using the
TIMEuniform enables efficient time-based animations.
After mastering these fundamentals, next learn about Uniforms for passing external values, controlling shader parameters from GDScript, and try object deformation using vertex shaders.