Really poor frame rates, I don't see what could be wrong?

Claude (Opus 4.6) oneshotted this perfectly from the description. Only problem is the frame rate, it’s just way too slow at 2-3fps. The piece is ~1400 pixels divided up into 120-220 pixel panels on each pin of an output expander:

```
// Comet Storm — curved comets with glowing heads and tapered trails
// Designed for cylindrical 3D pixel map (jacket)

var maxComets = 8
export function sliderMax_Comets(v) { maxComets = 1 + floor(v * 14) }

var trailLen = 0.35
export function sliderTrail_Length(v) { trailLen = 0.1 + v * 0.7 }

var spdMul = 1
export function sliderSpeed(v) { spdMul = 0.2 + v * 1.8 }

var bright = 1
export function sliderBrightness(v) { bright = v }

var N = 15
// Core state
var cTh  = array(N)
var cYp  = array(N)
var cVt  = array(N)
var cVy  = array(N)
var cCrv = array(N)
var cHue = array(N)
var cLf  = array(N)
var cAge = array(N)
var cMx  = array(N)
var cTrl = array(N)

// Precomputed per-frame (avoids work in render loop)
var cNvx = array(N)  // normalized velocity x (surface space)
var cNvy = array(N)  // normalized velocity y
var cTl  = array(N)  // trail length this frame
var cBnd = array(N)  // max |dy| or |dx| before skip
var cBn2 = array(N)  // bounding d2
var cAct = array(N)  // 0 = dead, >0 = life value

var ASP = 0.3
var HW = 0.03
var HW4 = HW * 4
var HW4sq = HW4 * HW4
var HW3sq = (HW * 3) * (HW * 3)
var satScale = 1 / HW3sq

for (var i = 0; i < N; i++) cLf[i] = 0

function spawn(i) {
    cTh[i] = random(PI2)
    cYp[i] = random(1)
    var dir = random(PI2)
    var spd = 0.15 + random(0.45)
    cVt[i] = sin(dir) * spd / ASP
    cVy[i] = cos(dir) * spd
    cCrv[i] = (random(1) - 0.5) * 1.6
    cHue[i] = random(1)
    cLf[i] = 0.001
    cAge[i] = 0
    cMx[i] = 1.5 + random(3)
    cTrl[i] = 0.7 + random(0.6)
}

var spTmr = 0

export function beforeRender(delta) {
    var dt = delta / 1000 * spdMul

    spTmr += delta / 1000
    var interval = 0.5 / max(maxComets, 1)
    if (spTmr > interval) {
        spTmr = 0
        var cnt = 0
        for (var i = 0; i < N; i++) { if (cLf[i] > 0) cnt++ }
        if (cnt < maxComets) {
            for (var i = 0; i < N; i++) {
                if (cLf[i] <= 0) { spawn(i); break }
            }
        }
    }

    for (var i = 0; i < N; i++) {
        if (cLf[i] <= 0) { cAct[i] = 0; continue }

        cAge[i] += dt

        if (cAge[i] < 0.35) {
            cLf[i] = cAge[i] / 0.35
        } else if (cAge[i] > cMx[i]) {
            cLf[i] = max(0, 1 - (cAge[i] - cMx[i]) / 0.5)
            if (cLf[i] <= 0) { cAct[i] = 0; continue }
        } else {
            cLf[i] = 1
        }

        var ca = cCrv[i] * dt
        var vxs = cVt[i] * ASP
        var vys = cVy[i]
        var cc = cos(ca)
        var ss = sin(ca)
        cVt[i] = (vxs * cc - vys * ss) / ASP
        cVy[i] = vxs * ss + vys * cc
        cTh[i] = mod(cTh[i] + cVt[i] * dt, PI2)
        cYp[i] += cVy[i] * dt

        if (cYp[i] < -0.4 || cYp[i] > 1.4) { cLf[i] = 0; cAct[i] = 0; continue }

        // Precompute render data — moves sqrt + divides out of per-pixel loop
        var vtS = cVt[i] * ASP
        var spd = sqrt(vtS * vtS + cVy[i] * cVy[i])
        if (spd < 0.001) { cAct[i] = 0; continue }
        cNvx[i] = vtS / spd
        cNvy[i] = cVy[i] / spd
        cTl[i] = trailLen * cTrl[i]
        cBnd[i] = cTl[i] + HW4
        cBn2[i] = cBnd[i] * cBnd[i]
        cAct[i] = cLf[i]
    }
}

export function render3D(index, x, y, z) {
    var pTh = atan2(z - 0.5, x - 0.5)
    if (pTh < 0) pTh += PI2

    var bestBr = 0
    var bestHue = 0
    var bestD2 = 1

    for (var i = 0; i < N; i++) {
        if (cAct[i] == 0) continue

        // Cheapest check first: vertical distance
        var dy = y - cYp[i]
        if (dy > cBnd[i] || dy < -cBnd[i]) continue

        // Angular distance
        var dth = pTh - cTh[i]
        if (dth > PI) dth -= PI2
        if (dth < -PI) dth += PI2
        var dx = dth * ASP

        // Bounding check on d2 — no sqrt needed
        var d2 = dx * dx + dy * dy
        if (d2 > cBn2[i]) continue

        // Project onto and perpendicular to velocity (precomputed normals)
        var along = dx * cNvx[i] + dy * cNvy[i]
        var perp = abs(dx * cNvy[i] - dy * cNvx[i])

        // Head glow — uses d2 directly, no sqrt
        var headBr = 0
        if (d2 < HW4sq) {
            var v2 = 1 - d2 / HW4sq
            headBr = v2 * v2 * 2.5
        }

        // Trail — tapered tail behind head
        var trBr = 0
        var tl = cTl[i]
        if (along < HW && along > -tl) {
            var t = -along / tl
            if (t < 0) t = 0
            var w = HW * (1 - t * 0.92) * 2.5
            if (perp < w) {
                var pf = 1 - perp / w
                var af = 1 - t
                trBr = pf * pf * af * af
            }
        }

        var br = headBr
        if (trBr > br) br = trBr
        br *= cAct[i]
        if (br > bestBr) {
            bestBr = br
            bestHue = cHue[i]
            bestD2 = d2
        }
    }

    if (bestBr < 0.004) {
        hsv(0, 0, 0)
    } else {
        // White-hot core: saturation low near head center, full in trail
        // Uses d2 directly — quadratic falloff instead of linear, looks fine
        var sat = bestD2 * satScale
        if (sat < 0.15) sat = 0.15
        if (sat > 1) sat = 1
        hsv(bestHue, sat, bestBr * bright)
    }
}
```

