Using beforeRender to generate an array of HSV values for supersampling

I see, so you are effectively moving all the pixels in your canvas over for each frame, then painting a new sine wave in the newest column.

Many ways to do this specific thing. If you generally want a scrolling 2D canvas like that, you can certainly do it. Nothing you said would make me thing you need a supersampled canvas though. If you did but still use nearest neighbor, you will get animation aliasing. You wouldn’t need Y supersampling if you paint your sine wave in a way that is anti-aliased. You could draw in 2 pixels whenever it doesn’t land on a whole number pixel index, relative to the strength. So for a value of 4.25 you would draw 75% in index 4, and 25% in index 5.

Three methods come to mind, you could move every pixel over left of right, copying pixels, and always paint in the left or rightmost column.

You could also implement a circular buffer, where you have an index pointing to the current “head” column, do your drawing there. To move, you just increase the head (wrapping if needed), and paint a new sine wave there. This has the advantage that you don’t need to move any pixels at all, just a single variable that points to the head. Rendering likewise starts at the head (or ends there depending on which direction you want it to move).

The more mathy version would be to store only the sine wave value/sample, instead of pixels, and render that to pixels in the render function. You could combine this with a circular buffer for speed.

Thanks! That gives me a lot to think about. The circle buffer is what I was planning originally for the code I posted in this thread, since it seemed like it would keep the speed of beforeRender relatively fast to not actually have to go through the whole array. Glad to know it will at least work in theory. I need to spend some more time thinking about the math of the whole thing and looking at other patterns.

I think I just like when the LEDs look like they’re holes with a more complex pattern behind them shining through as it moves under them, and that’s the look I’m trying to achieve.

1 Like

Again, the bubble code did something similar, moving most of the array upwards for a scrolling effect.

Using the new array functions (mutate and so on) is going to be faster than a loop, if only because it allows you use optimized loop code to iterate thru. So to make a scrolling array, you’d copy all elements in the right direction, and then set the edge ones as desired.

1 Like

I think you get this effect with may math based approaches using gradients. You hit aliasing issues once you start working in whole number pixels, quantized samples, etc. Unless aliasing is part of the desired look of course!

That looks like its a 2x vertical resolution?

1 Like

Yeah, so I could build a bubble “offscreen” and float it upwards. Seemed like the easiest method for the effect I wanted.

I think I realized that this is much, much easier than I’m making it out to be, as long as the overall pattern I want is a loop. I can just adjust X over time to act as a window into a much larger pattern, without wasting time rendering pixels that won’t be used unless they are the nearest neighbor, or RAM on an array. Not sure why it was so hard for me to realize that was possible.

I’m guessing that’s along the lines of what you meant by:

Even if it’s not at all along the same lines as what you meant, trying to figure out what exactly that means and how to do it lead me to this:

export function beforeRender(delta) {
  xmax = 8 //size of the actual x-side of LED array
  t1 = time(.2)
  
}

export function render2D(index, x, y) {
  v = 0
  x = x/50+(t1*(xmax-1)/(xmax-1)) // 1/(xmax-1) gives the separation between x values in the pixelmap, so this will make each full screen of real pixels = 1/xmax of the total length of the repeating pattern
  gauss = wave(x) // create a sine wave with a single peak over the entire length of the pattern
  detrend = wave(1-x) // create an offset  wave to keep the line centered
  y2 = wave(x*30) //draw a sine wave with frequency of 30 per full pattern
  y2 = y2-y2*gauss+0.5*gauss // when gauss = make amplitude of y2 inverse to gauss
  h = x //helps visualize the looping point
  if (abs(y2 - y) < .1){ // if distance of y from y2 is less than threshold
    v = 1-abs(y2 - y) // set v = value that is proportional to distance.
  }
  s = 1
  hsv(x, s, v*v)
}
4 Likes

Small tweak that will speed it up a wee bit:

Move (t1*(xmax-1)/(xmax-1)) into beforeRender
The value is consistent over the whole frame, you don’t need to keep recalcing it every pixel.
That right there will reduce math greatly

Coming back, cause I just noticed that math is bogus anyway, it’s always equal to t1

Uh, or just use t1 since (xmax-1)/(xmax-1) == 1 :yum:

1 Like

I am not a software guru neither a computer graphics. I am just EE.
So, from the EE (HW) point of view FPS is very depend on LEDs data rate and number of
serially connected LEDs. For example, WS2812b bit rate is 1.25mkS and each LED
requires 24 bits. So, each LED Frame (Pixel Frame) takes 30mkS.
This means per pixel timing for all math in the render is limited to this 30mkS time frame.
My good guess, in many cases (but not all) this should be OK. But if math takes
longer than 30mkS at least one pixel will be skipped and most likely will be Off.
Assuming all per pixel math is faster than 30mkS for the strip with 300 LEDs the
entire Strip Frame will be 9.5mS (min 0.5mS is required for the Strip Reset between
frames). The resulting max FPS will be no faster than 105.3
And this is physical limitation for the 300 WS2812b LEDs.

