top of page

# A few noise related things I completed but didn't get around to making a post on (until now)

The first one's a simple animated cloud, made using tessellation and a simple vertex offset along the normal. It uses a 4D Perlin noise fractal, running the output through an absolute value to create a sort of "billow" effect.

The second one's a wood shader I made. I used stretched 3D perlin noise, with a sawtooth wave remap (made using frac()) to create the rings. I used very high frequency stretched noise to create the grain. The last one is super interesting and may require some explanation, but let me show it first: This one came from an attempt to make a 3D normal map shader off of worley noise. My original attempt looked like this: This is what happens if you take a normal worley noise output, square it, and then turn that into a heightmap, and turn that heightmap into a normal map. Clearly doesn't look very good. So my thought was, normal worley noise plots a pixel's color based from the distance to the nearest point (where points are laid out in grid cells, with every point's position in that grid cell randomized). What if instead, we generated a voronoi graph, and plot each pixel's color to the distance from the nearest edge of that graph?

Now, to do voronoi in a shader, on a pixel by pixel basis, all you need to do to make voronoi is find out what grid cell the pixel is in, find what randomly generated points are in the 9 gridcells surrounding and including that cell, and find the distance from the nearest one. To find the normal, just get the direction from the randomized point to the current pixel. For the purposes of the image above, the height is just the radius minus the distance.

inline float VoronoiWithDir(float cellDensity, float2 UV, float radius, out float2 direction, float randomnessLimit = 1) { direction = float2(0, 0); float oneDivCellDensity = 1.0 / cellDensity; float2 uvTimesCellDens = UV * cellDensity; float minDistance = radius; for (int i = 0; i < 3; i++) { for (int j = 0; j < 3; j++) { int2 cell = floor(uvTimesCellDens) + int2(i - 1, j - 1); float2 cellValue = (hash22(cell) - 0.5) * randomnessLimit + 0.5; cellValue = (cellValue + (float2)cell) * oneDivCellDensity; float currentDist = distance(UV, cellValue); UNITY_FLATTEN if (minDistance > currentDist) { minDistance = currentDist; direction = normalize(cellValue - UV); } } } float returnValue = saturate(minDistance / radius); return returnValue; }

Let me explain this. At the start of the for loop, we find the integer "cell" our pixel is in. To do that, we take the UV, multiply it by the cell density, and then floor it. Let's say our UV ranged from 0 to 1, and our cell density was 8. That means our multiplied value is a float2 where the X and Y values range from 0 to 8. Floor them, and it's an int2, where our X and Y values can be any integer between 0 and 7 (inclusive). After that, we add a value of "int2(i - 1, j - 1)". What this means is that with each iteration of the for loop, we're setting the value of "cell" to the appropriate cell surrounding the cell the pixel is in. For instance, if i = 0 and j = 0, we're checking the upper left cell; if i = 1 and j = 1, we'll check the cell the pixel is in, and if i = 1 and j = 2, we're checking the cell below the cell the pixel is in. cellValue is set to hash22(cell). hash22 is a randomization function that takes in a float2 and outputs a float2, with both the x and y ranging from 0 to 1. This takes the current cell that's being checked and gets a randomized point in that cell. Now, the randomization function always gives the same output for the same input. Cells are always int2s, meaning any pixel that runs this function and checks a specific cell will always get the same result from the randomized function. This is what allows us to always get the same output for the position of a point within a cell. At this point, cellvalue is just a number between 0 and 1.

Afterwards, we subtract 0.5 from the cellvalue, multiply it by randomnessLimit, and add 0.5 to the cellvalue. This scales down the range that our cell can be in, to get less variation on our voronoi (if desired).

cellValue = (cellValue + (float2)cell) * oneDivCellDensity;

Here we're taking cellvalue (between 0 and 1) and adding its cell's position, and scaling it so that it's in real UV coordinates. This will allow us to find the distance to the current pixel. If the distance is the smallest so far, we record the distance and the direction, and then at the end we have the distance from and the direction to the nearest voronoi point, and we can output them from the function. Don't worry about the UNITY_FLATTEN; that's just telling our compiler to compile that if statement in a way that doesn't cause efficiency problems for multithreaded shaders.

And the surface function:

void surf (Input IN, inout SurfaceOutputStandard o) { float VoronoiImg = 0; float newRadius = _Radius / _CellDensity; float2 dir; VoronoiImg = 1 - saturate(VoronoiWithDir(_CellDensity, IN.worldPos.xy, newRadius, dir, 0.6)); VoronoiImg = 1 - VoronoiImg * VoronoiImg; float4 c = lerp(_Color1, _Color2, VoronoiImg); o.Albedo = c.rgb; o.Normal = normalize(float3(-dir * _NormalHeight * VoronoiImg, 1)); o.Metallic = _Metallic; o.Smoothness = _Glossiness; o.Alpha = c.a; }

As you can see, this was pretty simple.

