Quick answer: Call get_active_material(0).duplicate() and assign the result back to the node to ensure a unique ShaderMaterial instance, then call set_shader_parameter() on that unique copy. Also verify the parameter name matches the uniform declaration in the shader exactly—including capitalization.
You write a dissolve shader, hook it up in the Inspector, call material.set_shader_parameter("cutoff", dissolve_amount) in _process()—and nothing changes on screen. No error, no warning, just a completely static shader as if your code isn’t running. Or worse: you have ten enemies using the same material, and changing a uniform on one changes all of them simultaneously.
The Symptom
Shader uniform problems in Godot 4 present in several distinct ways:
- Calling
set_shader_parameter()has no visible effect even though the code runs without errors - Changing a uniform on one node changes the appearance of multiple unrelated nodes
- The shader looks correct in the editor but ignores runtime changes entirely
- An error in the Output panel:
Parameter 'my_param' not found in shader - Code migrated from Godot 3 throws
Invalid call. Nonexistent function 'set_shader_param'
What Causes This
Using the Godot 3 method name set_shader_param() in Godot 4. This method was renamed in Godot 4. set_shader_param() and get_shader_param() no longer exist. The new names are set_shader_parameter() and get_shader_parameter(). This produces a runtime error in most cases, but if the error is swallowed or the call path isn’t hit during testing, nothing happens on screen and the cause is invisible.
The material is a shared resource. When you assign a ShaderMaterial in the Inspector by dragging it from the FileSystem panel, Godot stores a reference to the same resource object in every node that uses it. Calling set_shader_parameter() on that shared resource changes it globally—every node using the material updates simultaneously. This is useful for a global effect like a world-tint, but it’s almost certainly not what you want for per-instance properties like a hit-flash color or a per-enemy dissolve amount.
The uniform name doesn’t match the shader declaration exactly. set_shader_parameter() takes a string name that must match the uniform declaration in the .gdshader file character-for-character. The name is case-sensitive. "CutOff" and "cutoff" are different names. There is no autocomplete for these strings in GDScript, so typos are silent failures—no error, no effect.
Setting the parameter on the wrong object. The parameter must be set on the ShaderMaterial instance, not on the MeshInstance3D or Sprite2D node directly. Accessing $Sprite.material may return null if no material override is set, or it may return the surface’s base material rather than a per-node override.
The Fix: Getting the Correct Material Instance
The standard safe pattern for accessing a node’s ShaderMaterial is via get_active_material() for mesh-based nodes:
var mat: ShaderMaterial = $MeshInstance3D.get_active_material(0) as ShaderMaterial
if mat:
mat.set_shader_parameter("dissolve_amount", 0.5)
For Sprite2D, ColorRect, and other 2D nodes use the material property:
var mat: ShaderMaterial = $Sprite2D.material as ShaderMaterial
if mat:
mat.set_shader_parameter("flash_intensity", 1.0)
Creating a Unique Material Instance per Node
If you need each node to have independent shader parameter values, you must give each node its own ShaderMaterial instance. There are two ways to accomplish this:
Option 1: material.duplicate() at runtime. Call duplicate() in _ready() to make a deep copy of the shared material and assign it back to the node. This is the most explicit approach:
extends MeshInstance3D
var _mat: ShaderMaterial
func _ready() -> void:
# Create a unique copy so changes don't affect other nodes
_mat = get_active_material(0).duplicate() as ShaderMaterial
set_surface_override_material(0, _mat)
func set_dissolve(amount: float) -> void:
_mat.set_shader_parameter("dissolve_amount", amount)
Option 2: Enable resource_local_to_scene on the material. In the Inspector, select your ShaderMaterial resource, expand its header, and check Resource → Local To Scene. With this flag enabled, Godot automatically creates a separate copy of the material for each scene instance that contains it. No code changes are needed at all:
# With resource_local_to_scene enabled on the .tres file,
# this is now safe — each scene instance has its own copy
func set_hit_flash(intensity: float) -> void:
var mat = $Sprite2D.material as ShaderMaterial
mat.set_shader_parameter("flash_intensity", intensity)
The trade-off: resource_local_to_scene works best when a scene is instanced many times and each instance must be independent. For one-off nodes that happen to share a material, duplicate() in _ready() is more explicit and easier to audit at a glance.
Matching the Uniform Name Exactly
Open your .gdshader file and check the exact spelling of each uniform you plan to set from code:
// In dissolve.gdshader
shader_type spatial;
uniform float dissolve_amount : hint_range(0.0, 1.0) = 0.0;
uniform sampler2D noise_texture : source_color;
uniform vec4 edge_color : source_color = vec4(1.0, 0.5, 0.0, 1.0);
The GDScript calls must use these exact names:
mat.set_shader_parameter("dissolve_amount", 0.7) # Correct
mat.set_shader_parameter("noise_texture", my_tex) # Correct
mat.set_shader_parameter("edge_color", Color.RED) # Correct
mat.set_shader_parameter("DissolveAmount", 0.7) # Wrong — case mismatch, silent failure
mat.set_shader_parameter("cutoff", 0.7) # Wrong — name doesn't exist, silent failure
A quick way to verify the parameter name is correct at runtime is to check its current value first. If get_shader_parameter() returns null for a name, that name does not exist in the shader:
var current = mat.get_shader_parameter("dissolve_amount")
print("Current dissolve_amount: ", current) # null = name is wrong
Using Global Shader Uniforms
When you want a single parameter to affect all materials that reference it simultaneously—a global time-of-day float, a world-space wind direction, or a screen-wide desaturation amount—Godot 4 provides global shader uniforms via the RenderingServer. These are ideal for environmental parameters that drive many materials at once without requiring a reference to each individual material.
First, declare the uniform as global in your shader source:
shader_type spatial;
// Global uniforms use the 'global' keyword
global uniform float world_time;
global uniform vec3 wind_direction;
void vertex() {
VERTEX.y += sin(world_time + VERTEX.x) * 0.05;
}
Then register and update the values from GDScript. Registration only needs to happen once, typically in an autoload singleton:
extends Node # Autoload: WorldShaderParams
func _ready() -> void:
RenderingServer.global_shader_parameter_add(
"world_time", RenderingServer.GLOBAL_VAR_TYPE_FLOAT, 0.0
)
RenderingServer.global_shader_parameter_add(
"wind_direction", RenderingServer.GLOBAL_VAR_TYPE_VEC3, Vector3(1, 0, 0)
)
func _process(delta: float) -> void:
# All materials using 'world_time' update at once with a single call
RenderingServer.global_shader_parameter_set(
"world_time", Time.get_ticks_msec() / 1000.0
)
Note that global uniforms are set project-wide and do not exist within a specific material. Calling set_shader_parameter("world_time", ...) on a ShaderMaterial will not work if the shader declares the uniform with the global keyword—you must use RenderingServer.global_shader_parameter_set() for global uniforms.
Migrating from Godot 3
If you are porting a project from Godot 3, do a project-wide search for set_shader_param and get_shader_param and replace every occurrence:
# Godot 3 — these methods no longer exist in Godot 4
material.set_shader_param("my_value", 1.0)
var v = material.get_shader_param("my_value")
# Godot 4 — correct method names
material.set_shader_parameter("my_value", 1.0)
var v = material.get_shader_parameter("my_value")
The Godot 4 compatibility layer does not alias the old names, so there is no silent fallback. The migration must be done explicitly across every script in the project.
Related Issues
Shader uniform issues are often discovered during testing on different hardware where visual regressions are easy to miss. See Fix: Godot Physics Interpolation Causing Jitter if visual artifacts persist after fixing uniforms, and Fix: Godot @export Variable Not Saving Between Sessions for issues with Inspector-assigned material references not surviving scene reloads.
One missed duplicate() and your whole fleet of enemies flashes red when only one of them gets hit.