Frame rates on v3 Standard vs v3 Pico

I 3D printed a “saturn-like” planet totem and it turned out great!

Here’s a video: https://www.youtube.com/watch?v=XR7h_hy_Yc8

Most of the classic patterns like honeycomb run at ~40 fps, which is perfectly fine IMO.

However, when I get fancy and start adding color palettes with @jeff 's fancy shimmer transitions, the fps drops down to around ~19 fps.

Here’s a rundown on my project:

  • Single Pixelblaze pico v3 (v3.51)
  • 719 pebble style leds: 5V WS2811 15mm pitch leds on aliexpress
  • It’s using a custom 3D map I created in blender

I’m looking for suggestions on increasing the frame rate.

  • Would a standard pixelblaze give better results than the pico?

Here’s an example I setup:

Wavy Bands (without palettes): 31 fps
Wavy Bands with palettes: 19 fps
Here’s @zranger1 's Wavy Bands pattern with my added palette code:

// Wavy Bands
//
// Requires a correctly configured 2D map
//
// MIT License - use this code to make more cool things!
//
// 6/22/2023 ZRanger1

// Added color palettes and some more controls
// 1/19/2024 dylan.conlin

// Global variables
var timebase = 0; // Time accumulator for animation control
export var nColumns = 4; // Number of displayed columns

// Speed variables with default values
export var xSpeed = 3.25; // Speed of x-axis wave movement
export var ySpeed = 5.18; // Speed of y-axis width variation
var tx, ty
// toggling this to true will enable the slider for column count
var adaptiveColumnCount = false;

// Parameter names that correspond to Pixelblaze UI controls
var shimmer = true;
export var transition = 0.15;
var secondsPerPalette = 8;

// Internal state variables
var paletteIndex;
var lastPaletteIndex = -1;

export function sliderColumns(v) {
  nColumns = mapRange(v, 1, 10);
}

export function sliderXSpeed(v) {
  xSpeed = mapRange(v, 1, 10);
}

export function sliderYSpeed(v) {
  ySpeed = mapRange(v, 1, 10);
}

export function sliderTransitionTime(v) {
  transition = mapRange(v, 0.10, 3.0);
}

// enable/disable column count based on palette
export function toggleAdaptiveColumnCount(v) {
  adaptiveColumnCount = v;
}

export function toggleShimmer(v) {
  shimmer = v;
}

export function inputNumberSecondsPerPalette(v) {
  secondsPerPalette = v;
}

export function showNumberPalette() {
  return paletteIndex;
}

function mapRange(value, min, max) {
  return min + (max - min) * value;
}


// Functions to handle transitions between color palettes
function handlePaletteTransitions() {
  paletteIndex = time(secondsPerPalette / 65.536 * palettes.length) * palettes.length;
  applyShimmerEffect();
  updatePaletteIfChanged();
}
function applyShimmerEffect() {
  if (frac(paletteIndex) > (1 - transition)) {
    var transitionFactor = (frac(paletteIndex) - (1 - transition)) / transition;
    if (shimmer && wave(transitionFactor / 2 - 0.25) > random(1)) {
      paletteIndex = mod(paletteIndex + 1, palettes.length);
    }
  }
}
function updatePaletteIfChanged() {
  var floorPaletteIndex = floor(paletteIndex);
  if (floorPaletteIndex !== lastPaletteIndex) {
    setPalette(palettes[floorPaletteIndex]);
    if (adaptiveColumnCount) {
      nColumns = palettes[floorPaletteIndex].length / 4;  
    }       
    lastPaletteIndex = floorPaletteIndex;
  }
}

// Function called before each frame render. Calculates time-based variables
// for animation.
export function beforeRender(delta) {
  // Accumulate time with loop around 3600 seconds for continuous animation
  timebase = (timebase + delta / 1000) % 3600;
  
  // Calculate speeds for x and y axis movements based on slider inputs
  tx = -timebase / xSpeed;   // Speed of x-axis movement, negative for reverse direction
  ty = timebase / ySpeed;    // Speed of y-axis movement
}

// Render function for 2D LED matrices. Creates wavy column patterns.
export function render2D(index, x, y) {
  handlePaletteTransitions(); // Manage transitions between color palettes

  // Apply transformations to x and y coordinates for wave effects
  y -= 0.3 * perlin(x * 2, y * 2, ty, 1.618); 
  x += 0.1752 * sin(4 * (tx + y));
  
  // Quantize x-coordinate into columns and determine brightness
  var column = floor(x * nColumns);
  var brightness = (x * nColumns - 0.5);
  brightness = 1 - (2 * abs(brightness - column));
  brightness = pow(brightness, 1.25); // Apply gamma correction for brightness

  var hue = column / nColumns; // Determine hue based on column position

  paint(hue, brightness); // Paint the pixel with calculated hue and brightness
}