However, the algorithm for the more polygonal version, where we plotted instead the height based on the the distance from the closest line (up to a certain radius, and then it flattens out) and the direction based on the direction to the closest line (unless, again, we're outside the radius distance from the line, then the direction is (0,0))...

This took me a long time and turned out to be a bit more complicated and quite a bit more inefficient.

float VoronoiNormalized2(float cellDensity, float2 UV, float radius, out float2 direction) { direction = float2(0, 0); float oneDivCellDensity = 1.0 / cellDensity; float2 points; int i; int j; // Find the points for the 9 surrounding cells for (i = 0; i < 3; i++) { for (j = 0; j < 3; j++) { int2 cell = floor(UV * cellDensity) + int2(i - 1, j - 1); float2 cellValue = ((hash22(cell)) - 0.5)*0.6 + 0.5; cellValue = (cellValue + (float2)cell) * oneDivCellDensity; points[i + j * 3] = cellValue; } } float closestDist = radius; //for every combination of points in the surrounding 9 cells, find the shortest distance from any line bisecting them for (i = 0; i < 9; i++) { for (j = i + 1; j < 9; j++) { float dist; float2 dir; //// float2 point1 = points[i]; float2 point2 = points[j]; float2 linePoint1 = (point1 + point2) * 0.5; float2 linePointVector = normalize(point1 - point2); float distanceToLine = dot(linePoint1 - UV, linePointVector); float2 pointOnLine = UV + distanceToLine * linePointVector; dist = abs(distanceToLine); dir = linePointVector * sign(distanceToLine); bool lineIsValid = true; float2 toPoint1 = pointOnLine - point1; float2 toPoint2 = pointOnLine - point2; float distSqr1 = dot(toPoint1, toPoint1); float distSqr2 = dot(toPoint2, toPoint2); float minDist = min(distSqr1, distSqr2); for (int k = 0; k < 9; k++) { float2 toCurrentPoint = pointOnLine - points[k]; float distanceToPointSqr = dot(toCurrentPoint, toCurrentPoint); lineIsValid = lineIsValid && (distanceToPointSqr >= minDist); } //// UNITY_FLATTEN if (lineIsValid && closestDist > dist) { direction = dir; closestDist = dist; } } }

// Prevent normalization if our direction is (0,0) direction = all(direction == float2 (0, 0)) ? direction : normalize(direction); return saturate(closestDist / radius); }

For the first for loop, it's pretty self explanatory if you saw the previous explanation. It's finding the Voronoi Points in the 9 surrounding cells and storing them in an array. Only thing to point out is that we need to limit the range from 0.2 to 0.8; this is due to a problem with the algorithm that will require us to sample even MORE points if we don't clamp the range. Otherwise we get problems like this: The second for loop... we have a double nested for loop, that's a little strange. The first loop iterates over i from 0 to 9, and the second one iterates over j from i + 1 to 9. What this is, is the "handshake problem". Imagine there are 9 people and each one needs to shake hands with every other person. No one would shake hands with themselves, and no one needs to shake hands with anyone else twice. This for loop does a "handshake" with every number between 0 and 9, without any number "handshaking" with itself, and no two numbers "handshaking" twice.

But instead of a handshake, it does our algorithm, which is:

1. Take points[i] and points[j], two of our Voronoi Points

2. Find the point halfway between them (linePoint1)

3. Find the normalized vector from one to the other (linePointVector)

4. Find the distance to the line bisecting the two points, by finding the length of the line from our pixel to the midpoint (linePoint1) projected onto the normalized vector from one point to the other (linePointVector) (The length of that purple line is distanceToLine. Keep in mind, there's a chance that distanceToLine can be negative depending on whether our pixel is on the same ; the rest of the code is designed to accommodate for this.)

5. get pointOnLine (the closest point on the line to our pixel), dist (the actual distance) and dir (the direction, as a normalized float2, to the point on the line)

The next thing we have to do is make sure PointOnLine is actually on a valid cell wall. There's a chance that it could be somewhere like this: To do that, we need yet ANOTHER loop (you can see why this has efficiency problems!). The way to do this is to make sure PointOnLine is closer to point1 and point2 than any of our other 9 points. If it is, it's on a valid cell wall, and its distance can be factored in when finding the minimum.

That's all the tricky stuff explained.

Here's the surface function:

void surf (Input IN, inout SurfaceOutputStandard o) { float VoronoiImg = 0; float newRadius = _Radius / _CellDensity; float divFactor = 0; float2 dir; VoronoiImg = saturate(VoronoiNormalized2(_CellDensity, IN.worldPos.xy, newRadius, dir)); float4 c = lerp(_Color1, _Color2, VoronoiImg); o.Albedo = c.rgb; o.Normal = normalize(float3(dir * _NormalHeight, 1)); o.Metallic = _Metallic; o.Smoothness = _Glossiness; o.Alpha = c.a; } Tags: