qr code

BIT-101

Bill Gates touched my MacBook Pro

Looping Noise


This is a story. A story of laziness and stupidity. But also a story of redemption and victory.

It’s about noise. Like Perlin noise or Simplex noise. And making it move. But making it move in a specific way. It’s about the problem, the crappy solution I lived with for too long, the eventual hack I figured out, and finally, the real solution which I knew was there all along but for some reason never implemented. And some other roads worth following in the future.

Setting the stage

So you want to animate some noise. It doesn’t really matter what you’re doing with that noise or how you’re rendering it. For something to work with, let’s just render some noise on a square surface. We’ll get the value of the noise at each pixel location and convert that to a grayscale value and color the pixel using that. I’m gonna go with some pseudocode in this post because I don’t want to get caught up in language details.

for x = 0 to width {
  for y = 0 to height {
    n = noise2(x * scale, y * scale)
    n = map(n, -1, 1, 0, 1)
    setPixelGray(x, y, n)
  }
}

Generally you want to scale your pixel values down a lot lower before feeding them into your noise function. Very low values give smoothly changing values, while high values look more chaotic.

The pseudocode noise2 function is 2d noise - generally Perlin or Simplex.

I’m assuming your noise functions return values from -1 to 1, and you have a function that will set a pixel to a grayscale value from 0 to 1, so the map function converts between the two. If you want to learn more about map, read this.

Again, your language and platform and libraries may do something different, but I trust you can work it out. A possible result from the above:

2d simplex noise in grayscale

Animating it

OK, now we want to make it move!

We could move it on the x-axis or the y-axis like so…

n = noise2((x + offset) * scale, y * scale)
// or
n = noise2(x * scale, (y + offset) * scale)

Here, I’m considering that offset is some value that is changing over the course of the animation. Maybe on frame 1 it’s 0 and on the last frame it’s 400. This is going to give you something like this:

Not particularly exciting. Yes, we are animating the noise, but we’re not really changing what the noise looks like, we’re just panning across a noise field. Also, when it gets to the end it jumps back to the beginning. Nothing more I hate than a non-looping short animation. Well, there a few things I hate more, but it’s up there.

Into higher dimensions!

OK, not higher higher dimensions, but higher than two. Three for now.

If we use three-dimensional noise, rather than a flat plane of noise, we have a solid space of noise. It’s infinite in size, but you can imagine it as a cube. We can move through this cube on the x, y or z axes. At any given point, we’ll be looking at one slice of the noise at a given z value. It’s a bit like doing a CT scan or MRI or ultrasound on that noise cube. And if you change the z value over time, you can animate those slices and the noise morphs around into different shapes.

So we’re looking at something like this:

n = noise3(x * scale, y * scale, z)

We could scale z here too, but in this case we’re not stuck with pixel coordinates, so there’s no real need to do that. We can just have z interpolate between whatever values we want. For example, z might be 0.0 on the first frame, and 1.0 on the last frame.

Here’s what this looks like:

OK, that’s pretty cool, but it certainly doesn’t loop. It goes from one z value to another and when the animation ends it jumps back to the start and plays again. So we need a smooth transition back around to the starting point.

So here’s the crappy solution I’ve been using for years. [shame…]

Diligence overcomes difficulties; sloth makes them - Ben Franklin

I animate the z value from value A to value B (say 0 to 1) for the first half of the animation, and then back to 0 again for the second half. If we do this linearly, it looks like this:

A bit better. It loops, but not very smoothly. It moves along at a linear pace then hits the end and bounces back the other way. So linear is not good - and not what I actually used. Just showing the process step by step.

Imagining you have a normalized value t that is 0.0 on the first frame and 1.0 on the last one (technically just shy of 1.0 but that’s a story for another day). You create your noise like so:

n = noise3(x * scale, y * scale, sin(t * tau) * offset)

Multipling t by tau (2PI) gives us an angle. Take the sine of that and multiply it by an offset value - how far we want it to move back and forth in the z-axis. 1.0 will do fine for starters.

This is pretty nice… way better than the linear version. No jarring jumps at least. It kind of coasts slowly to a stop before reversing. I did things like this for years. And at some point I started hating it.

What I want is something that just continues to morph the noise’s shape, but magically ends up exactly back where it started.

