Just spent a few hours playing with Noise again.
@jeff’s Perlin Noise 2D code works, and works decently, I got it work in my testing… but… it turns out that for a 2D display (like a led matrix), you really want 3D perlin noise, to make it look right. You can shift the ‘noise map’ around, but you can’t make it change itself smoothly, without a 3rd dimension. (So we can use t (time) in place of z, and that makes it actually evolve smoothly as you watch it)
So I dug into the code, and found something I’ll adapt into a self contained and hopefully fast version. Turns out that the massive p array in Jeff’s code (and most others, too), is a well mixed up set of values from 1-255 or so, then duplicated into 256-512, for avoiding array overrunning). If that array isn’t well mixed, the results are less mixed… but everyone is using the same ‘random’ set of values Ken Perlin used in his original reference code… because there is a small risk of not having a good mix… so everyone just uses a known list? Weird.
It also makes for ugly PB code, because we can’t just do array = [1,2,3…], we have to add each item one at a time.
Found a ‘quick’ and simple 3D perlin noise in JS, that among other things, allows you to build the array on the fly, so I’ll port that, and hopefully we’ll have something that works in the limited PB JS and also is truly more random too.
Update: ok, a few hours later… we have code
Perlin 3D noise
// Perlin 3D noise
//
// Written by Thom Chiovoloni, dedicated into the public domain
// per http://creativecommons.org/publicdomain/zero/1.0
// https://github.com/thomcc/quick-noise.js
//
// adapted by ScruffyNerf into Pixelblaze's JSish language
// you could replace the use of random() with a different RNG if you wish
var arraySize = 256
var permSize = arraySize*2
var gradSize = permSize
var arr = array(arraySize)
var perm = array(permSize)
var grad1 = array(gradSize)
var grad2 = array(gradSize)
var grad3 = array(gradSize)
var gradBasis = array(36)
var gradIndex = 0
var gradIdx = 0
export var scale = 1
export function sliderScale(v){
scale = 0.0001+(v*10)
}
function buildTable() {
arr.mutate(arrayFill)
arr.mutate(arrayShuffle)
arr.mutate(arrayShuffle) // let's mix it up again, just to be sure, optional
perm.mutate(permClone)
grad(1,1,0); grad(-1,1,0); grad(1,-1,0); grad(-1,-1,0)
grad(1,0,1); grad(-1,0,1); grad(1,0,-1); grad(-1,0,-1)
grad(0,1,1); grad(0,-1,1); grad(0,1,-1); grad(0,-1,-1)
perm.forEach(gradPop)
}
function arrayFill(v,i,a){
return i
}
function arrayShuffle(v,i,a){
r = floor(random(a.length))
t = arr[r]
arr[r] = v
return t
}
function permClone(v,i,a){
if (i > arraySize -1) {
return arr[i-arraySize]
} else {
return arr[i]
}
}
function grad(x, y, z) {
gradBasis[gradIndex] = x
gradIndex++
gradBasis[gradIndex] = y
gradIndex++
gradBasis[gradIndex] = z
gradIndex++
}
function gradPop(v,i,a){
g = v%12 * 3
grad1[gradIdx] = gradBasis[g]
grad2[gradIdx] = gradBasis[g+1]
grad3[gradIdx] = gradBasis[g+2]
gradIdx++
}
function fade(t) {
return t * t * t * (t * (t * 6 - 15) + 10)
}
buildTable()
function noise(x, y, z, xWrap, yWrap, zWrap) {
// x, y, z are numbers.
// xWrap, yWrap, and zWrap are integer powers of two between 0 and 256.
// (0 and 256 are equivalent). If these aren't provided, they default to 0.
xMask = ((xWrap-1) & 255) >> 0
yMask = ((yWrap-1) & 255) >> 0
zMask = ((zWrap-1) & 255) >> 0
px = floor(x)
py = floor(y)
pz = floor(z)
x0 = (px+0) & xMask
x1 = (px+1) & xMask
y0 = (py+0) & yMask
y1 = (py+1) & yMask
z0 = (pz+0) & zMask
z1 = (pz+1) & zMask
x -= px
y -= py
z -= pz
u = fade(x)
v = fade(y)
w = fade(z)
r0 = perm[x0]
r1 = perm[x1]
r00 = perm[r0+y0]
r01 = perm[r0+y1]
r10 = perm[r1+y0]
r11 = perm[r1+y1]
h000 = perm[r00+z0]
h001 = perm[r00+z1]
h010 = perm[r01+z0]
h011 = perm[r01+z1]
h100 = perm[r10+z0]
h101 = perm[r10+z1]
h110 = perm[r11+z0]
h111 = perm[r11+z1]
n000 = grad1[h000]*(x+0) + grad2[h000]*(y+0) + grad3[h000]*(z+0)
n001 = grad1[h001]*(x+0) + grad2[h001]*(y+0) + grad3[h001]*(z-1)
n010 = grad1[h010]*(x+0) + grad2[h010]*(y-1) + grad3[h010]*(z+0)
n011 = grad1[h011]*(x+0) + grad2[h011]*(y-1) + grad3[h011]*(z-1)
n100 = grad1[h100]*(x-1) + grad2[h100]*(y+0) + grad3[h100]*(z+0)
n101 = grad1[h101]*(x-1) + grad2[h101]*(y+0) + grad3[h101]*(z-1)
n110 = grad1[h110]*(x-1) + grad2[h110]*(y-1) + grad3[h110]*(z+0)
n111 = grad1[h111]*(x-1) + grad2[h111]*(y-1) + grad3[h111]*(z-1)
n00 = n000 + (n001-n000) * w
n01 = n010 + (n011-n010) * w
n10 = n100 + (n101-n100) * w
n11 = n110 + (n111-n110) * w
n0 = n00 + (n01-n00) * v
n1 = n10 + (n11-n10) * v
return n0 + (n1-n0) * u
}
var time
export function beforeRender(delta) {
time += delta/1000
}
export function render2D(index, x, y) {
hsv(noise(x*scale, y*scale,time), 1, 1)
}
Not bad, runs at 17.5 frames a second on 16x16 matrix, v3 PB.
Ran into the array size limit, trying to make a 512x3 array, so I broke it into 3 arrays of 512. (This uses 1 256, 4 512s, and a 32, in addition to whatever your pattern uses)
There is supposed to be a way to make this hashable, and not use all of these lookups, that might be only way to speed this up, if hash calcing is faster than array lookups, of which this uses many)
@wizard , consider this a request to push something like this (or better) into firmware, a noise() function is pretty critical for many fun things. Needs to be at least 3d noise (and with the awesome level of 3D mapping we have otherwise in PB, really 4D noise might be worthwhile if it’s doable)
Tweaks before I add it to the library…
- I can make it fall back to the ‘known’ array, which should guarantee the same results every time, assuming you don’t randomize otherwise (ie given the same map, it’ll look identical every pattern run then)
- cleanup and add something nicer than just a full map noise display. I did add a slider for scale, but really it should be a scale on all 3 axis, so you could distort X and Y, and also change the time scaling/speed.
- In other code, moving the origin center to the middle of the map (.5,.5) is nice for other reasons of math, but noise likes positive numbers, so that correcting might be worth putting in as part of the function. abs()ing would be weird (and mirror), so maybe just a global offset?
- If you want two different noise fields, right now you can’t do that. Wondering if there is a way around that… Might just have to do a large offset instead, because of the array overhead.
- Sigh, probably Simplex is needed, as faster, scales better too.
Added: and guess what, it won’t run on a PB v2 because it runs out of array space.
(My dev v3 blew up (went into setup and won’t seem to wake up despite reflashing it), so I grabbed a handy v2, and learned this the hard way. On the bright side, I’m going to try a Value Noise implementation I found, and see how it looks.
Value Noise code - same pattern as above roughly but different
var seed = random(100)
function lerp(a, b, t) {
return (1 - t) * a + t * b;
}
function hash2d(x, y) {
x = 50 * frac(x * 0.3183099 + 0.71);
y = 50 * frac(y * 0.3183099 + 0.113);
return -1 + 2 * frac(1.375986 * seed + x * y * (x + y));
}
function hash3d(x, y, z) {
x = 50 * frac(x * 0.3183099 + 0.71);
y = 50 * frac(y * 0.3183099 + 0.113);
z = 50 * frac(z * 0.3183099 + 0.33);
return -1 + 2 * frac(1.375986 * seed + x * y * z * (x + y + z));
}
function noise2d(x, y) {
ix = floor(x);
iy = floor(y);
fx = frac(x);
fy = frac(y);
ux = fx * fx * (3 - 2 * fx);
return lerp(
lerp(hash2d(ix, iy), hash2d(ix + 1, iy), ux),
lerp(hash2d(ix, iy + 1), hash2d(ix + 1, iy + 1), ux),
fy * fy * (3 - 2 * fy)
);
}
function noise3d(x, y, z){
ix = floor(x);
iy = floor(y);
iz = floor(z);
fx = frac(x);
fy = frac(y);
fz = frac(z);
ux = fx * fx * (3 - 2 * fx);
uy = fy * fy * (3 - 2 * fy);
return lerp(
lerp(
lerp(hash3d(ix + 0, iy + 0, iz + 0), hash3d(ix + 1, iy + 0, iz + 0), ux),
lerp(hash3d(ix + 0, iy + 1, iz + 0), hash3d(ix + 1, iy + 1, iz + 0), ux),
uy
),
lerp(
lerp(hash3d(ix + 0, iy + 0, iz + 1), hash3d(ix + 1, iy + 0, iz + 1), ux),
lerp(hash3d(ix + 0, iy + 1, iz + 1), hash3d(ix + 1, iy + 1, iz + 1), ux),
uy
),
fz * fz * (3 - 2 * fz)
);
}
function fractal2d(x, y, octaves) {
val = 0;
for (i = 0; i < octaves; i++) {
val += noise2d(x, y) / pow(2, 0.5 + i - 0.5 * i);
x -= i * 19;
y += i * 7;
x *= 1.57;
y *= 1.57;
}
return val;
}
function fractal3d(x, y, z, octaves) {
val = 0;
for (i = 0; i < octaves; i++) {
val += noise3d(x, y, z) / pow(2, 0.5 + i - 0.5 * i);
x -= i * 7;
y += i * 13;
z -= i * 23;
x *= 1.57;
y *= 1.57;
z *= 1.57;
}
return val;
}
export var noisescale = 1
export function sliderScale(v){
noisescale = v*10
}
var time
export function beforeRender(delta) {
time += delta/5000
}
export function render2D(index, x, y) {
hsv(noise3d(x*noisescale, y*noisescale, time), 1, 1)
}
Run at 10.5 frames on my v2… so it should be faster on a v3 (I need to solder one up until we figure out how to recover my other one. I also have a pile of Picos to be soldered.)
Value vs Perlin? They are different, but might be an acceptable substitute in some cases.
And minutes later, realizing I could solve the array usage, you don’t need double sized arrays, if you just mod results to 256, I have Perlin Noise working on the v2, at 12fps…
v2 compatible Perlin - now with smaller arrays!
// Perlin 3D noise (now with even more reduced array usage!)
//
// Written by Thom Chiovoloni, dedicated into the public domain
// per http://creativecommons.org/publicdomain/zero/1.0
// https://github.com/thomcc/quick-noise.js
//
// adapted by ScruffyNerf into Pixelblaze's JSish language
// you could replace the use of random() with a different RNG if you wish
var arraySize = 256
var permSize = arraySize
var gradSize = permSize
var perm = array(permSize)
var grad1 = array(gradSize)
var grad2 = array(gradSize)
var grad3 = array(gradSize)
var gradBasis = array(36)
var gradIndex = 0
var gradIdx = 0
function buildTable() {
perm.mutate(arrayFill)
perm.mutate(arrayShuffle)
perm.mutate(arrayShuffle) // for good measure, shuffle twice
grad(1,1,0); grad(-1,1,0); grad(1,-1,0); grad(-1,-1,0)
grad(1,0,1); grad(-1,0,1); grad(1,0,-1); grad(-1,0,-1)
grad(0,1,1); grad(0,-1,1); grad(0,1,-1); grad(0,-1,-1)
perm.forEach(gradPop)
}
function arrayFill(v,i,a){
return i
}
function arrayShuffle(v,i,a){
r = floor(random(a.length))
t = a[r]
a[r] = v
return t
}
function grad(x, y, z) {
gradBasis[gradIndex] = x
gradIndex++
gradBasis[gradIndex] = y
gradIndex++
gradBasis[gradIndex] = z
gradIndex++
}
function gradPop(v,i,a){
g = v%12 * 3
grad1[gradIdx] = gradBasis[g]
grad2[gradIdx] = gradBasis[g+1]
grad3[gradIdx] = gradBasis[g+2]
gradIdx++
}
function fade(t) {
return t * t * t * (t * (t * 6 - 15) + 10)
}
buildTable()
function noise(x, y, z, xWrap, yWrap, zWrap) {
// x, y, z are numbers.
// xWrap, yWrap, and zWrap are integer powers of two between 0 and 256.
// (0 and 256 are equivalent). If these aren't provided, they default to 0.
xMask = ((xWrap-1) & 255) >> 0
yMask = ((yWrap-1) & 255) >> 0
zMask = ((zWrap-1) & 255) >> 0
px = floor(x)
py = floor(y)
pz = floor(z)
x0 = (px+0) & xMask
x1 = (px+1) & xMask
y0 = (py+0) & yMask
y1 = (py+1) & yMask
z0 = (pz+0) & zMask
z1 = (pz+1) & zMask
x -= px
y -= py
z -= pz
u = fade(x)
v = fade(y)
w = fade(z)
r0 = perm[x0%256]
r1 = perm[x1%256]
r00 = perm[(r0+y0)%256]
r01 = perm[(r0+y1)%256]
r10 = perm[(r1+y0)%256]
r11 = perm[(r1+y1)%256]
h000 = perm[(r00+z0)%256]%256
h001 = perm[(r00+z1)%256]%256
h010 = perm[(r01+z0)%256]%256
h011 = perm[(r01+z1)%256]%256
h100 = perm[(r10+z0)%256]%256
h101 = perm[(r10+z1)%256]%256
h110 = perm[(r11+z0)%256]%256
h111 = perm[(r11+z1)%256]%256
n000 = grad1[h000]*(x+0) + grad2[h000]*(y+0) + grad3[h000]*(z+0)
n001 = grad1[h001]*(x+0) + grad2[h001]*(y+0) + grad3[h001]*(z-1)
n010 = grad1[h010]*(x+0) + grad2[h010]*(y-1) + grad3[h010]*(z+0)
n011 = grad1[h011]*(x+0) + grad2[h011]*(y-1) + grad3[h011]*(z-1)
n100 = grad1[h100]*(x-1) + grad2[h100]*(y+0) + grad3[h100]*(z+0)
n101 = grad1[h101]*(x-1) + grad2[h101]*(y+0) + grad3[h101]*(z-1)
n110 = grad1[h110]*(x-1) + grad2[h110]*(y-1) + grad3[h110]*(z+0)
n111 = grad1[h111]*(x-1) + grad2[h111]*(y-1) + grad3[h111]*(z-1)
n00 = n000 + (n001-n000) * w
n01 = n010 + (n011-n010) * w
n10 = n100 + (n101-n100) * w
n11 = n110 + (n111-n110) * w
n0 = n00 + (n01-n00) * v
n1 = n10 + (n11-n10) * v
return n0 + (n1-n0) * u
}
export var noisescale = 1
export function sliderScale(v){
noisescale = v*10
}
var time
export function beforeRender(delta) {
time += delta/5000
}
export function render2D(index, x, y) {
hsv(noise(x*noisescale, y*noisescale, time), 1, 1)
}
If you run both, you can easily see the difference in Value and Perlin… Value is much less smooth, it’s ‘peaky’, no matter how you scale it… Perlin absolutely is the winner for natural flow.
And in the midst of porting Flow my Tears, and running out of array space again, I just realized I no longer need to duplicate the built array into the perm array, which will save even more space. Revised code is now above.