// Function to project 2D pattern into 3D space.
export function render3D(index, x, y, z) {
  var x1 = (x - cos(z / 4 * PI2)) / 2;
  var y1 = (y - sin(z / 4 * PI2)) / 2;
  render2D(index, x1, y1);
}

var lava = [0.0, 68/255, 1/255, 84/255, 0.18, 0.071, 0.0, 0.0, 0.376, 0.443, 0.0, 0.0, 0.424, 0.557, 0.012, 0.004, 0.467, 0.686, 0.067, 0.004, 0.573, 0.835, 0.173, 0.008, 0.682, 1.0, 0.322, 0.016, 0.737, 1.0, 0.451, 0.016, 0.792, 1.0, 0.612, 0.016, 0.855, 1.0, 0.796, 0.016, 0.918, 1.0, 1.0, 0.016, 0.957, 1.0, 1.0, 0.278, 1.0, 1.0, 1.0, 1.0,];
var ib_jul01 = [0.0, 0.761, 0.004, 0.004, 0.369, 0.004, 0.114, 0.071, 0.518, 0.224, 0.514, 0.11, 1.0, 0.443, 0.004, 0.004,];
var palettes = [
  lava,
  ib_jul01,
]
2 Likes

You’ll get a big, immediate frame rate increase by moving handlePaletteTransitions() out of render2d, where it is called for each pixel, to beforeRender(), where it’ll be called once per frame.

Everything else looks good – of course there’s no end to the little stuff you can optimize, but the moving the palette hander call is the main thing here.

Edit: That actually might not help much – just tested on an 800 pixel matrix, including the 2D->3D projection math, and I’m getting roughly the same results you are (On a standard v3, though that shouldn’t matter.) It may be that these patterns just do enough per-pixel calculation to slow the Pixelblaze down as the pixel count goes up.

3 Likes

Yeah, agree with @zranger here. I think the easiest solution is to get another Pixelblaze and run them in sync mode so each one only calculates half the pixels. Instant double of your frame rate.

The Pico and v3 are equally fast at computing pixels, so feel free to pick whatever works better for you!

1 Like

Here’s my code golfed version of this. Fairly decent speed up. On a 1024 pixel system the FPS went from ranging around 13.5-16.5 up to 18.5-20. With 70% as many pixels, you might see about 28FPS.

Note that setPalette is not an expensive call. It just keeps track of which array you feed it. No need to guard that, so I took out the lastPaletteIndex stuff.

The base paletteIndex and transition curve stuff doesn’t change per pixel, so moved those to beforeRender and pre-calc as much as we can there.

  paletteIndex = time(secondsPerPalette / 65.536 * palettes.length) * palettes.length;
  transitionFactor = (frac(paletteIndex) - (1 - transition)) / transition;
  transitionWave = wave(transitionFactor / 2 - 0.25)

The per-pixel code is now much smaller and faster:

function handlePaletteTransitions() {
  var piNew = paletteIndex
  if (shimmer && transitionFactor > 0 && transitionWave > random(1)) {
    piNew = mod(piNew + 1, palettes.length);
  }
  setPalette(palettes[piNew]);
  if (adaptiveColumnCount) {
    nColumns = palettes[piNew].length / 4;  
  }
}

Whole pattern with changes:

// Wavy Bands
//
// Requires a correctly configured 2D map
//
// MIT License - use this code to make more cool things!
//
// 6/22/2023 ZRanger1

// Added color palettes and some more controls
// 1/19/2024 dylan.conlin

// Global variables
var timebase = 0; // Time accumulator for animation control
export var nColumns = 4; // Number of displayed columns

// Speed variables with default values
export var xSpeed = 3.25; // Speed of x-axis wave movement
export var ySpeed = 5.18; // Speed of y-axis width variation
var tx, ty
// toggling this to true will enable the slider for column count
var adaptiveColumnCount = false;

// Parameter names that correspond to Pixelblaze UI controls
var shimmer = true;
export var transition = 0.15;
var secondsPerPalette = 8;

// Internal state variables
var paletteIndex;
var transitionFactor;
var transitionWave;

export function sliderColumns(v) {
  nColumns = mapRange(v, 1, 10);
}

export function sliderXSpeed(v) {
  xSpeed = mapRange(v, 1, 10);
}

export function sliderYSpeed(v) {
  ySpeed = mapRange(v, 1, 10);
}

export function sliderTransitionTime(v) {
  transition = mapRange(v, 0.10, 3.0);
}

// enable/disable column count based on palette
export function toggleAdaptiveColumnCount(v) {
  adaptiveColumnCount = v;
}