The problem is we’re just using three dimensions, taking 2D slices, sliding down through the cube and back up. So the reverse path is exactly the same as the forward path. We need another dimension to get more movement. Then you could loop through those two dimensions in a circle or ellipse and have a unique path that arrives back where it started.

I’d like to borrow a dimension please

At first I thought, “Oh, I can just borrow the x or y dimension and make it do double duty. And you kind of can. The code would be something like this…

x = x * scale
y = y * scale
n := noise3(x, y + cos(t * tau) * offset, sin(t * tau) * offset)

Here we pre-multiply x and y by scale only to make the longer line a bit shorter.

Again, we have t * tau to get an angle, but now we take take the cosine and sine of that, times an offset. That gives us a unique y and z value that will follow a circular path and end up back at the start. Sounds good, but here’s what it looks like:

It’s not super satisfying. It’s a bit of the original panning combined with the 3D slice scanning. It’s still got that move through to a certain point, then reverse feel to it. Not as bad, but not great. So borrowing a dimension is not going to work.

So a minute ago I said we need another dimension. That right there is the solution. We just need 4D noise. It exists. But I didn’t have it in my library. And I didn’t want to dig up a version of it and port it to my language of choice (Go). So rather than do that…

The Hack

Learning nothing, I steadfastly tried another way to get around 4D noise. And I’m kind of ashamed to say, it worked out pretty damn well.

My thoughts: What if I take the z-slices in one area of the noise field using sine, and then take z-slices from another area using cosine? Then average the two of them. Since the movement is out of phase, one would be moving down quickly while the other was slowing down, and the first would be still moving down while the other was moving back up.

Or, another way of saying it is: rather than adding a dimension or borrowing one, we’re just using a different part of the noise field and pretending it’s another dimension. But this gives us two different slices, which we just average together.

n1 = noise3(x * scale, y * scale, sin(t * tau) * offset)
n2 = noise3(x * scale + 100, y * scale + 100, cos(t * tau) * offset)
n = (n1 + n2) / 2

And here’s what that looks like:

Honestly, that’s not too bad. Animation-wise, it’s exactly what I wanted. I did find that the averaging wound up kind of washing things out and reducing contrast. In other words, averaging is going to result in somewhat of a bell curve. Fewer darks, fewer lights, more grays. So I used another small hack after the above to increase the contrast:

n = map(n, -0.8, 0.8, 0.0, 1.0)

Due to the averaging, more values will wind up in between -0.8 and 0.8. So we just map value in that range to 0, 1 in the map function we were already using. It makes the darks darker and the lights lighter. You can play around with that value to get different levels of contrast. But beware, too much and your darks and whites get overblown.

Here’s a corrected version:

I was pretty proud of this. And also rather ashamed at the same time. If you’re on a platform where you just can’t get a 4D noise function, I think this might be a viable solution. But it was time to put on my big boy boots and do it right.

Just do it

Yup. I ported a Simplex 4D function to Go.

It wasn’t all that hard. I should have done it years ago. I actually re-ported all four dimensions from this C code into Go. All told it took about an hour. And I’ve been loving 4D noise for the past week.

To make smoothly looping 2D noise using 4D noise, you use two of the dimensions for your rendering and the other two to loop in a “circle”. Something like this:

n = noise4(x * scale, y * scale, cos(t * tau) * offset, sin(t * tau) * offset)

I’m using the x and y dimesions for my x and y values and doing the looping with z and w.

Here’s an example of rendering that:

Other examples

blah

Resources

I did a lot of research on this while I was trying to figure out a way to do this without using 4D noise. There may still be a valid way to do it, but I didn’t find one using regular noise functions. A lot of Reddit and StackOverflow answers merely talk about smoothly animating 1D noise using 2D noise. But they rarely express it in those terms, just like, “Yeah, it’s easy to have looping noise, just use 2D noise with polar coordinates and that will smoothly loop the resulting value.” Yes, the 1D value.

Daniel Shiffman has a nice video on this topic, where he explains more in depth about, again, looping 1D noise using 2D. He gets into animating 2D noise using 3D noise, but never gets to the point of looping the 2D noise. Still, it’s a very well explained video, which might clarify some of the points I brushed over here.

– link to video

He also mentions

Summary

« Previous Post
Next Post »

Comments? Best way to shout at me is on Mastodon

Or share this post directly on Mastodon