Anybody got any hints?

(Just as a general thing, I’ve noticed lots of patterns which I think should be quick just aren’t. What’s the processor on a Pixelblaze like performance-wise compared to like a Teensy 3.1?)

Looks reasonably efficient for what it is, but looping over every comet for every pixel is pretty expensive. Claude has done its due diligence with stuff like // Cheapest check first: vertical distance but what you have here is essentially a ray tracer.

It would probably be faster to iterate over the comets and draw into a framebuffer array in beforeRender then just look up the value in render3D. You could even get the tails for free by attenuating the framebuffer as I do for ‘motion blur’ in these patterns: LEDaliClock matrix pattern - #10 by sorceror and Yet another approach to text rendering (WIP) - #23 by sorceror .

I would be interested to hear what Claude can make of this idea!

1 Like

I agree - The simplest way I can think of to do this is, as @sorceror suggested is to render a “hot” spherical comet head into a framebuffer, then have it decay over time by “cooling” - subtracting a small amount from brightness on every frame. This way you get the tails for free.

There are several stock Pixelblaze patterns including, I think, the original KITT, that work this way.

I actually did this exact thing - lots of music reactive comets with tails for Titanic’s End a couple of years ago. Here’s a pointer to the GLSL shader, to help Claude understand the method, and here’s a short video of it in action (with tails turned way down – they’re adjustable just by changing the cooling rate.)

2 Likes

late answer, but from my own 3600 pixel array on a single PB, what makes PB cool (writing code in the browser in an interpreted language) is also what makes it very slow.
It runs on ESP32 which can easily run tens of thousands of pixels if your code in C++ but with PB, it’s 20 to 50 times slower than raw C++ in my experience.
the output expander (I have one too) helps go beyond the speed of a single strand, but your bottleneck is the the code interpreter.
Now, I will still say that 2-3fps is still slower than expected and maybe if you know how to optimize your code for the interpreter, it could go faster, but don’t expect huge miracles given the above