Task #9: Here Comes/Goes the Sun

It’s signs of spring here in the Northern Hemisphere… And likely signs of the fall in the Southern…

In honor of the change in daylight hours, let’s do a sunset /sunrise…

There are plenty of rainbow patterns… but what about a sunset? Lush colors, fading away…

Or a sunrise:. From dark to light, peeking over the horizon…

Your mission, here in Task #9 is to do something that rises or sets, and the resulting lightshow.

Maybe you just make a rising sun? A ball of light that glides upwards from the bottom.

That’s the newbie version: Make a circle that rises into the matrix from the bottom upwards

As always, golfing is optional and always fun to see.

1 Like

The sun is coming soon! Here’s a bit of my process along the way…

So, when asked to make a simulated sun, my first thought was, “particle system”. And then, “hmmm, this may require an NVIDIA RTX 4090Ti. I wonder how much particle system I can actually build on a Pixelblaze.”

The pattern below is an attempt to find out. It’s also fun to play with – it’s a 2D n-body gravity simulator. (If you try this and crank the number of particles up, turn the gravity down a bit so all the particles won’t collapse into a personal Pixelblaze black hole.)

Click to Expand Code
/* 2D n-body gravity simulator

 Note: as you'd expect, large numbers of particles at high gravity tend to
 collapse and merge.  Lower the gravity a bit, and they'll fly free again.
 
 Requires a 2D LED array and appropriate pixel mapper.
 
 MIT License
 
 Version  Author        Date      
 1.0.1    JEM(ZRanger1) 04/03/2021
*/ 

// display size - enter the dimensions of your matrix here
var width = 16;
var height = 16;

// Global variables for rendering
var frameBuffer = array(height);  
var frameTimer = 9999;           // accumulator for simulation timer
var clearFn = clearFrameBuffer;  // pointer to function to clear screen between frames

// Global variables for particle system
var _x = 0;   
var _y = 1;
var _dx = 2;
var _dy = 3;
var _hue = 4;

var MAX_PARTICLES = 32;     
export var numParticles = 3;
export var gravity = -8;    // gravitational acceleration constant
export var speed = 25;      // milliseconds between simulation frames
var C = 3.25;               // local speed of light. 
 
// UI
export function sliderGravity(v) {
  gravity = -14.3 * (0.02+v);
}

// overall simulation speed in ms per frame
export function sliderSpeed(v) {
  speed = 150 * (1-v);
}

var last_n = numParticles;
export function sliderParticles(v) {
  numParticles = 1+floor(v * (MAX_PARTICLES-1))
  
// we zero velocity vectors to get a restart when the
// number of particles changes.
  if (last_n != numParticles) {
    initParticles();
    clearFn = clearFrameBuffer;
    frameTime = 9999;
    last_n = numParticles;
  }
}

// we need two particle buffers so we can calculate every particle's
// instantaneous effect on every other particle. Instead of copying,
// we swap pointers on each frame.
var pb1 = array(MAX_PARTICLES);  
var pb2 = array(MAX_PARTICLES);
var particles,work_particles;


function allocateFrameBuffer() {
  for (var i = 0; i < height; i ++) {
    frameBuffer[i] = array(width);
  }
}

// zero entire frame buffer
function clearFrameBuffer() {
  for (var y = 0; y < height; y++) {
    for (var x = 0; x < width; x++) {
      frameBuffer[x][y] = 0;
    }
  } 
  clearFn = eraseParticles;
}

// not used by this version, but later...
function coolFrameBuffer() {
  for (var y = 0; y < height; y++) {
    for (var x = 0; x < width; x++) {
      frameBuffer[x][y] = max(0,frameBuffer[x][y] - 0.05);
    }
  } 
}

// erase only the dots we drew last frame.  Much faster than
// full clear.
function eraseParticles() {
  for (var i = 0;i < numParticles; i++) {
    frameBuffer[particles[i][_x]][particles[i][_y]] = 0;
  }
}

