Quick answer: Pass a Packed*Array matching the shader’s element type. PackedVector3Array for vec3, PackedFloat32Array for float. Mismatched types silently fail.

A custom lighting shader samples up to 8 point lights via an array uniform. set_shader_parameter is called with positions, but the scene renders unlit. The uniform never updated.

Match Shader Element Type

// shader
uniform vec3 light_positions[8];

# GDScript
var positions: PackedVector3Array = [
    Vector3(0, 2, 0),
    Vector3(5, 2, 0),
]
material.set_shader_parameter("light_positions", positions)

Note the typed array. Plain Array of Vector3 works in some versions but is slower and version-fragile.

Fixed Length

The CPU array can be shorter than the shader array; unwritten elements are zero. Pad to expected length if your shader assumes all slots filled:

positions.resize(8)   # pad with Vector3.ZERO
material.set_shader_parameter("light_positions", positions)

Verify Uniform Name

Typo-safe lookup:

var shader = material.shader
for u in shader.get_shader_uniform_list():
    print(u.name, " ", u.get("type"))

Confirms the exact uniform name and type the shader expects. Spelling mistakes are silent failures.

For Per-Instance Arrays

set_shader_parameter is per-material. For per-instance arrays use ShaderMaterial duplication or MultiMesh custom data:

multimesh.use_custom_data = true
multimesh.set_instance_custom_data(idx, Color(...))

Verifying

Render scene; lights appear. Drag a light position; scene updates. Profile: array uniform shows up in the RenderingServer profiler as one upload per material change.

“Shader arrays are strongly typed. Match the Packed type and pad to length; otherwise the uniform stays zero.”

For dynamic light counts, declare the array max-length in shader and pass a count uniform too — the shader can loop only over used slots.