Worley noise is a type of noise used for procedural texturing in computer graphics. In its most basic form, it looks like this:
(color r2 (worley q 50 :oversample true))
That’s ugly and boring, but it’s a quick way to see what the effect looks like. If we use Worley noise to distort a 3D shape, we can get something like a hammered or cratered texture:
(def s (osc t 5 | ss 0.2 0.8 * 30 + 10))
(ball 100
| expound (worley p s) (sqrt s)
| slow 0.8
| shade sky
| rotate y (t / 10))
Like many procedural textures, it looks a lot better if you repeat the effect a few times with different frequencies:
(def s (osc t 5 | ss 0.2 0.8 * 30 + 10))
(ball 100
| expound (fbm 3 worley p s) (sqrt s)
| slow 0.8
| shade sky
| rotate y (t / 10))
There are some visual artifacts in these renderings, because they’re using a fast approximation of Worley noise that gives the wrong answer for some values.
To explain these artifacts in more detail, we have to understand a little bit about how Worley noise works.
It’s pretty simple: you start with a grid of points.
(circle 0
| color (hsv (hash $i) 0.5 1)
| tile: $i [30 30]
| expand 3)
Then you move each point by some random offset:
(def animate (osc t 5 | ss 0.1 0.7))
(circle 0
| color (hsv (hash $i) 0.5 1)
| move (hash2 $i * animate * 15)
| tile: $i [30 30] :oversample true
| expand 3)
When you’re writing a shader, you don’t actually have the ability to generate random numbers, so we’re using a hash function to produce random-looking offsets based on the logical position of each point (that is, $i = [0 0] for the center point, $i = [1 0] for the point to the right of that, etc).
Finally, once you have the points at random-looking positions, you compute the distance to the nearest point for every simple pixel that you sample – and that’s Worley noise.
(def animate (osc t 5 | ss 0.1 0.7))
(def points
(circle 0
| color (hsv (hash $i) 0.5 1)
| move (hash2 $i * animate * 15)
| tile: $i [30 30] :oversample true
))
(set background-color
(vec3 (shape/distance points / 30)))
(expand points 3)
How do you compute the distance to the nearest point for any pixel you ask about? It’s actually pretty simple: you know that you started with a perfectly even square grid. For any pixel, you can compute the “grid cell” that that pixel falls into ([0 0], [0 1], etc). It’s just the pixel divided by the grid size, rounded to the nearest integer.
(def animate (osc t 5 | ss 0.1 0.7))
(union
(color r2 (hsv (hash $i) 0.5 1))
(circle 3 | color (hsv (hash $i) 0.5 0.1))
| move (hash2 $i * animate * 15)
| tile: $i [30 30] :oversample true :sample-from -1
)
And you know that the nearest point is either in this grid, or it’s in one of the immediately adjacent grids, because we only offset our points by at most half the grid size, so each randomly distributed point is still inside its original grid cell. Which means there’s no point inside any other cell that could be nearer than any point in one of the adjacent cells.
(def animate (osc t 5 | ss 0.1 0.7))
(def question-point (rotate [(osc t 5 50 100) 0] (t / 2)))
(def question-cell (question-point / 30 - 1 | round))
(union
(color r2 (hsv (hash $i) 0.5
(gl/if (<= (max ($i - question-cell | abs)) 1) 1 0.25)))
(circle 3 | color (hsv (hash $i) 0.5 0.1))
| move (hash2 $i * animate * 15)
| tile: $i [30 30] :oversample true :sample-from -1
| union
(union (circle 4 | color black) (circle 3 | color white)
| move question-point))
So that leaves you nine points to check, for every single pixel in your shader. Here’s the optimization that’s causing visual artifacts: instead of checking all nine adjacent cells, only check the current cell and the three cells closest to the point in question. The nearest point to your sample position is probably in one of those cells, but it doesn’t have to be. So you might get some visual artifacts occasionally.
(def animate (osc t 5 | ss 0.1 0.7))
(def question-point (rotate [(osc t 5 50 100) 0] (t / 2)))
(def question-cell (question-point / 30 - 1 | round))
(def question-bias (question-point / 30 | fract | round * 2 - 1 -))
(defn all [bvec] (and bvec.x bvec.y))
(defn or-bvec [a b] [(or a.x b.x) (or a.y b.y)])
(union
(color r2 (hsv (hash $i) 0.5
(gl/let [offset ($i - question-cell)]
(gl/if (and (<= (max (abs offset)) 1)
(all
(or-bvec (equal offset question-bias)
(equal offset [0 0]))))
1 0.25))))
(circle 3 | color (hsv (hash $i) 0.5 0.1))
| move (hash2 $i * animate * 15)
| tile: $i [30 30] :oversample true :sample-from -1
| union
(union (circle 4 | color black) (circle 3 | color white)
| move question-point))
# ahh that took so long
But notice: this is getting a little bit complicated. And the original code snippet I showed you wasn’t very complicated at all.
(def animate (osc t 5 | ss 0.1 0.7))
(def points
(circle 0
| color (hsv (hash $i) 0.5 1)
| move (hash2 $i * animate * 15)
| tile: $i [30 30] :oversample true
))
(set background-color
(vec3 (shape/distance points / 30)))
(expand points 3)
Nowhere does that code compute cell coordinates or check for the nearest point. I just constructed this thing, said shape/distance, and somehow that just… gave me the distance to the nearest point.
I was able to do that because Bauble is a playground for making 3D graphics with signed distance functions. Bauble’s whole deal is computing distances to things! And Worley noise is just the signed distance function of a bunch of randomly distributed points. I’m used to thinking of signed distance functions as defining implicit surfaces of 3D shapes, but Worley noise uses the distance as a scalar in its own right.
So.
This is interesting.
What if… we took other signed distance functions, and used them as procedural noise distortions?
We’ll start simple. Instead of points, what if we randomly distribute a bunch of squares?
(def animate (osc t 5 | ss 0.1 0.7))
(def size 60)
(def points
(rect size
| color (hsv (hash $i) 0.5 1)
| move (hash2 $i * animate * 15)
| tile: $i (0.5 * size | vec2) :oversample true
))
(set background-color
(vec3 (shape/distance points + size / (0.5 * size))))
(expand points (- 3 size))
It’s not obvious that that will be interesting. Let’s look at it in action:
(defn squarley [input &opt period]
(default period 1)
(gl/with [q (input / period)]
(rect 1
| color (hsv (hash $i) 0.5 1)
| move (hash2 $i * 0.5)
| tile: $i [1 1] :oversample true
| shape/distance)))
(ball 100 | expound (squarley p.xy 20) 20
| rotate y (t / 10))
Since we only defined this noise function in 2D, we need a two-dimensional input. That’s a pretty boring 2D input. This is a little more interesting:
(defn squarley [input &opt period]
(default period 1)
(gl/with [q (input / period)]
(rect 1
| color (hsv (hash $i) 0.5 1)
| move (hash2 $i * 0.5)
| tile: $i [1 1] :oversample true
| shape/distance)))
(ball 100
| expound (squarley
[(atan2 p.xy | abs) (sqrt (150 - p.z | abs))]
[.5 1]) 20
| rotate y (t / 10))
We can apply multiple octaves of this, to get… something.
(defn squarley [input &opt period]
(default period 1)
(gl/with [q (input / period)]
(rect 1
| color (hsv (hash $i) 0.5 1)
| move (hash2 $i * 0.5)
| tile: $i [1 1] :oversample true
| shape/distance)))
(ball 100
| expound (fbm 3 squarley
[(atan2 p.xy | abs) (sqrt (150 - p.z | abs))]
[.5 1]) 20
| rotate y (t / 10)
)
But so far this is not a very interesting effect. What if we vary the orientation as well?
(def animate (osc t 5 | ss 0.1 0.7))
(def size 60)
(def points
(rect size
| color (hsv (hash $i) 0.5 1)
| rotate (hash $i 1000 * pi/2 * animate)
| move (hash2 $i * animate * 15)
| tile: $i (0.5 * size | vec2) :oversample true
))
(set background-color
(vec3 (shape/distance points + size / (0.5 * size))))
(expand points (- 3 size))
It’s a little bit more random-looking, I guess:
(defn squarley [input &opt period]
(default period 1)
(gl/with [q (input / period)]
(rect 1
| rotate (hash $i 1000 * pi/2)
| color (hsv (hash $i) 0.5 1)
| move (hash2 $i * 0.5)
| tile: $i [1 1] :oversample true
| shape/distance)))
(ball 100
| expound (fbm 3 squarley
[(atan2 p.xy | abs) (sqrt (150 - p.z | abs))]
[.5 1]) 20
| rotate y (t / 10)
)
But distorting 3D space with 2D noise is not… it doesn’t look great.
Let’s jump to 3D.
(def animate (osc t 5 | ss 0.1 0.7))
(def size 60)
(def points
(box size
| shade (hsv (hash $i) 0.5 1)
| rotate (hash3 $i 2000) (hash $i 1000 * pi/2 * animate)
| move (hash3 $i * animate * 15)
| tile: $i (0.5 * size | vec3) :limit 5 :oversample true
))
(union
(plane (- ray.direction)
| color (shape/distance points + size / (0.5 * size)))
(expand points (- 3 size)) | scale 2)
It’s a lot harder to visualize the distance field in 3D. What you’re seeing there is the distance field at the plane that passes through the origin and faces towards the camera. I know it’s not a great visualization, but the point is that this technique generalizes to 3D (even if it’s hard to imagine the distance field at every point in 3D space).
Let’s see how this looks when we use it to distort a 3D shape:
(defn cubeley [input &opt period]
(default period 1)
(gl/with [p (input / period)]
(box 1
| rotate (hash3 $i 2000) (hash $i 1000 * pi/2)
| move (hash3 $i * 0.5)
| tile: $i [1 1 1] :oversample true
| shape/distance)))
(torus y 100 50
| expound (cubeley p 30) 20
| rotate y (t / 10))
It’s kind of interesting? Definitely better than what we had before. Sort of a faceted gemstone effect.
Do you think our computers will catch on fire if we try multiple octaves of this?
(defn cubeley [input &opt period]
(default period 1)
(gl/with [p (input / period)]
(box 1
| rotate (hash3 $i 2000) (hash $i 1000 * pi/2)
| move (hash3 $i * 0.5)
| tile: $i [1 1 1] :oversample true
| shape/distance)))
(torus y 100 50
| expound (fbm 3 cubeley p 30) 20
| rotate y (t / 10))
I’m glad you’re still with me.
Let’s trade the boxes for cones:
(defn coneley [input &opt period]
(default period 1)
(gl/with [p (input / period)]
(cone y 1 1
| move (hash3 $i * 0.5)
| tile: $i [1 1 1] :oversample true
| shape/distance)))
(ball [100 150 100]
| expound (coneley p 30) 20
| rotate y (t / 10)
)
It’s kind of an interesting pinecone-y texture? I guess?
There are more primitives to try. But of course we don’t have to limit ourselves to primitive shapes.
This is a classic SDF example to demonstrate how easy it is to do constructive solid geometry stuff:
(box 100 :r 10
| subtract :r 10 (sphere 120))
What if… we used that as the basis for our Worley noise?
(def jitter (osc t 5 | ss 0.1 0.9))
(defn cubeley [input &opt period]
(default period 1)
(gl/with [p (input / period)]
(box 1 :r 0.1
| subtract :r 0.1 (sphere 1.2)
| move (hash3 $i * 0.5 * jitter)
| tile: $i [1 1 1] :oversample true
| shape/distance)))
(torus y 100 50
| expound (cubeley p 30) 20
| rotate y (t / 10))
I think it’s kind of more interesting without the randomization.
We’ve constructed an interesting 3D noise function, and we’re using it to distort 3D space. But of course, we can go back to considering this a “noise texture” in the original sense of the word:
(def jitter (osc t 5 | ss 0.1 0.9))
(defn cubeley [input &opt period]
(default period 1)
(gl/with [p (input / period)]
(box 1 :r 0.1
| subtract :r 0.1 (sphere 1.2)
| rotate (hash3 $i 1000) (hash $i 2000 * jitter)
| move (hash3 $i * 0.5 * jitter)
| tile: $i [1 1 1] :oversample true :sample-from -2 :sample-to 2
| shape/distance)))
(r2 | color (vec3 (fbm 4 cubeley [q 0] 128 | abs)))
Kinda neat.
The point of all of this is: Worley noise invites us to reconsider signed distance functions as more than implicit surfaces. And since Bauble makes it easy to construct signed distance functions, it’s a good playground for experimenting with textures like this.
Even if we never found anything particularly attractive, it’s fun to play around with space.
If this is your first time seeing Bauble, hey welcome! This post is an expansion of something I briefly talked about in a YouTube video once. The video has many more examples of the sorts of things that Bauble can do. Check it out if this piqued your interest!