// allocate memory for particle tables
function allocateParticleLists() {
  for (var i = 0; i < MAX_PARTICLES; i ++) {
    pb1[i] = array(5);
    pb2[i] = array(5); 
  }
}

// set particles to random positions, 0 initial velocity
// all movement is generated by gravity!
function initParticles() {
  particles = pb1;
  work_particles = pb2;
  var hue = 0.001;
  
  for (var i = 0; i < MAX_PARTICLES; i ++) {
    particles[i][_x] = random(width);
    particles[i][_y] = random(height);
    particles[i][_dx] = 0;
    particles[i][_dy] = 0;
    particles[i][_hue] = hue;
    hue = (hue + 0.27) % 1
  }  
}

function swapParticleBuffers()  {
  var tmp = work_particles;
  work_particles = particles;
  particles = tmp;
}

function moveParticles() {
  for (var i = 0; i < numParticles; i++) {
    accel_x = 0;
    accel_y = 0;
    for (var j = 0; j < numParticles; j++) {   
      if (i == j) {
        continue;  // you aren't moved by your own gravity
      }
      // calculate effect of gravity between two particles
      // force of gravity decreases with distance, except that
      // "escape velocity" isn't possible, so particles can't just walk
      // out of our simulation. All particles are considered to have
      // a mass of 1.
      var dx = (particles[i][_x] - particles[j][_x]);
      var dy = (particles[i][_y] - particles[j][_y]);
       
      var r = sqrt(dx*dx + dy*dy);
      var f = (r > 1) ? gravity / r * r : gravity;
      accel_x += f * dx / r;   
      accel_y += f * dy / r;
    }
    // calculate new velocity and limit it to C - the local speed of light
    work_particles[i][_dx] = clamp(particles[i][_dx] + (accel_x / 100),-C,C);
    work_particles[i][_dy] = clamp(particles[i][_dy] + (accel_y / 100),-C,C); 

    work_particles[i][_hue] = particles[i][_hue];

    // calculate new location based on velocity, and wrap around
    // the edges of our display.  Note that a wrapping universe
    // makes things a little weird!
    work_particles[i][_x] = particles[i][_x] + work_particles[i][_dx];
    if (work_particles[i][_x] < 0) { work_particles[i][_x] = width - 1; }
    else if ((work_particles[i][_x] >= width)) { work_particles[i][_x] = 0; }
    
    work_particles[i][_y] = particles[i][_y] + work_particles[i][_dy];
    if (work_particles[i][_y] < 0) { work_particles[i][_y] = height - 1; }
    else if ((work_particles[i][_y] >= height)) { work_particles[i][_y] = 0; }

    // draw the particle into our frame buffer    
    frameBuffer[work_particles[i][_x]][work_particles[i][_y]] = particles[i][_hue];
  }
  swapParticleBuffers();
}

// Initialization
allocateFrameBuffer();
allocateParticleLists();
initParticles();

export function beforeRender(delta) {
  frameTimer += delta;
  
  if (frameTimer > speed) {
    clearFn();
    moveParticles();
    frameTimer = 0;
  }
}

export function render2D(index, x, y) {
  var v = frameBuffer[(x * 16)][(y * 16)];
  hsv(v, 1, (v > 0) * 0.6);
}

That’s really funny cause I was thinking about doing a particle system for PB today, and got busy with other things.

Great minds think alike, you know… I’m trying to knock a few things off my to-do list before I get my second COVID shot tomorrow so I can relax and enjoy the side effects.

It’s pretty surprising how much computation you can do if you structure it carefully. That pattern will calculate motion for 32 particles, each influencing all the others each other, on a PB2 at 16fps with the simulation running full speed. Slow it down a little so you can watch, and the frame rate goes back up really quickly. A PB3 could easily drive more particles. Things are looking pretty good for the corona and solar flare portion of my sun!

Here’s the code for my sunrise. I’ve just uploaded it to the library as well. The sun rises when you load the pattern, and when you move the “Make the Sun Rise” slider. Once up, the sun is well… having a pretty active year. Another good challenge – this was fun to think about and fun to make!

/* 2D Sunrise/solar activity simulator

 Move the slider to see the sunrise again!

 Requires a 2D LED array and appropriate pixel mapper.
 
 MIT License
 
 Version  Author        Date      
 1.0.0    JEM(ZRanger1) 04/04/2021
*/ 

// display size - enter the dimensions of your matrix here
var width = 16;
var height = 16;
var centerX = (width-1) / 2;
var centerY = (width-1) / 2;

// Global variables for rendering
var sunDiameter = 5.79
var sunMask = array(pixelCount);  // holds precalculated brightness data for sun
var frameBuffer = array(height);  // main drawing surface
var frameTimer = 9999;            // accumulator for simulation timer

// Indices for particle data array
var _x = 0;   
var _y = 1;
var _dx = 2;
var _dy = 3;
var _hue = 4;

// particle system parameters
var MAX_PARTICLES = 32;     
var numParticles = 24;
var gravity = -155;       // gravitational acceleration constant
var drift;                // allows the center of gravity to "wobble" slightly
var sunRise = (height - 1);
var speed = 80;           // milliseconds between simulation frames
var C = 3.25;             // local speed of light. 
var r1,r2,t1;             // timers and waves for animation
 
// we need two particle buffers so we can calculate every particle's
// instantaneous effect on every other particle. Instead of copying,
// we swap pointers on each frame.
var pb1 = array(MAX_PARTICLES);  
var pb2 = array(MAX_PARTICLES);
var particles,work_particles;

// we "stage manage" the sunrise by switching between several
// different beforeRender functions during the run.
var preRender = doSunrise;

// Moving this slider makes the sun rise!
export function sliderMakeTheSunRise(v) {
  preRender = doFadeout;
}

// precompute brightness mask for the sun.  This
// saves us from having to compute distance for all
// the sun's pixels on every frame
function initSunMask() {
  for (var i = 0; i < pixelCount; i++) {
    var x = i % width;
    var y = floor(i / width);
    x = centerX - x; y = centerY - y;
    var dx = sqrt(x*x + y*y);
    dx = (dx < sunDiameter) * (1-(dx / sunDiameter)); 
    sunMask[i] = dx;
  }
}

// draws the sun on the frame buffer, modeling surface activity
// with an additive sine wave plasma.  This function also
// manages "sunrise" by allowing you to offset the sun's
// position in the Y direction.
function renderSunMask() {
  for (var y1 = 0;y1 < height; y1++) {
    if ((y1+sunRise) >= height) break;    
    for (var x1 = 0; x1 < width; x1++) {
      var v2 = sunMask[x1 + (y1 * width)];
      if (v2) {
        var x = x1 / width; var y = y1 / height;
        var v1 = wave(x*r1 + y) + triangle(x - y*r2) + wave(v2);
        v1 = v1/3; 
        v1 = (v1*v1*v1);   
        frameBuffer[x1][y1+sunRise] = floor((0.2*v1) * 1000) + (v2*v1*4);
      }
    }
  }
}

function allocateFrameBuffer() {
  for (var i = 0; i < height; i ++) {
    frameBuffer[i] = array(width);
  }
}

// gradually reduce brightness of all colored pixels
function coolFrameBuffer() {
  for (var y = 0; y < height; y++) {
    for (var x = 0; x < width; x++) {
      var n = frameBuffer[x][y] % 1;
      if (!n) continue;      
      frameBuffer[x][y] = floor(frameBuffer[x][y])+max(0,n - 0.081);
    }
  } 
}

// allocate memory for particle tables
function allocateParticleLists() {
  for (var i = 0; i < MAX_PARTICLES; i ++) {
    pb1[i] = array(5);
    pb2[i] = array(5); 
  }
}

