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
- 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.
- On the left under Examples, click Rainbow. The code is loaded into the editor.
- In the device list, select the fixtures the effect should run on. Without a selection the script runs on all fixtures.
- Click Execute at the bottom. The status bar shows that the script is running, and the preview and your LEDs show the rainbow.
- Change a number in the code, for example the speed, and click Execute again.
- 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 lineor--[[ several lines ]] - Division always returns a decimal:
1 / 2is0.5. Integer division uses//:7 // 2is3.
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.
- In the Parameter sidebar, click + New Parameter.
- Enter a Parameter ID (e.g.
speed) and a Parameter Name (e.g. “Speed”). - Choose the type Float (decimal), Integer (whole number) or Boolean (on/off) and, for numbers, enter Min, Max and Default.
- 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:
- Set
parallel = trueat the very top of the script. - Loop from
led_starttoled_end - 1instead of0toled_count - 1. Keep usingled_countfor 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
.zfxfile 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.