Skip to content
PixDrive
All articles

Script Engine: custom effects with Lua

Write, test and save your own LED effects in Lua – with parameters, audio reaction, 2D matrices and multi-core rendering.

Updated

When the built-in effects aren’t enough, you write your own in PixDrive Studio. The Script Engine runs Lua 5.4: your script is called for every frame and sets the color of every single LED. You see changes right away in the preview and on your devices.

This guide takes you from your first script to parameters, audio reaction, 2D matrices and multi-core rendering.

Your first script

  1. Open Scripts in the toolbar at the top. You’ll see the LED preview, the code editor below it and the Parameter sidebar on the right.
  2. On the left under Examples, click Rainbow. The code is loaded into the editor.
  3. In the device list, select the fixtures the effect should run on. Without a selection the script runs on all fixtures.
  4. Click Execute at the bottom. The status bar shows that the script is running, and the preview and your LEDs show the rainbow.
  5. Change a number in the code, for example the speed, and click Execute again.
  6. Save stores the script under a name in the Library.

To stop it, click the × next to the running script in the Active Effects list. Running scripts are marked there with an amber dot.

Structure of a script

Every script must define a render() function. It is called once per frame. Code outside render() runs only once at startup – ideal for preparing values.

-- runs once at startup
local offset = 0.0

function render()
    -- runs every frame
    offset = offset + delta * 60.0
    for i = 0, led_count - 1 do
        local hue = (i / led_count * 360.0 + offset) % 360.0
        set_led_hsv(i, hue, 1.0, 1.0)
    end
end

A few Lua basics you’ll use all the time:

  • Declare variables with local: local x = 5
  • Loop over all LEDs: for i = 0, led_count - 1 do … end
  • Condition: if x > 0 then … else … end
  • Comments: -- one line or --[[ several lines ]]
  • Division always returns a decimal: 1 / 2 is 0.5. Integer division uses //: 7 // 2 is 3.

Setting LEDs

LEDs are set directly with functions. Indices run from 0 to led_count - 1.

Function Effect
set_led_hsv(i, h, s, v) Color as hue, saturation, brightness. h: 0–360, s and v: 0.0–1.0
set_led_rgb(i, r, g, b) Color as red, green, blue, each 0.0–1.0
set_led_rgba(i, r, g, b, a) like RGB, plus transparency a (0.0–1.0)
set_led_cct(i, brightness, ct) white only: ct 0.0 = warm (2700 K) to 1.0 = cool (6500 K)

Keep color values within 0.0–1.0 with clamp(value, 0.0, 1.0) and wrap the hue with % 360.0.

Available variables

PixDrive sets these values before every frame:

Variable Meaning
time seconds since the effect started
delta seconds since the last frame – for smooth motion independent of the frame rate
frame number of the current frame, counting from 0
led_count number of LEDs
matrix_breite matrix width in pixels, 0 for an LED strip
matrix_hoehe matrix height in pixels, 0 for an LED strip
led_start, led_end LED range of this CPU core, see Performance

Parameters: adjust values live

Parameters make values like speed or hue adjustable without touching the code.

  1. In the Parameter sidebar, click + New Parameter.
  2. Enter a Parameter ID (e.g. speed) and a Parameter Name (e.g. “Speed”).
  3. Choose the type Float (decimal), Integer (whole number) or Boolean (on/off) and, for numbers, enter Min, Max and Default.
  4. Confirm with OK.

In the script the value is available as a variable with the prefix param_ – speed becomes param_speed:

-- @params: [{"id":"speed","name":"Speed","param_type":{"type":"Float","config":{"default":1.0,"min":0.1,"max":5.0,"step":0.05}},"automatable":true}]
function render()
    local t = time * param_speed
    for i = 0, led_count - 1 do
        set_led_hsv(i, (i / led_count * 360.0 + t * 60.0) % 360.0, 1.0, 1.0)
    end
end

PixDrive writes the first line with -- @params: automatically whenever you add or remove a parameter. You can also edit it by hand – the whole definition must then be on one line, and the sidebar picks up the change.

While the script is running, sliders appear below the parameter list. Changes take effect on the output immediately.

2D matrices

For LED matrices, pos_x(i) and pos_y(i) return the position of an LED in the image, each from 0.0 to 1.0 (pos_x: left → right, pos_y: top → bottom). Wiring, start corner and serpentine layout of the matrix are already taken into account.