// set particles to random positions, 0 initial velocity
// all movement is generated by gravity!
function initParticles() {
  particles = pb1;
  work_particles = pb2;
  var sunRadius = -sunDiameter / 2

  for (var i = 0; i < MAX_PARTICLES; i ++) {
    particles[i][_x] = centerX + sunRadius + random(sunDiameter);
    particles[i][_y] = centerY + sunRadius + random(sunDiameter);
    particles[i][_dx] = 0;
    particles[i][_dy] = 0;
    particles[i][_hue] = random(0.06);
  }  
}

function swapParticleBuffers()  {
  var tmp = work_particles;
  work_particles = particles;
  particles = tmp;
}

// calculate new position and acceleration for each "solar flare" particle
// and draw them in the frame buffer.  In this particle system, only
// the sun's gravity matters.  We move the center of gravity around
// a little over time to keep things stirred up.
function moveParticles() {
  drift = random(2) - 1;    
  
  for (var i = 0; i < numParticles; i++) {
    var dx = (particles[i][_x] - (centerX+drift));
    var dy = (particles[i][_y] - (centerY+drift));

    // compute force of gravity based on distance from the center of the sun       
    r = sqrt(dx*dx + dy*dy);
    var f = (r > 1) ? gravity / r * r : gravity;
    accel_x = f * dx / r;   
    accel_y = f * dy / r;

    // calculate new velocity and limit it to C - the local speed of light
    work_particles[i][_dx] = clamp(particles[i][_dx] + (accel_x / 100),-C,C);
    work_particles[i][_dy] = clamp(particles[i][_dy] + (accel_y / 100),-C,C); 

    work_particles[i][_x] = particles[i][_x] + work_particles[i][_dx];
    work_particles[i][_y] = particles[i][_y] + work_particles[i][_dy];

    // clip to our drawing area  
    if ((work_particles[i][_x] < 0) || (work_particles[i][_x] >= width)) continue;
    if ((work_particles[i][_y] < 0) || (work_particles[i][_y] >= height)) continue;    
    
    // draw in the frame buffer, limiting the drawing area and matching brightness
    // so it looks like flares are erupting fro the surface.
    if (r >= (sunDiameter * 0.9)) {
      work_particles[i][_hue] = particles[i][_hue]; 
      var bri = frameBuffer[work_particles[i][_x]][work_particles[i][_y]] % 1;
      bri = (bri) ? bri : 0.5
      frameBuffer[work_particles[i][_x]][work_particles[i][_y]] = (floor((work_particles[i][_hue]+t1) * 1000)) + bri;
    }
  }
  swapParticleBuffers();
}

// Initialization
allocateFrameBuffer();
allocateParticleLists();
initSunMask();
initParticles();

// prerender function that fades the screen to black
// over a period of time, then triggers a sunrise
// and switches back to the normal beforeRender function.
var fadeTime = 0;
function doFadeout(delta) {
  frameTimer += delta;
  fadeTime += delta

  if (frameTimer > speed) {
    coolFrameBuffer();
    frameTimer = 0;
  }
  if (fadeTime > 1500) {
    fadeTime = 0;
    sunRise = (height - 1);
    preRender = doSunrise;
  }
}

// prerender function that implements the rising sun
// and pauses for a moment before starting full
// activity.
var sunrisePause = 0;
function doSunrise(delta) {
  frameTimer += delta;
  
  sunRise = max(0,sunRise - (delta * 0.007)); 
  if (sunRise == 0) sunrisePause += delta;
  
  r1 = wave(time(0.064));
  r2 = wave(time(0.035));
  t1 = 0.05 * time(0.08);
  
  if (frameTimer > speed) {
    coolFrameBuffer();
    renderSunMask();
    frameTimer = 0;
  }  
  if (sunrisePause > 2500) {
    sunrisePause = 0;
    preRender = doActiveSun;
  }
}

