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

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.