-- concentric rings
function render()
    for i = 0, led_count - 1 do
        local x = pos_x(i) - 0.5
        local y = pos_y(i) - 0.5
        local dist = sqrt(x * x + y * y)
        set_led_hsv(i, (dist * 720.0 - time * 90.0) % 360.0, 1.0, 1.0)
    end
end

If a script should work on strips and matrices, check matrix_breite > 0 and use i / led_count for strips.

Reacting to music

When a microphone is enabled in the settings, audio values are available. Without a microphone they are 0.0 or false – your script still runs.

Variable Meaning
audio_bass bass (60–250 Hz), 0.0–1.0
audio_mids mids (250 Hz–2 kHz), 0.0–1.0
audio_highs highs (2–16 kHz), 0.0–1.0
audio_volume overall volume, 0.0–1.0
audio_beat true when a beat was detected
-- brightness follows the bass
function render()
    for i = 0, led_count - 1 do
        local hue = (i / led_count * 360.0 + time * 30.0) % 360.0
        set_led_hsv(i, hue, 1.0, audio_bass * 0.8 + 0.2)
    end
end

Math functions

The most important functions are available without the math. prefix:

Function Result
sin(x), cos(x), tan(x) trigonometry in radians – a full turn is 2 * pi
atan2(y, x) angle of a point, −π to π
sqrt(x), pow(x, y), abs(x) square root, power, absolute value
floor(x), ceil(x) round down or up
min(a, b), max(a, b) smaller or larger value
clamp(x, lo, hi) limit x to the range lo–hi
lerp(a, b, t) blend between a and b, t from 0.0 to 1.0
fmod(x, y) remainder of a division, like x % y
pi 3.14159…

You can also use everything else from the Lua standard library, such as math.random.

Remembering values between frames

Variables you declare outside render() keep their value from frame to frame:

-- a dot of light runs along the strip
local position = 0.0

function render()
    position = position + delta * 30.0
    if position >= led_count then position = 0.0 end
    local p = floor(position)
    for i = 0, led_count - 1 do
        local bright = clamp(1.0 - abs(i - p) * 0.3, 0.0, 1.0)
        set_led_hsv(i, 40.0, 1.0, bright)
    end
end

Use delta for motion instead of a fixed step per frame – the effect then runs at the same speed at any frame rate.

Performance on large installations

By default a script runs on one CPU core. With very many LEDs and heavy per-pixel calculations the frame rate can drop. That’s when you enable multi-core rendering:

  1. Set parallel = true at the very top of the script.
  2. Loop from led_start to led_end - 1 instead of 0 to led_count - 1. Keep using led_count for positions.
parallel = true

function render()
    for i = led_start, led_end - 1 do
        set_led_hsv(i, (i / led_count * 360.0 + time * 60.0) % 360.0, 1.0, 1.0)
    end
end

Alternatively, for_each_led(function(i) … end) handles the loop for you. Good to know:

  • The work is only split from 1024 LEDs, across up to 8 cores. Below that the script runs on a single core as usual.
  • Each core has its own variables. Values you remember between frames are not shared between cores.
  • Writes to LEDs outside a core’s range are ignored.

More tips: calculate values that are the same for all LEDs before the loop, and create tables outside render() instead of rebuilding them every frame.

Finding errors

  • Validate checks the code without running it – for example whether the syntax is correct and render() exists.
  • Errors appear in red below the editor, for example [Laufzeit] Zeile 12: …. The affected line is highlighted in the editor.
  • A runtime error that occurs every frame is shown only once. Leeren clears the list.

Common causes: a missing render() function, a forgotten end, or a parameter variable without the param_ prefix.

Reusing scripts

  • Library: Saved scripts are listed on the left under Library. A click loads them into the editor. Save overwrites the loaded script directly, Save as New Script creates a copy. The trash icon deletes a script immediately, without confirmation.
  • Sequencer: In the sequencer, one click places a library script as a 5-second clip at the playhead.
  • Presets: A new preset takes over all effects that are currently running – including a script you started with Execute.
  • Export and import: .zfx exports a script as an effect file, Import loads a .zfx file into the editor.

Writing scripts with AI

The LLM button copies a complete description of the script API to the clipboard. Paste it into the AI chat of your choice and describe the effect you want – then copy the generated code into the editor and start it with Execute.

Demo version

In the demo the Script Engine is fully usable, but scripts may be at most 20 lines long. The -- @params: line counts, and the editor shows the current length at the top. Longer scripts can still be validated and saved, but only run with a license. The sequencer is not available in the demo.