Debug shaders with preview breakpoints

2026/09/23

Type
Learning Resource
Format
Study Guide
Version
Godot 4.x
Subject Tags
    Code
    MIT
    Game Assets
    2016-2026, GDQuest© - All rights reserved
    All else
    2016-2026, GDQuest© - All rights reserved
    Created
    2026/09/23
    Updated
    2026/09/23

    Debug shaders with preview breakpoints

    For years, to debug shader code, you had to rely almost entirely on the final output. To inspect a variable in the middle of a shader, you would comment out everything after it and assign the value to the output COLOR built-in.

    For example, let's take this shader that draws a blue circle:

    void fragment() {
    	float distance_to_center = distance(UV, vec2(0.5));
    	float circle_mask = smoothstep(0.41, 0.4, distance_to_center);
    
    	COLOR.rgb = vec3(0.1, 0.4, 0.8);
    	COLOR.a = circle_mask;
    }
    A shader drawing a blue circle

    To inspect the distance to center, I need to insert a line of code and comment out other lines of code:

    void fragment() {
    	float distance_to_center = distance(UV, vec2(0.5));
    	//float circle_mask = smoothstep(0.405, 0.4, distance_to_center);
    
    	COLOR.rgb = vec3(distance_to_center);
    	
    	//COLOR.rgb = vec3(0.1, 0.4, 0.8);
    	//COLOR.a = circle_mask;
    }
    Previewing the distance to center

    It works, and it is a technique we still use, but it's cumbersome as you have to undo your changes to keep coding your shader.

    Thankfully, Godot 4.7 introduced shader preview breakpoints, which turn all that hassle into a single click.

    In this guide, you will learn how to:

    We will also learn the previewer's limitations.

    Prerequisites

    In this study guide, I assume that you know how to set up and code a simple fragment shader.

    If you're new to shaders, you can get started with this free course: Your First Shader from ZERO in Godot 4

    Shader preview breakpoints started as an add-on I made to make shader programming easier, with the help of the community. It was then ported to the engine and improved by Chaosus and vaner-org. Thanks, guys! That was a great collaboration.

    If you use Godot to code shaders, this feature will save you a lot of repetitive steps!

    OldDew

    Teacher at GDQuest

    Get the shader to follow along

    I will use the portal shader you learn to create in our free intro to shaders course. This is what it looks like:

    Portal Shader

    To try the shader preview breakpoints with me, copy the shader code below and apply it to a Sprite2D node in any Godot project:

    Code Reference: res://portal.gdshader

    This is the complete shader code after adding gradients to the rim and the center shape.

    shader_type canvas_item;
    
    uniform float radius: hint_range(0.0, 0.5) = 0.4;
    uniform float rim_width: hint_range(0.0, 0.3) = 0.05;
    
    uniform float softness: hint_range(0.0, 0.2) = 0.01;
    
    uniform float inner_gradient_start: hint_range(0.0, 0.5) = 0.1;
    uniform float inner_gradient_width: hint_range(0.0, 0.5) = 0.2;
    
    uniform vec3 inner_start_color: source_color = vec3(0.0, 0.0, 0.0);
    uniform vec3 inner_end_color: source_color = vec3(0.15, 0.21, 0.53);
    
    uniform vec3 rim_start_color: source_color = vec3(0.38, 0.63, 1.0);
    uniform vec3 rim_end_color: source_color = vec3(0.69, 0.88, 0.99);
    
    uniform float rim_gradient_width: hint_range(0.0, 0.5) = 0.1;
    
    uniform float breathing_strength: hint_range(0.0, 0.1) = 0.03;
    uniform float displacement_strength : hint_range(0.0, 0.2) = 0.05;
    
    uniform vec2 noise_direction = vec2(0.8, 1.0);
    uniform float noise_speed : hint_range(0.0, 1.0) = 0.1;
    
    void fragment() {
    	vec2 center = vec2(0.5, 0.5);
    
    	vec2 sample_uv = UV + TIME * noise_direction * noise_speed;
    	float noise_displacement = texture(TEXTURE, sample_uv).r;
    	
    	float oscillation = sin(TIME);
    
    	float distance_from_center = 
    		distance(UV, center) + 
    		noise_displacement * displacement_strength +
    		oscillation * breathing_strength;
    	
    	float mask_outer = smoothstep(
    		radius, 
    		radius - softness, 
    		distance_from_center
    	);
    
    	float mask_inner = smoothstep(
    		radius - rim_width, 
    		radius - rim_width - softness, 
    		distance_from_center
    	);
    
    	float mask_rim = mask_outer - mask_inner;
    
    	float inner_gradient_factor = smoothstep(
    		inner_gradient_start,
    		inner_gradient_start + inner_gradient_width,
    		distance_from_center
    	);
    
    	vec3 inner_gradient = mix(
    		inner_start_color,
    		inner_end_color,
    		inner_gradient_factor
    	);
    	
    	float rim_gradient_start = radius - rim_width;
    
    	float rim_gradient_factor = smoothstep(
    		rim_gradient_start,
    		rim_gradient_start + rim_gradient_width,
    		distance_from_center
    	);
    
    	vec3 rim_gradient = mix(
    		rim_start_color,
    		rim_end_color,
    		rim_gradient_factor
    	);
    
    	vec3 final_color =
    		mask_rim * rim_gradient +
    		mask_inner * inner_gradient;
    
    	COLOR = vec4(final_color, mask_outer);
    }

    How to use shader breakpoints

    To insert a shader preview breakpoint, move your mouse cursor to the Shader Editor's left gutter, next to a fragment shader line you want to preview. An eye icon will appear.

    Click the icon to preview what the fragment shader produces up to that point in your code.

    The image next to each preview breakpoint shows the result of applying the variable on that line to the COLOR built-in.

    How does this work under the hood?

    Since the COLOR built-in has the vec4 type, smaller variable types are placed from left to right inside a vec4 before being assigned.

    If you preview a variable called my_var, it will show different values based on its type:

    • If it's a float, COLOR will be vec4(my_var, 0.0, 0.0, 1.0)
    • If it's a vec2, COLOR will be vec4(my_var, 0.0, 1.0)
    • If it's a vec3, COLOR will be vec4(my_var, 1.0)
    • If it's a vec4, COLOR will be my_var

    This is why, in the video above, when we preview the center variable, we get a yellow color:

    vec2 center = vec2(0.5, 0.5);
    Snippet of shader code with a preview breakpoint next to the center variable assignment
    Previewing the center returns yellow

    Behind the scenes, the editor runs a shader that looks like this:

    void fragment() {
    	vec2 center = vec2(0.5, 0.5);
    	COLOR = vec4(center, 0.0, 1.0);
    }

    Using multiple breakpoints

    You can use as many preview breakpoints as you like. This makes it easy to inspect effects that need many steps to build. For example, the shader creates a mask for the portal's rim by subtracting two circles of different sizes. This is done in three steps:

    1. Create the outer circle mask
    2. Create the inner circle mask
    3. Subtract the two masks to get a ring

    Create a breakpoint next to each step to reveal the full sequence leading to the ring mask. This makes it much easier to see where an unexpected value comes from.

    Preview breakpoints can be computationally expensive!

    Godot runs a new shader for each breakpoint. Too many breakpoints might slow down your editor, especially in the case of 3D shaders.

    You can use as many breakpoints as you like, but you should be mindful of how many are open and close the ones you no longer need.

    Previewing shader changes

    Each preview window updates automatically as you change the shader code. Try changing the value of the center variable and see how it affects the shader result and preview breakpoints:

    Why does changing the center affect the mask previews?

    You might expect a breakpoint to only show the value of the variable at that line. That's not the case!

    Think about how breakpoints work when you debug GDScript code: All the code that comes before the breakpoint runs. When the breakpoint is reached, the execution stops.

    It's similar with shaders. When you preview the line defining the mask_outer variable, the preview window is rendered using the shader code up until that point.

    This is why changing center also changes the mask_outer preview. The preview still has to calculate everything that mask_outer depends on, including distance_from_center, which depends on center.

    Cleaning up preview breakpoints

    There are two ways to remove a preview breakpoint:

    1. Click the eye icon in the editor's gutter.
    2. Hover the preview image. This reveals the Close button and the breakpoint's line number. Click the Close button to remove the breakpoint.

    To navigate to the line of code that the breakpoint is on, click the line number that appears when you hover over the preview image.

    Previewing uniform changes

    Uniforms let you edit an effect's properties directly from the Inspector dock, and preview breakpoints can reflect those changes. However, unlike shader code edits, in Godot 4.7, Inspector updates do not automatically refresh preview breakpoints. For now, you need to refresh manually.

    In the Inspector, navigate to ShaderMaterialShader Parameters and change the Radius property. Look at the bottom-left of the shader editor and click the Update button to synchronize the breakpoint previews with the Inspector changes.

    Why don't uniform changes apply automatically?

    When the feature was added to Godot, there was no way to detect changes to shader parameters through the Inspector and the solution to still get the feature in Godot 4.7 was to update the previews manually in that case.

    Automatic updates when changing shader parameters in the Inspector might come in a future version of Godot. For example, this PR aims to add this feature: Make text shader preview automatically synchronize its parameters with active ShaderMaterial

    Breakpoint limitations and workarounds

    The preview breakpoints do not work in every situation. They are limited in these cases:

    Breakpoints only work inside fragment shaders

    If you try to add breakpoints outside of the fragment shader, instead of a preview, Godot will show an error message. It cannot preview because there is no data to calculate the output of the function:

    Workaround: Call your function in the fragment shader and store the output in a variable.

    In this example, I'm calling the circle() function five times and adding five breakpoints to preview each circle:

    Breakpoints do not see the TEXTURE built-in

    You use the TEXTURE built-in to sample the main texture of a 2D node such as a Sprite2D.

    Breakpoints do not support this built-in. For example, adding a breakpoint to the sampled_texture variable below shows a white square instead of the actual texture:

    void fragment() {
    	vec4 sampled_texture = texture(TEXTURE, UV);
    }

    Let's look at how this plays out on the portal shader. It uses the TEXTURE built-in to sample a noise texture meant to distort the portal.

    Add breakpoints to the line in which the noise is sampled and the last line that sets the COLOR built-in.

    We get a white square instead of the actual texture!

    Workaround: Create a new sampler2D uniform and use that for sampling the texture instead of the TEXTURE built-in. In this clip, I move the noise texture to a new uniform:

    Why did you add repeat_enable to the sampler2D uniform?

    Sprite2D nodes define how their texture repeats through the TextureRepeat property. The portal's noise texture has Repeat set to Enabled. This allows the animation offsetting the texture to run indefinitely.

    sampler2D uniforms are not controlled by the parent sprite. They are created inside the shader, which means you need to manually tell Godot that the texture will repeat with the repeat_enable hint. You can check out all editor hints in the official documentation.

    Breakpoints do not work inside loops

    Setting breakpoints inside while, for, and do...while loops will also cause an error. This prevents you from inspecting individual loop iterations.

    Workaround: If you used the loop to update a variable, you can preview its final value by assigning it to itself immediately after the loop completes and placing a breakpoint on that line.

    updates / code patches

    Become an Indie Gamedev with GDQuest!

    Don't stop here. Step-by-step tutorials are fun but they only take you so far.

    Try one of our proven study programs to become an independent Gamedev truly capable of realizing the games you’ve always wanted to make.

    Nathan

    Founder and teacher at GDQuest
    • Starter Kit
    • Learn Gamedev from Zero
    Check out GDSchool

    You're welcome in our little community

    Get help from peers and pros on GDQuest's Discord server!

    20,000 membersJoin Server

    Contribute to GDQuest's Free Library

    There are multiple ways you can join our effort to create free and open source gamedev resources that are accessible to everyone!

    Site in BETA!found a bug?