Bubble column prototype w/tempered glass diffuser

I’m certain that a million people have tried this before me, but…

Wow, the small rectangular bits of broken tempered glass make a fantastic diffuser! Here’s the prototype for a larger sculpture I’m working on – it’s a strip of 70 APA102 LEDs mounted on one side of a transparent tube, which is filled with glass bits. There’s a lot more subtle color variation and reflected sparkle IRL, but the video shows the overall look pretty well.

It’s running a bubble column pattern that I hacked together for the occasion because it just looked cool with all the internal reflections – perlin noise to color the background and time bubble releases, and a simple particle system for the bubbles. Pixelblaze makes this so easy lately!


Super cool! I got to see it better and understand how transfixing it is when made sure YouTube was serving me the HD version in a full-screen window, and then I could see the fine details in this glass!


I’m glad I have a 4K portrait monitor for these videos!

I think it could use bubbles with different speeds, dim “small” bubble moving fast and bright “big” bubbles moving more slowly. Oh and when the small bubbles reach a big bubble they get trapped behind it then join it and make it bigger.

… or a TNG warp core mode … :smiley:


Great ideas, @sorceror! I’ll post code when I’m done - hoping it will look good on any well diffused vertical display.

tl;dr - discussion for anyone interested in how the sausage is made:

Right now, groups of bubbles are injected at the bottom at slightly varied “slow” speeds, and accelerate towards a terminal velocity as they move upward. You can see that they do separate slightly as they move up, but this could definitely use more variation. I kept the range of speeds narrow, and didn’t allow much slowing down because…

this first cut uses a heating/cooling model, like “Sparks” or “Fireflies” to draw the bubbles. It runs fast, and is easy to code, but it has a problem in that movement in the “up/forward” direction happens in discrete 1-pixel jumps, so if slowed, it becomes nastily jumpy.

For v2, I’ll try distance-based rendering, which gives good anti-aliasing in all directions and will allow a wider range of bubble speeds. I’ll also borrow code from my old “Metaballs of Fire” pattern to handle bubble merging. That ought to fix it right up!

And oh yes, a warp core would be definitely be awesome!!

1 Like

Here’s the current code. I’ve not posted it to the library, because it’s fairly specific to a certain type of display – a 1D setup with maybe a couple hundred pixels per vertical and a really good diffuser. Doesn’t necessarily look great on bare 5 meter strips.

Anyway, this version launches bubbles at a tunable rate, with lots of random speed variance, merging bubbles, and an interesting “fizz” effect (which can be disabled) when bubbles collide.

Bubble Column
// Bubble Column
// Rising bubbles in a slowly swirling fluid for
// well diffused, vertically oriented 1D displays
// MIT License
// Take this code and use it to make cool things!
// 12/18/23 ZRanger1
var numBubbles = 9
var bubbleSize = 3
var valveOpen = 0.6
var fluidH = 0.64
var fluidB = 0.08

var startVelocity = 10         
var velocityRange = 1.5 * startVelocity;   
var acceleration = 0.02

var velocity = array(numBubbles)
var position = array(numBubbles)
var pixels = array(pixelCount)

var timebase = 0

// start bubbles off the display so they'll
// get injected over time by the normal mechanism.
for (i = 0; i < numBubbles; i++) {
  position[i] = pixelCount + 10

// UI 

// base hue of "fluid" in column.
export function hsvPickerFluidHue(h,s,v) {
  fluidH = h 

// higher == new bubbles more often
export function sliderBubbleValve(v) {
  valveOpen = 0.8 * (1 - v)

function randomSpeed() {
  return startVelocity  + random(velocityRange) - velocityRange / 2

export function beforeRender(delta) {
  delta /= 1000;
  timebase = (timebase + delta) % 3600;
  valve = 0.5 + 0.5 * perlin(timebase / 2,timebase,21.76,PI2);
  // check each bubble's contribution to each pixel
  for (pixel = 0; pixel < pixelCount; pixel++) {
    pixels[pixel] = 0;
    for (bubble = 0; bubble < numBubbles; bubble++) {
      // the line below is (a) a minor optimization, and 
      // (b) adds an "effervescent" effect when bubbles 
      // merge. Comment it out if you want smoother bubbling.
      if (pixels[pixel] > fluidB) break;
      dist = 1-min(1,abs(pixel - position[bubble])/bubbleSize) 
      pixels[pixel] += dist * dist * dist * dist

  // move the bubbles
  for (bubble = 0; bubble < numBubbles; bubble++) {
    position[bubble] += velocity[bubble] * delta;
    velocity[bubble] += acceleration;
    // let the bubble run slightly off the end of the strip so
    // decay looks right at top of column.
    // When the bubble is no longer visible, set up to 
    // reinject it at the bottom if the "valve" is open.
    if (position[bubble] > pixelCount) {
      if (valve >= valveOpen) {
        position[bubble] = 0;
        velocity[bubble] = randomSpeed();

export function render(index) {
  a = 0.165*perlin(11*(index/pixelCount - timebase / 10),66.6,33.3,PI2) 
  v = pixels[index]

  if (v <= fluidB) {
    hsv(fluidH + a, 1, fluidB)    
  else {
    hsv(fluidH + a, 0.45 , v)
1 Like

This is awesome! I really would love to see that in the pattern library, just add a comment about it (and a link to the video would explain everything!) in the comment preamble and I’m sure it would benefit some folks!

Trying to upload it now. It gives me a “Sorry, I can’t upload that pattern file” box.

I just successfully uploaded another pattern. Am I just trying to upload things too often, or did I break the file somehow? (It seems ok – transfers between Pixelblazes anyway.)

Might have the same ID as an existing pattern, if so you can adjust the ID in the epe file. It’s there to stop accidental double posts.

I guess that was it – changed the ID and it uploaded fine. Thanks, @wizard!