Suricrasia Online

Welcome to Suricrasia Online!

"Connecting your world, whenever!"

Useful functions for shader live coding

Here is a list of some GLSL functions that I find to be very useful when shader live coding. For each function I've also described how I memorize them. This assumes an understanding of GLSL functions. Hope this helps!

Rotate point around arbitrary axis

vec3 erot(vec3 p, vec3 ax, float ro) {
  return mix(dot(ax, p)*ax, p, cos(ro)) + cross(ax,p)*sin(ro);
}

This function returns the point p rotated ro radians around the axis ax. This rotation is centered around the origin. Note that ax must be normalized. Below is a figure illustrating the action of the function. p' is the return value.

Memorization

I find the best way to memorize something is to understand how it works. For that I think it is handy to explain each term independently. The first term is mix(dot(ax, p)*ax, p, cos(ro)). This is a mix of two points in 3d space. Let's drill deeper and examine where these points are. Here is a diagram of the players:

Here we see that dot(ax, p)*ax is the projection of the point p onto the vector ax. For ease of explanation, we'll start calling this point q.

Moving on, our term becomes mix(q, p, cos(ro)). Let's forget the cos part for now, and imagine an arbitrary mix value of x instead. Whenever we have a mix between two points in 3d space, what we get is a point on the line between them. The mix value is the "index" on that line. for x = 0, we get q, for x = 1 we get p, and any value between 0 and 1 we get a point on the line between p and q

You will also see that if we have x = -1, then we get a point on the line opposite q. This is the same distance that p is from q, it is just on the opposite side. Turns out, this point is p rotated 180 degrees around the axis, since cos(180°) = -1.

Since this term always produces a point along the line between p and q, the second term must push the point off this line. Depicted below is the arc that the point p will follow when rotated 360 degrees. This forms a circle that is perpendicular to the axis of rotation ax.

Let's consider the case where our angle is 90°. The first term will give us the point q, since cos(90°) = 0. The cross(ax,p) part of the term is just what we need to push q to lie on the circle. And because sin(90°) = 1, we get the whole vector added to q.

As a matter of fact, if we imagine looking at this circle from the top, then we'll find the standard parametrization of the circle with sine and cosine.

The exact order of the arguments in the cross call is slightly important. It decides if the point is rotated in a clockwise or anti-clockwise direction. This is usually unimportant, and I don't expect myself to remember. If you think it is important, you should check which direction the version you memorized rotates.

If that explanation did nothing for you, perhaps wikipedia will help. This is actually the Rodrigues' rotation formula, I just size-optimized it for GLSL.

Random unit vector

vec3 rndunit(float seed) {
  return normalize(tan(hash3(seed)));
}

This function generates a random unit vector from a given seed. It depends on a function called "hash3" which generates a random vec3 in the range [-1, 1]. How you generate the random vector is up to you.

Memorization

This is probably the easiest to remember, because it's just slightly different than the naive approach of normalizing the vector directly. Since we are mapping a vector in a box to a vector on the sphere, what we get are vectors that are clumped up along the edges and corners of the cube. The tan call is essentially a correction factor to account for this problem. Pictured below are 2000 generated vectors mapped onto the sphere. On the left is normalizing without using tan, on the right is.

Mathematically speaking, the way this works is by making the random vector more gaussian. When we normalize a gaussian distributed vector, we get a uniformly random unit vector. The tan function just so happens to redistribute the uniformly random components so that they get closer to a gaussian distribution.

Random rotation around the origin

vec3 rndrot(vec3 p, vec4 rnd) {
  return erot(p, normalize(tan(rnd.xyz)), rnd.w*acos(-1));
}

This function uses the erot function defined in the last section. The rnd parameter is four random values in the range [-1, 1]. How you produce this random variable is up to you.

Memorization

A random rotation can be generated by choosing a random axis of rotation, and a random amount of rotation. To get the axis of rotation, we simply use the trick outlined in the section above. For the random rotation, we multiply the fourth random component by acos(-1), which gives us pi. If you wish, you can multiply by pi directly if you can remember its digits and want to look cool on stream.

A wild way to calculate normals

vec3 norm(vec3 p) {
  mat3 k = mat3(p,p,p) - mat3(0.001);
  return normalize(scene(p) - vec3(scene(k[0]),scene(k[1]),scene(k[2])) );
}

Some might rightly find that this way of computing normals is really silly, but I think it is less prone to typo than the standard way. Here I am using an epsilon value of 0.001. The function call to scene refers to whatever signed distance function you define. I usually call mine "scene."

Memorization

The main gimmick of this function is that it takes advantage of some lesser known behaviour for mat3. When we pass a scalar into its constructor, what we get is the identity multiplied by that scalar. In other words, it creates a matrix that is zero except for the diagonal, where it is the value you passed in. When we pass in three vectors to the constructor, it creates a matrix where those three vectors are the columns.

Since matrix subtraction is element-wise, what we get is a matrix k:

The columns of this matrix are exactly the vectors we want when doing finite differences. We can grab these columns using the array accessor k[i], where i is between 0 and 2. We then pass each of these columns into our signed distance function, take the difference with the signed distance directly at p, then normalize.