If all math is done in “beforeRender” function it implies necessity for the Frame Buffer
and it may (and will) have impact on max FPS. Frame Timing will be 9.5mS plus whatever
it takes for all calculations.
However this (buffered) approach will guaranty processing for all pixels.
Many things (I guess, even scrolling) could be done just by simply manipulating
array index(es) which is fast.

I have no idea how tasks are split between 2 cores but if one core is used
just for supporting all HW functions, second one could be entirely dedicated
for processing Frame Buffer. In this case 9.5mS should be enough for
relatively complex animation.

In addition to my eyes timing control (i.e. FPS) is much easier with function like this:

//
// ***** Frame Rate Timing *****
//

elapsedMs = 0

export function beforeRender(delta)
{
  elapsedMs = elapsedMs + delta
   
  if (elapsedMs > frameRate)
  {
    elapsedMs = 0
    updatePixelArray()
  }
}

This is nothing more than my thought from HW point of view.

If the slope is > 1, there could be more than 2 pixels. So you might want to calculate sin() between the pixel columns, draw a line between left and right, and then light each pixel according to the length of the line segment in it. Or you could do the integral …

Just to be clear: if you do t1=time(whatever), or wave(time(whatever)) or any other thing like this,
The value won’t change until the next frame, regardless of how long the render takes. Doesn’t matter where you do it, in render() or otherwise. Doing it in beforeRender() is where it will change once (per frame). Doing it in render(), it’s still the same result, no matter whether you are looking at pixel #1 or pixel #3000, and the loop between them takes a long time to run

1 Like

I just read the topic again: Using beforeRender to generate an array of HSV values for supersampling and I thought I should mention that supersampling H doesn’t really make sense since it’s a circular value. I fill my framebuffer with RGB (possibly using the HSVtoRGB fn you’ll see in my patterns).

2 Likes

I’ve definitely come to the conclusion by now that this entire idea was both bad in general, and not necessary. However, I’m still glad I at least got it to work in some way. A changing pattern that was generated in beforeRender scrolled across the pixels at 14 whole fps!. When I started working on it I knew it was probably going to end up being completely scrapped. It was just a fun goal to keep me engaged in “sitting in front of my screen with my eyes closed trying to imagine what the hell I’m doing” programming.

2 Likes

Most certainly worthwhile! I usually do that away from the computer, then I just sit down and write the code. For example the Sierpinksi pattern took me a week of thinking about it while falling asleep and a couple of hours of coding.

1 Like

Correct me if I am wrong, but this is true only if Time Function is gated with whatever pixel number x.
Something like this:

if (x == index)
{
  t1=time(whatever)
}

Otherwise t1 will be called once per each pixel, not once per frame.
But you are correct, whatever is in the “beforeRender” called only once per frame.

Incorrect. The value of time(whatever) won’t change inside of any given frame.

It’s a static value while render happens.

So time(.15) is always in the same spot of that cycle, no matter when you call it inside render()

Yes, time(.3) is a different value result, but it also won’t change.

I am pretty sure the documentation on time() says this, but if not, we should make it clearer.

Here’s a bit from a previous post.

1 Like

This must be true if Timer runs (much) slower than render.
time(.1) loop takes ~6.5S or 6500mS (I am guessing the exact loop time is 6553.5mS)
For the 300 LEDs max render loop is ~9.5mS I.e. render will run near 700 times per
one Timer loop. This way time(x) value will be (about) the same for a single given
render cycle. But this time will slowly drift because render loop and timer loop are
not synchronized.
I am guessing the expected values for the time(x) function is 1/65535 to 1
Am I correct? (16-bit physical counter with 1KHz reference clock is a related HW).
So, what happens with a nice looking pattern if timer argument is 1/65535?
In this case timer loop will be 1mS which is about 10 times faster than render loop
for the same 300 LEDs strip. And in this case it will be a difference from where
time(x) function is running (beforeRender or inside render).

Please don’t take me wrong.
I am trying to come up with some sort of universal approach how to smoothly control
timing for the running pattern.
I played with few preloaded patterns and quickly figured out how easy pattern
could be broken just by very minor adjustment for the timing variables.
I clearly understand, buffered approach has a limitation for the max FPS but from
the other side provides a very easy timing control with a single variable and pixels
manipulation with arrays. And of course, assuming the PB with LEDs is not a
substitution for the HD TV.

@Vitaliy , see my previous post, link to my comment in another topic.

The time() calls work off a snapshot taken once per animation frame. This has many advantages.

There’s no difference between a call in before render or the last pixel in render (for the same interval parameter), no matter how long rendering takes.

Values larger than 1 are just fine. The maximum interval is something like 24 days.

The time() function is based on milliseconds, so things start to get poorly quantized with extremely short intervals.

For POV with very high frame rates (like 1000) using delta provided in beforeRender will yield better results with resolution down to a clock cycle.

2 Likes