Animated Worley Noise (Trypophobia Warning)
So this started as an attempt to replicate the Shadergraph "Voronoi" in normal CG shaders, but got a lot more interesting. My main reason for doing this is to make a procedurally generated pattern to match the pattern caused by water caustics, in a cheap way that's not too heavy on the GPU.
Voronoi, or "Worley Noise" to be more accurate, seemed like a good way to do it. After looking up a bit, the way Worley Noise works is by dividing a texture up into grids, putting a point at a random position in each grid section, and then for each point on the texture, outputting a value that is lighter (or darker) based on the distance to the nearest point. First I needed a system to generate random floats in a shader; I was able to achieve this by converting the following code:
https://www.shadertoy.com/view/4djSRW
to CG. After a bit of deliberation (code exists for this online, but when I'm messing around, I like to make sure I can figure out how to do it myself) I came up with the following code to do generate the voronoi:
inline float Voronoi(float cellDensity, float2 UV, float radius) { float oneDivCellDensity = 1.0 / cellDensity; float minDistance = radius; for (int i = 0; i < 3; i++) { for (int j = 0; j < 3; j++) { int2 cell = floor(UV * cellDensity) + int2(i-1, j-1); float2 cellValue = hash22(cell); cellValue = (cellValue + (float2)cell) * oneDivCellDensity; minDistance = min(minDistance, distance(UV, cellValue)); } } float returnValue = saturate(minDistance / radius); return returnValue; }
With a simple fragment shader we're good to go:
fixed4 frag (v2f i) : SV_Target { fixed4 col = fixed4(1, 1, 1, 1); float2 VoronoiUVs = i.worldPos.xz; float VoronoiImg = Voronoi(_CellDensity, VoronoiUVs, _Radius); col = VoronoiImg; col.a = 1; UNITY_APPLY_FOG(i.fogCoord, col);
return col; }
If we need all the "edges" to be full brightness, as may be the case in some uses, we can modify our code slightly:
inline float VoronoiNormalized(float cellDensity, float2 UV, float radius) { float oneDivCellDensity = 1.0 / cellDensity; float distances[9]; for (int i = 0; i < 3; i++) { for (int j = 0; j < 3; j++) { int2 cell = floor(UV * cellDensity) + int2(i - 1, j - 1); float2 cellValue = hash22(cell) * 0.7f; cellValue = (cellValue + (float2)cell) * oneDivCellDensity; distances[i+j*3] = distance(UV, cellValue); } } float minDistance = radius; float minDistance2 = radius; for (int k = 0; k < 9; k++) { { UNITY_FLATTEN if (minDistance > distances[k]) { minDistance2 = minDistance; minDistance = distances[k]; } else if (minDistance2 > distances[k]) { minDistance2 = distances[k]; } } } float returnValue = saturate(minDistance / minDistance2); return returnValue; }
We're not going to be using this, but it's nice to know how to do it.
The next step is, we want to animate this.
To do this, we're going to take add a seed to the randomization function of each point, generated by flooring the time input to an int. This creates "steps" in between the seed changing. By using the frac of that time, we can get the point we are in the current step (a value that starts at 0 and goes to 1 as we reach the end of the step), and by calculating what the seed will be at the next step, and lerping between the two calculated values, we get smooth motion:
inline float VoronoiAnimatedLowQuality(float cellDensity, float2 UV, float radius, float speed) { float oneDivCellDensity = 1.0 / cellDensity; float minDistance = radius; for (int i = 0; i < 3; i++) { for (int j = 0; j < 3; j++) { int2 cell = floor(UV * cellDensity) + int2(i - 1, j - 1); float time = _Time.x * speed; int timeFloor = floor(time) * 100; int nextTimeFloor = timeFloor + 100; float timeFrac = frac(time); float2 cellValue1 = hash22(cell + int2(timeFloor, timeFloor)); float2 cellValue2 = hash22(cell + int2(nextTimeFloor, nextTimeFloor)); float2 cellValue = lerp(cellValue1, cellValue2, timeFrac); cellValue = (cellValue + (float2)cell) * oneDivCellDensity; minDistance = min(minDistance, distance(UV, cellValue)); } } float returnValue = saturate(minDistance / radius); return returnValue; }
Naturally we want the movements of the centers to be desynced. We can do this easily, by adding a random offset for each cell when we create our time variable:
float time = _Time.x * speed + hash22(cell);
If we want a bit more smooth, less jerky (and creepy!) movement, we need to do some interpolation.
inline float VoronoiAnimated(float cellDensity, float2 UV, float radius, float speed) { float oneDivCellDensity = 1.0 / cellDensity; float minDistance = radius; for (int i = 0; i < 3; i++) { for (int j = 0; j < 3; j++) { int2 cell = floor(UV * cellDensity) + int2(i - 1, j - 1); float time = _Time.x * speed + hash22(cell); int timeFloor = floor(time) * 100; int prevTimeFloor = timeFloor - 100; int nextTimeFloor = timeFloor + 100; int nextTimeFloor2 = timeFloor + 200; float timeFrac = frac(time); float2 cellValue0 = hash22(cell + int2(prevTimeFloor, prevTimeFloor)); float2 cellValue1 = hash22(cell + int2(timeFloor, timeFloor)); float2 cellValue2 = hash22(cell + int2(nextTimeFloor, nextTimeFloor)); float2 cellValue3 = hash22(cell + int2(nextTimeFloor2, nextTimeFloor2));
float2 slope1 = ((cellValue1 - cellValue0) + (cellValue2 - cellValue1)) / 2; float2 slope2 = ((cellValue2 - cellValue1) + (cellValue3 - cellValue2)) / 2; slope1 /= (cellValue2 - cellValue1); slope2 /= (cellValue2 - cellValue1); slope1 = clamp(slope1, float2(-100, -100), float2(100, 100)); slope2 = clamp(slope2, float2(-100, -100), float2(100, 100));
float f = timeFrac; float easeInx = (1 - slope1.x)*f*f + slope1.x * f; float easeOutx = (slope2.x - 1)*f*f + (2 - slope2.x)*f; float easeInOutx = easeInx * (1 - f) + easeOutx * f; float easeIny = (1 - slope1.y)*f*f + slope1.y * f; float easeOuty = (slope2.y - 1)*f*f + (2 - slope2.y)*f; float easeInOuty = easeIny * (1 - f) + easeOuty * f; float2 cellValue; cellValue.x = lerp(cellValue1.x, cellValue2.x, easeInOutx); cellValue.y = lerp(cellValue1.y, cellValue2.y, easeInOuty); cellValue = (cellValue + (float2)cell) * oneDivCellDensity; minDistance = min(minDistance, distance(UV, cellValue)); } } float returnValue = saturate(minDistance / radius); return returnValue; }
this math was a bit of a trial to figure out; what it's doing is, instead of calculating the current step and the next step, it calculates 4 steps: the previous step, the current step, the next step, and the step after the next step. We want to create an easing in and an easing out for both the X and the Y, but instead of easing in or out, we want a cubic equation to use as a 1D Non-Linear transformation that, when used as the weight to a lerp function, will create a speed and direction of movement that will match the speed and direction at the end of the previous step.
Let's say you have your 4 steps. 0 is the previous step, 1 is the current step, and 2 and 3 are the next two steps. We'll tackle just the Y axis to start with, and then once we're done we'll do the same thing to both axes.
What you want to do is find the slopes of 0->1 and 1->2 and average them to get the desired slope at 1, and the slopes of 1->2 and 2->3 and average them to get the desired slope at 2 (desired slopes shown in blue):
Then you want to find the cubic equation that will bass through points 1 and 2 and have the correct slope at each point.
Although since we want a lerp weight, what we'll be doing instead is scaling the whole thing vertically (including the slopes) so that 1 and 2 are exactly 1 unit apart in the Y direction.
In order to do this what I did was I took the parabola that passes through points 1 and 2 and has the desired slope at point 1, as well as the parabola that passes through points 1 and 2 and has the desired slope at point 2, and linear interpolated between one equation and the other based on time. That's what this code is doing:
float easeIny = (1 - slope1.y)*f*f + slope1.y * f; float easeOuty = (slope2.y - 1)*f*f + (2 - slope2.y)*f; float easeInOuty = easeIny * (1 - f) + easeOuty * f;
Once all this is done, you have this:
Square the output, make it blend alpha, and add some noise displacement to your UVs, and you'll have this:
Edit: I modified the code so that the cells can travel further, and changed the noise displacement a bit, and it looks even better: