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