// prerender function for the sun in "normal" mode with
// sunspots, surface activity, solar flares and whatnot
function doActiveSun(delta) {
  frameTimer += delta;
  
  r1 = wave(time(0.064));
  r2 = wave(time(0.035));
  t1 = 0.05 * time(0.08);
  
  if (frameTimer > speed) {
    coolFrameBuffer();
    renderSunMask();
    moveParticles();
    frameTimer = 0;
  }  
}

export function beforeRender(delta) {
  preRender(delta);
}

export function render2D(index, x, y) {
  var x1,x2,v,h;
  x1 = floor(x * width) ; y1 = floor(y * height);
  v = frameBuffer[x1][y1];
  h = floor(v) / 1000;
  v = v % 1;
  hsv(h, 1, v);
}
2 Likes

I’m thinking of removing the gravity/etc and streamlining your particle system code, making it more neutral, so adding other things besides gravity could be done with it. Need to find some time to play with it.

Go for it! This code exists to be played with and adapted for other uses. A more complete “normal” particle system would be really fun.

I was thinking last night, if we can do n-body gravity, we’re like 90% of the way to boids! And lots of other interesting things as well.

1 Like

Yeah, boids and many other fun toys

Here’s my first completely solo pattern, for the sunrise challenge.
While I initially intended it to be a 2D pattern as I am using it on lights around a bed, I liked the look of a 1D pattern, across the 0-1 world of the render2D pattern. I’m sure there is a more logical way of organizing the render2D/render3D sections.

// my first fully independent pattern, for the sunrise challenge.
// there are 3 different main variables, 
// 1)  the curve for the increasing brightness, which uses the Spread (essentially the final total width of bright pixels)
// and the Sharpness, which is the focal nature of initial bright pixels.  This is a function of time.  Currently time loops, but modifying the t1 variable could allow 
// for a run-once type scenario
// 2)  the hue, set to hueSun for within the sun, and hueEdges for the edges, and between the two uses a coarse gradient between the two.  This works for yellow-red, as
// they are neighbours on the HSV color spectrum, however a more sophistocated gradient formula would be needed for non-neighbour colors.
// 3)  the sun radius, increases over time, and then the center of this is desaturated to make it white

// for all of this example, the sun rises centred on the y-axis.  This could be easily changed in the render2D function.
// While my physical implementation is a 3D model, I found the 2D rendering works quite nicely as is, and wouldn't likely benefit from the 3rd dimension

export var hueSun = .10, hueEdges = 0.01   //0.05, 0.001
export var sharpness = 2, spread = 10
var location = 0.5 // the centre of the sunrise
export var t1  // main time cycling variable, from 0 to 1
export var sunRadius = 0
var rimToWhite = 0.1

//make a sharpness UI slider to spread the color across the whole strip or sharpen to a spot
export function sliderSpread(v) {
  spread = v * 10 // give a range of 0-10
}

// make a function for how sharp the initial pinpoint of sunrise will be, range 0-3
export function sliderSharpness(v) {
	sharpness = v * 3
}


// over-simplified, given that the HSV colorspace almost allows for a reasonable sunset color gradient.  Ideally, this would be a proper gradient calculation
// this will be fed a value from 0 to 0.5.  at 0, it should return hueSun ,and at 0.5 (or more, error correction), it should return hueEdges
function sunsetGradient(distFromSun) {
  var returnHue = hueSun
  if (distFromSun <=0) returnHue = hueSun
  else if (distFromSun >=0.4) returnHue = hueEdges
  else {
    // safely have a value of cDist between 0 and 0.4.  calculate slope of higher-lower/horizontal, plus lower, to give line between higher and lower, over the given horizontal distance.
	  returnHue = ((hueEdges-hueSun)/0.4)*distFromSun + hueSun
  }
  return returnHue
}

// time loops every 65.54*interval seconds, and interval 0.015 = 1 second apprx.
// the clamp formula with time causes the sun size growth to lag 0.1 time unit behind the increase in brightness
export function beforeRender(delta) {
  t1 = time(0.5)
  sunRadius = (clamp(t1-0.1,0,1))*0.35
}

