It is interesting to experiment with these noise functions. I played around with them and did some tests.
Before getting into what I found, I want to point out that
- this took me at least 10 tries to get right
- half of this wasn’t obvious to me and I had to google a bunch of stuff
- along the way I discovered a bug in the way py5 currently uses its noise library
- the results surprised me also
To be most efficient I wanted to do this without for loops. Generally, Python performs best when you use what are called vectorized operations. A vectorized operation lets you apply an operation to all of the numbers in an array at the same time. Of course, somewhere there is looping happening; the difference is that the vectorized operation will do the loop in optimized C code instead of Python.
Here’s what I came up with:
import numpy as np
from PIL import Image
import noise
import vnoise
OpenSimplex2S = JClass('py5.noise.OpenSimplex2S')
w, h = 1200, 800
vector_noise = vnoise.Noise()
xgrid, ygrid = np.meshgrid(np.linspace(0, 12 // 3, num=w // 3, dtype=np.float32), np.linspace(0, 12, num=h, dtype=np.float32))
noise_array = np.full((h, w // 3, 3), 255, dtype=np.uint8)
noise_array2 = noise_array.copy()
noise_array3 = noise_array.copy()
def setup():
size(w, h)
global open_simplex
open_simplex = OpenSimplex2S(millis())
def draw():
# noise library, which is what py5 currently uses, but I am calling it directly because of a bug I found writing this ;)
noise_array[:, :, 0] = 255 * (np.vectorize(noise.pnoise3)(ygrid, xgrid, frame_count * 0.1, octaves=1) + 1) / 2
image(Image.fromarray(noise_array, mode='HSV').convert('RGB'), 0, 0)
# vnoise library
noise_array2[:, :, 0] = 255 * (vector_noise.noise3(ygrid, xgrid, frame_count * 0.1, octaves=1, grid_mode=False) + 1) / 2
image(Image.fromarray(noise_array2, mode='HSV').convert('RGB'), 400, 0)
# open simplex
noise_array3[:, :, 0] = 255 * (np.vectorize(open_simplex.noise3_Classic)(ygrid, xgrid, frame_count * 0.1) + 1) / 2
image(Image.fromarray(noise_array3, mode='HSV').convert('RGB'), 800, 0)
I did the best I could to make sure I was comparing apples to apples. It looks to me like the OpenSimplex2S object is using only one octave in noise3_Classic. The granularity of the texture looks to me like one octave and similar to the other two, and I couldn’t find anything in the code that seems to dispute that. But if you think I made a mistake there, please let me know.
I’m using the np_vectorize
tool, which takes any old function and does the best it can to optimize it for broadcasting operations over arrays. The vnoise
library doesn’t need to use that.
The PIL Image HSV to RGB conversion I didn’t know about and had to google. It seemed like the most efficient way to convert from one color space to another, but there might be better ways. Again, this is a vectorized operation, and will be much faster than doing this one pixel at a time in a for loop. Inside the PIL library, in C code, it is doing a loop to convert each pixel.
To get performance statistics I can use the built-in tools. While the sketch is running, I type:
profile_draw()
and then a few minutes later,
print_line_profiler_stats()
These are the results:
Timer unit: 1e-06 s
Total time: 604.128 s
File: <ipython-input-4-415b4354e79f>
Function: draw at line 8
Line # Hits Time Per Hit % Time Line Contents
==============================================================
8 def draw():
9 # noise library, which is what py5 currently uses, but I am calling it directly because of a bug I found writing this ;)
10 293 329755860.0 1125446.6 54.6 noise_array[:, :, 0] = 255 * (np.vectorize(noise.pnoise3)(ygrid, xgrid, frame_count * 0.1, octaves=1) + 1) / 2
11 293 19325199.0 65956.3 3.2 image(Image.fromarray(noise_array, mode='HSV').convert('RGB'), 0, 0)
12 # vnoise library
13 293 37371843.0 127549.0 6.2 noise_array2[:, :, 0] = 255 * (vector_noise.noise3(ygrid, xgrid, frame_count * 0.1, octaves=1, grid_mode=False) + 1) / 2
14 293 5231848.0 17856.1 0.9 image(Image.fromarray(noise_array2, mode='HSV').convert('RGB'), 400, 0)
15 # open simplex
16 293 207530770.0 708296.1 34.4 noise_array3[:, :, 0] = 255 * (np.vectorize(open_simplex.noise3_Classic)(ygrid, xgrid, frame_count * 0.1) + 1) / 2
17 293 4912409.0 16765.9 0.8 image(Image.fromarray(noise_array3, mode='HSV').convert('RGB'), 800, 0)
Neat, huh? It shows you how much time is spent on each line. There’s a lot happening in that table. The clearest line is the “% Time” column. It shows that it is spending 54% of the time using the first method, which is py5’s current noise implementation. It spends 34% of the time using the OpenSimplex java code. And it spends 6% of the time using vnoise.
I knew the noise library would be slower than vnoise but I did not think it would be that slow.
Now this does not mean that the open simplex library is slow or no good; I’m confident that most of that time was spent moving a few numbers at a time from Python to Java and then one number returned from Java to Python, assigned to the array. Py5 is not very efficient when you make thousands of little calls to Java like that, so it is best to avoid that wherever you can.
There’s another technique that could be used here to make OpenSimplex much, much faster than the other two options, and that is by creating a hybrid Java-Python application, which py5 currently supports. Basically you’d write some Java code to accept an entire array of inputs, and then in Java you would do the loop over the calls to noise3_Classic
for each value in the array, and then you would return the entire array back to Python. This approach is more effort to code, but I believe it would be faster than vnoise. That’s too much to explain here though but I will be documenting this eventually.
Does all of this make sense? I can provide more detail anywhere if you need it. I’m here to help!