Quick answer: Push the entire array each frame using material.set_shader_parameter("name", packed_array). Per-element updates aren’t supported; you must rewrite the whole array.
A custom lighting shader uses a uniform array to hold up to 16 dynamic light positions. The first frame the array updates correctly. Subsequent frames, the shader keeps using the initial values — lights don’t move with their source nodes.
Uniform Arrays in Godot Shaders
Declare a uniform array in the shader with a fixed size:
// dynamic_lights.gdshader
shader_type canvas_item;
uniform vec3 light_positions[16];
uniform int active_light_count = 0;
uniform float light_radius = 100.0;
void fragment() {
vec3 col = vec3(0.0);
for (int i = 0; i < active_light_count; i++) {
float dist = distance(SCREEN_UV * 1024.0, light_positions[i].xy);
col += vec3(1.0) * smoothstep(light_radius, 0.0, dist);
}
COLOR = vec4(col, 1.0);
}
The array has a compile-time-fixed length (16 in this example). At runtime, you can’t push individual slots — only the entire array.
Pushing the Array Each Frame
@onready var mat: ShaderMaterial = $LightingCanvas.material
func _process(_delta):
var positions = PackedVector3Array()
for light in dynamic_lights:
positions.append(Vector3(light.global_position.x, light.global_position.y, 0))
# Pad with zeros to fill the fixed 16 slots
while positions.size() < 16:
positions.append(Vector3.ZERO)
mat.set_shader_parameter("light_positions", positions)
mat.set_shader_parameter("active_light_count", dynamic_lights.size())
Notice the padding step: Godot expects the array length passed to match the shader-declared size. Mismatched sizes either silently clamp or cause the parameter to be ignored entirely depending on driver.
Type Choices
- PackedFloat32Array — for
float[]uniforms. - PackedVector2Array — for
vec2[]. - PackedVector3Array — for
vec3[]. - PackedColorArray — for
vec4[]when treated as colors. - PackedInt32Array — for
int[].
Pass the wrong typed array and Godot logs “Invalid shader parameter type” in the console — check the output panel under --verbose.
When to Switch to a Texture
For arrays larger than ~64 elements, the uniform-array path is slow on the Compatibility renderer (each frame uploads the whole array via GLSL ES). Encode the data into a 2D texture and sample it:
uniform sampler2D light_data : hint_default_white;
// Each pixel encodes one light’s data
vec4 light_info = texelFetch(light_data, ivec2(i, 0), 0);
vec2 pos = light_info.xy;
Update the texture from script using ImageTexture or RD.texture_update. Texture uploads can be partial (a single line) and parallelize across many shaders sharing the data.
Verifying
Move a light source and watch the shader output. If only the first frame reflects the new position, your set_shader_parameter is in _ready instead of _process. Move the call to a per-frame callback or trigger it via a signal whenever data changes.
“Shader uniform arrays are read-only blocks. To update, you replace the whole block every frame.”
If you find yourself building a huge fixed-size array each frame, switch to a texture lookup — faster, cleaner, no upper limit.