export function toggleShimmer(v) {
  shimmer = v;
}

export function inputNumberSecondsPerPalette(v) {
  secondsPerPalette = v;
}

export function showNumberPalette() {
  return paletteIndex;
}

function mapRange(value, min, max) {
  return min + (max - min) * value;
}


// Functions to handle transitions between color palettes
function handlePaletteTransitions() {
  var piNew = paletteIndex
  if (shimmer && transitionFactor > 0 && transitionWave > random(1)) {
    piNew = mod(piNew + 1, palettes.length);
  }
  setPalette(palettes[piNew]);
  if (adaptiveColumnCount) {
    nColumns = palettes[piNew].length / 4;  
  }
}

// Function called before each frame render. Calculates time-based variables
// for animation.
export function beforeRender(delta) {
  // Accumulate time with loop around 3600 seconds for continuous animation
  timebase = (timebase + delta / 1000) % 3600;
  
  // Calculate speeds for x and y axis movements based on slider inputs
  tx = -timebase / xSpeed;   // Speed of x-axis movement, negative for reverse direction
  ty = timebase / ySpeed;    // Speed of y-axis movement

  paletteIndex = time(secondsPerPalette / 65.536 * palettes.length) * palettes.length;
  transitionFactor = (frac(paletteIndex) - (1 - transition)) / transition;
  transitionWave = wave(transitionFactor / 2 - 0.25)
}

// Render function for 2D LED matrices. Creates wavy column patterns.
export function render2D(index, x, y) {
  handlePaletteTransitions()
  // Apply transformations to x and y coordinates for wave effects
  y -= 0.3 * perlin(x * 2, y * 2, ty, 1.618); 
  x += 0.1752 * sin(4 * (tx + y));
  
  // Quantize x-coordinate into columns and determine brightness
  var column = floor(x * nColumns);
  var brightness = (x * nColumns - 0.5);
  brightness = 1 - (2 * abs(brightness - column));
  
  //removed for speed, has minimal visual impact
  // brightness = pow(brightness, 1.25); // Apply gamma correction for brightness

  var color = column / nColumns; // Determine color based on column position

  paint(color, brightness); // Paint the pixel with calculated hue and brightness
}

// Function to project 2D pattern into 3D space.
export function render3D(index, x, y, z) {
  var x1 = (x - cos(z / 4 * PI2)) / 2;
  var y1 = (y - sin(z / 4 * PI2)) / 2;
  render2D(index, x1, y1);
}

var lava = [
  0.0, 68/255, 1/255, 84/255, 
  0.18, 0.071, 0.0, 0.0, 
  0.376, 0.443, 0.0, 0.0, 
  0.424, 0.557, 0.012, 0.004, 
  0.467, 0.686, 0.067, 0.004, 
  0.573, 0.835, 0.173, 0.008, 
  0.682, 1.0, 0.322, 0.016, 
  0.737, 1.0, 0.451, 0.016, 
  0.792, 1.0, 0.612, 0.016, 
  0.855, 1.0, 0.796, 0.016, 
  0.918, 1.0, 1.0, 0.016, 
  0.957, 1.0, 1.0, 0.278, 
  1.0, 1.0, 1.0, 1.0,];
var ib_jul01 = [
  0.0, 0.761, 0.004, 0.004, 
0.369, 0.004, 0.114, 0.071, 
0.518, 0.224, 0.514, 0.11, 
1.0, 0.443, 0.004, 0.004,];
var palettes = [
  lava,
  ib_jul01,
]
4 Likes

I’ll keep the synced pixelblaze trick in mind for my next project.

I implemented the changes from @wizard and now I’m getting around 28 fps :tada:

Thank you all for the help!

2 Likes

What’s your method for converting your 3D maps from Blender to JavaScript array?

I create 3D maps in blender by importing my object, which I create with the Scaniverse iOS app, into Blender and then manually adding empty axis objects to each LED, making sure to add them in the correct sequence that the LEDs light up.

Once I’ve added an empty axis object to each LED, I run a python script to export them as a JSON array of x, y, z coordinates.

Here’s an example I used for a heart-shaped model I created:

import bpy
import json

name = "heart-totem"
empty_prefix = "Empty"  # Replace with your Empty object naming pattern

objects = bpy.data.objects
empty_data = []
output_file = "~/Users/dylanconlin/Downloads/" + name + ".json"

for obj in objects:
    if obj.name.startswith(empty_prefix) and obj.type == 'EMPTY':
        empty_data.append(obj.location[:])

with open(output_file, 'w') as f:
    json.dump(empty_data, f, indent=4)

print(f"Coordinates saved to {output_file}")
3 Likes