Build a working maze generator using a random walker algorithm with a clean OOP structure in p5js.
Looping animations are everywhere: generative art, shaders, UI animations, game assets, and GIFs.
But creating perfectly looping procedural animations is surprisingly difficult.
A common approach is to animate some parameter over time:
The beginning and end don't match, because procedural randomness is typically not periodic.
The idea to fix this is simple, we need to create periodic randomness. But how do we create randomness that loops perfectly?
To get a feel for how we can implement periodic noise and how it works we will go through an example using perlin noise in p5js. We will generate a simple 2D terrain and create a perfectly looping gif.
The technique shown is applicable to other procedural animation/generation as well (not just perlin noise gifs).
If you are not familiar with perlin noise, you can take a look at this article explaining what perlin-noise is and how we can use it in procedural generation.
Lets start with this bad looping gif of a 2d "map" scrolling from right to left:
You can clearly see the cut where the gif starts from the beginning which is something we don't want.
You can see side by side which section of perlin noise we are using to create the current animation frame and the corresponding frame in fig. 1.2.
The reason why the animation in fig. 1.1 doesn't loop smoothly is because it follows a straight line path through the noise space. Thus the beginning and end of the path are different.
To alleviate this problem we can move the section of noise we work with in a circle instead of a straight line. By moving on a closed path the noise inherently becomes periodic and we won't have any cuts when the animation restarts.
You can see the result below:
Although fig. 1.3 shows that there is no longer an obvious cut when the video restarts, we see another obvious problem. The animation itself has changed. It is no longer scrolling but rather orbiting. We are now scrolling sideways.
To overcome this problem we will take the idea of moving in a circle to three dimensions or in general, if we are using \(n\)-dimensional perlin noise in the original animation we'll now use \(n+1\) dimensions.
So what we'll do is map our flat canvas to the outside of a cylinder in 3d space. By doing so we know there won't be any cuts as we are still going in a circle but the animation will stay the same!
You can think of this as if we were to wrap the canvas in fig. 1.1 into a hollow cylinder.
We can do this by mapping the each point \((x,y)\) to \((x',y',z')\):
$$x'=\sin\left(\frac{x\cdot 2\pi}{U}\right)\cdot R$$ $$y'=y$$ $$z'=\begin{cases} \sqrt{R^2-x'^2} & \text{if $\frac{6\pi}{4} < (\frac{x\cdot 2\pi}{U}$ mod $2\pi) <\frac{2\pi}{4}$} \\ -\sqrt{R^2-x'^2} & \text{otherwise} \end{cases} $$Where \(U\) is the circumference and \(R\) is the radius of the cylinder. By changing the size of the cylinder we can adjust the length of the final animation.
Note that in this example, to stick with the naming of the 2D case, \(y\) represents the height of the cylinder. This might be counter intuitive as we expect \(z\) to represent the height.
As (in p5js at least) \(noise(x)=noise(-x)\) we will offset the center of our cylinder from \(0\). This can be done by adding \(R\) to each coordinate. So, we'll use \(noise(R+x',R+y',R+z')\) instead of \(noise(x,y)\).
You can see the section of noise we are using for the final animation in fig. 1.4 below. However it is hard to visualize 3d noise correctly..
And the final animation without any jumps or cuts:
By sampling higher-dimensional noise along a closed path we can create perfectly looping procedural animations. This technique can be applied to any procedural animation that relies on randomness and is not inherently periodic.
All in all I am quite happy with the results. I think the idea of using an extra dimension is quite neat.
This project (with exceptions) is published under the CC Attribution-ShareAlike 4.0 International License.