export function render2D(index, x, y) {
   var cDist = abs(location-y)
   var distFromSun = clamp((cDist-sunRadius),0,1)
   var sat
   var hue
 // y=-2*(0.5*x)^2 + x
   var va = - spread * pow(((1/spread)*cDist), sharpness) + (t1)
   va = clamp(va,0,1) //clamp the value between zero and one
 
    // calculate white (desaturated) centre of sun
    if (cDist<(sunRadius-rimToWhite)) {
      // from cDist=0 to cDist=sunRadius-rimToWhite, gradient between 0.5 and 1  sat=(1/(radius-rimToWhite))*cDist
      sat=((1-0.5)/(sunRadius-rimToWhite))*cDist + 0.5
    }
    else sat = 1
    
    // calculate gradient,as a functin of the distance from the sun centre.
    hue = sunsetGradient(distFromSun)
    
    hsv(hue, sat , va)
}

export function render3D(index, x, y, z) {
   render2D(index,x,y)
}

edit: fixed my math on the value (brightness) equation.
I would still sort of like the first color to be the edge (red) color, then fade into the yellow, then progress as already coded, but that’s a problem for another day.

3 Likes

@bdm! Ben!! This is a monster of a first pattern from scratch!

Great job! I like it so much that I’ll submit a tweak of yours as my opinionated submission; hopefully there’s some good tricks to pick up along the way.

var hueSun = .10, hueEdges = 0.01
var sharpness = 3, spread = .8
var location = 0.5 // the centre of the sunrise
export var t1  // main time cycling variable, from 0 to 1
var sunRadius = 0.5
var rimToWhite = 0.01

// over-simplified, given that the HSV colorspace almost allows for a reasonable sunset color gradient.  Ideally, this would be a proper gradient calculation
// this will be fed a value from 0 to 0.5.  at 0, it should return hueSun ,and at 0.5 (or more, error correction), it should return hueEdges
function sunsetGradient(distFromSun) {
  var returnHue = hueSun
  if (distFromSun <=0) returnHue = hueSun
  else if (distFromSun >=0.4) returnHue = hueEdges
  else {
    // safely have a value of cDist between 0 and 0.4.  calculate slope of higher-lower/horizontal, plus lower, to give line between higher and lower, over the given horizontal distance.
	  returnHue = ((hueEdges-hueSun)/0.4)*distFromSun + hueSun
  }
  return returnHue
}

// Only here for the v2
function hypot(a, b) { return sqrt(a * a + b * b) }

//https://www.gizma.com/easing/
function easeOutQuad(t, b, c, d) {
	t /= d
	return -c * t * (t-2) + b
}

// time loops every 65.54*interval seconds, and interval 0.015 = 1 second apprx.
export function beforeRender(delta) {
  t0 = clamp(1.5 * time(0.2) - .25, 0, 1)
  t1 = easeOutQuad(t0, 0, 1, 1)
  sunRadius = .3 + t0 / 3
}

export function render2D(index, x, y) {
  x -= .5; y = 4 - y - t1
  var cDist = hypot(x, y - 2 * t1)
  var distFromSun = clamp((cDist-sunRadius)/2,0, 1)
  var sat, hue
 
  var va = spread * pow((1/spread)-cDist, sharpness) + t1
  va = clamp(va, 0, .5) //clamp the value
 
  // calculate white (desaturated) centre of sun
  sat = (cDist<(sunRadius-rimToWhite)) ?.5/(sunRadius-rimToWhite)*cDist + .5 : 1
    
  // calculate gradient,as a functin of the distance from the sun centre.
  hue = sunsetGradient(distFromSun)
    
  hsv(hue, sqrt(sat), va * va * va)
}

export function render3D(index, x, y, z) {
   render2D(index,x,y)
}
export function render(index) {
   render2D(index, .5, index / pixelCount)
}
1 Like

@bdm, I love the range of colors – it looks very much like the spring/summer sun out here in the desert!