A hash function you can trust

#define FK(k) floatBitsToInt(cos(k))^floatBitsToInt(k)
float hash(vec2 p) {
  int x = FK(p.x); int y = FK(p.y);
  return float((x-y*y)*(x*x+y)-x)/2.14e9;
}

Coming up with a hash function for vec2 is a time honored tradition. Often times this is done by passing it into the sine function, multiplying by a large value, and taking the fractional component. The idea is that the least significant bits of the sine function are random enough for use in a shader. Below is an example by Dave_Hoskins of shadertoy:

float hash(vec2 p) {
  return fract(sin(dot(p, vec2(12.9898, 78.233))) * 43758.5453);
}

Although small, this technique has drawbacks. The main problem is that the function will stop looking random if the range of p is too large or too small. This is a consequence of the dot product and multiplication factors in the function. If our range is so small that the factors are nullified, what we end up with is some weird aliasing artifacts. If it is too large, we have problems with numerical stability. Below are some images of this function at different scales:

The 2^x value in the corner is the value that the screen coordinates were multiplied by before being passed into the hash function.

Now this might not seem so bad, we just have to bear it in mind. However, the other thing is we cannot expect ourselves to memorize the magical numbers exactly, and instead we'll just invent some new magic numbers that have the same order of magnitude. Unfortunately, this will change the range where the function looks adequately random. Here is the function again, but with different random constants of the same magnitude:

Oops! The range where the hash function looks good decreased substantially! Again, you might not think this is a big deal, but I want peace of mind with my hash functions. This is why I designed my own which is included here. It produces good randomness for all ranges, from the largest floating point values to the smallest. You can play with the function at this shadertoy.

Memorization

Let us examine each line independently.

#define FK(k) floatBitsToInt(cos(k))^floatBitsToInt(k)

This define creates the macro FK(k) which in my mind stands for "FucK(k)." That is, it will fuck up the float k and return an integer. It does this by xoring (the ^ operator) two floatBitsToInt calls. This floatBitsToInt function is rarely used in the wild, so it will serve us well to remember it. I like to use a little song:

The reason we use both cos(k) and k is to reap the benefits of getting some pseudo-randomness from the least significant bits of the cosine function, much like the regular hash function implementation. This is especially important when the values we pass into the hash function are integers. Without it, the values we get are not random at all. Below are images of the has function restricted to the integers. On the left we don't xor with cos, on the right we do.

Huge difference! So remember cos! The reason we want to use k as well is for when it is so small that cos(k) always returns 1.

float hash(vec2 p) {
  int x = FK(p.x); int y = FK(p.y);

The next two lines are relatively self evident. We're just applying FK to the two components of the p vector, and setting them to int variables.

  return float((x-y*y)*(x*x+y)-x)/2.14e9;

This is the meat of the function. What we're doing is throwing our two ints into a special polynomial function, then converting to a float and mapping the result to the range (-1, 1). This is done by dividing the converted float by 2.14e9. This is the only constant you need to remember. The way I remember it is "pi - 1, e9", because for some reason the digits of pi take up a lot of room inside my mind.

The polynomial itself can take multiple forms, but the important part is it looks like (x*x±y)*(y*y∓x)+?, where ? is either x or y, does not matter. Notice that the ± and ∓ are flipped. If you add y in the first part, you must subtract x in the second part, and vice versa. This is to break symmetry. Otherwise we get hash(x,y) ≈ hash(y,x). Adding x or y to the whole thing also helps with this problem.

Distance from point to a line segment

float linedist(vec3 p, vec3 a, vec3 b) {
  float k = dot(p-a, b-a)/dot(b-a,b-a);
  return distance(p, mix(a, b, clamp(k, 0, 1)));
}

This returns the distance from point p to the line segment with the endpoints a and b. This is probably my favorite function thanks to its sheer usefulness for SDF modeling. It is also dimension-agnostic. You can change the vec3 to vec2 in the function declaration and it will become the line distance in 2 dimensions.

Memorization

To make this clearer, I'm going to add one extra line to the function which should explain how it goes about computing the line distance:

float linedist(vec3 p, vec3 a, vec3 b) {
  float k = dot(p-a, b-a)/dot(b-a,b-a);
  vec3 closest_point_on_line = mix(a, b, clamp(k, 0, 1));
  return distance(p, closest_point_on_line);
}

The function finds the point on the line that is closest to p, and then returns the distance to it. This is done by projecting the point onto the line, and clamping it's projection so it is between the endpoints. If we excluded the clamp(k, 0, 1) code and just passed in k, what we'd get is the closest point on the infinite line between a and b. This is also a useful function to know.

The actual memorization of this is rather annoying because of the dot(p-a, b-a)/dot(b-a, b-a) part. I find that I forget if it is b-a or a-b. Here's a simple way to remember it. While typing out this function, just "BAAA" like a sheep to yourself. Problem solved. You can even memorize the whole line this way. "PAA! BAA!", then "BAA BAA!!!"

This is you now.

You can remember the rest by recalling that a mix of two points in 3d space is on the line between them. So we need to clamp k to 0 and 1, and mix a and b so that we get a point between the two of them. Then take the distance between the resulting point and p and we're done!