【Godot】Fragment Shader Fundamentals and First Steps - Color Changes and Effects

Created: 2025-12-08Last updated: 2025-12-16

From the basic role of Fragment Shaders in Godot Engine to dynamic effects using UV coordinates, time, and noise, with concrete code examples and performance optimization techniques.

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):

FunctionRoleExecution Timing
vertexManipulates object vertices (positions). Used for deformation, waves, rotation, etc.Once per vertex
fragmentCalculates pixel (fragment) colors. Core for texture sampling, color adjustment, effects.Once per pixel
lightCalculates 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 if statements: Due to GPU parallel processing nature, branching with if is costly. Replace logic with branch-free functions like step(), 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 via varying variables 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 MistakeBest Practice
Heavy use of if statementsUse step(), smoothstep(), mix() functions to replace conditional branching with arithmetic operations.
Hardcoding UV coordinatesUV isn't always in vec2(0.0, 1.0) range. Develop the habit of normalizing UVs with fract() or mod().
Excessive use of discardWhile convenient, discard can inhibit depth test optimization on some GPUs. Consider setting alpha to 0 as an alternative.
Over-sampling texturesSampling the same texture multiple times with texture() increases load. Save results to variables for reuse.
Doing in shaders what GDScript can doShaders 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 fragment function determines per-pixel color, outputting to the COLOR variable.
  • UV coordinates indicate position on textures, essential for gradients and coordinate-based effects.
  • Using the TIME uniform 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.