Repeating your Coordinate System

A very useful operation in fragment shader development is repeating a coordinate system. It’s also quite neat since we’re able to draw several shapes without the use of a loop. When working in 3D and raymarching it’s extra awesome because we can fake the repetition of infinite shapes into a space. Here I discuss one method of performing this repetition task.

First let’s create a simple framework:

float sdCircle(vec2 p, float r){
  return length(p)-r;
}

void main(){
  vec2 p = (gl_FragCoord.xy/u_res)-0.5;
  float i = step(sdCircle(p, .5), 0.);
  gl_FragColor = vec4(vec3(i),1);
}

We define a vector for each fragment that ranges from -.5 from the bottom left corner to +.5 to the top right with a zero vector in the middle of the canvas.

We define a function to calculate the distance from p to the center of the circle, using step will make the intensity variable (i) either 0 or 1.

It yields a happy circle:
repeat_circle

Let’s say we want to repeat this circle 4 times. We’ll need to create a vector that repeats across the canvas several times. To help with this, we’ll use mod().

Now the span of the current coordinate system is 1 (-.5 to +.5) so if we want 4 cells, we should use mod(p, .25) ….or better yet mod(p, 1/4) which is slightly more intuitive. We’ll also need to adjust the radius of the circles so they fit in the new cell sizes. Let’s make them half the cell size.

  float cellSz = 1./4.;
  float r = cellSz/2.;
  p = mod(p, vec2(cellSz));
  float i = step(sdCircle(p, r), 0.);

It gives us:
repeat_2.png

Wait, what’s this? Why are the circles cut off and no longer happy?

Well, remember how we used mod? mod returns positive values–in our case: 0 to .25. This means that for the variable p, the bottom left corner is (0,0) and the very top is (.25, .25)

What we actually want is ‘centered’ ranges from vec2(-.125) to vec2(.125), right? But wait! We already did this! At the start of main we divided the fragCoord by resolution then subtracted .5. We just need to do something similar. In this case, subtract half of whatever the cell size is.

  p = mod(p, vec2(cellSz))-vec2(0.5*cellSz);

Now our circles are happy again!
repeat_3

Advertisements

length, distance, sqrt

coonextions.gif

I was really amazed the first time I saw a similar visualization on the Processing homepage…err, like 10 years ago. It’s such a simple concept, but very pretty. Anyway, I finally sat down and gave it a go in glsl. While working with it I played around with the distance calculations and figured I should write a tiny bit about that.

When you need to calculate the distance between two points, you have a few options. You can write a function yourself, or use one of the built-in functions in glsl: distance(), length(), or sqrt(). distance() takes in two vectors. length() accepts 1 vector and sqrt() takes in a float (typically). But sometimes you don’t need the exact distance. Sometimes you’re just trying to find out if the length of a vector is longer than some defined value. In these situations you can use the squared distance instead.

For the squared distance you just need to get the difference between vector A and B then assign to say, C. The squared distance would then be: c.x*c.x+c.y*c.y. Or even: dot(c,c); So basically it’s everything length does without the call to sqrt–which is the computationally expensive part.

The values returned from these two methods (sqrt distance and squared distance) are of course fundamentally different, so you’ll need to adjust the value you are comparing against if you switch between the two.

Deconstructing Voxel Rendering

mc

For someone who is fairly obsessed with pixel art, it isn’t a surprise I really like voxel rendering, too.

I feel I finally reached the point where I have enough knowledge about shader code that I can start to deconstruct voxel shaders to understand them. So I began to do just that. I started with a simple shader on ShaderToy, took it apart and put it back together. I renamed variables, made some changes, simplified some things, etc.

Here is the shader on shadertoy. The original source from where I got the code is posted at the top of the shader.
https://www.shadertoy.com/view/4lKyDW

Okay, so let’s dig in.

Define some constants. Nothing too special here. MAX_STEPS is used for raymarching. Grass and Dirt are colors obviously.

const float MAX_STEPS = 1000.;
const float GrassHeight = 0.8;
const vec3 Grass = vec3(.2, 1, 0);
const vec3 Dirt = vec3(.5,.25, 0);

Just a main(), no magic yet…

void mainImage(out vec4 fragColor, in vec2 fragCoord) {

Okay, we’re raymarching right? That means, we’ll need to define the ray start position. We can set x to whatever. It doesn’t really matter. The y component needs to be a large enough value so we can see the terrain. Z is set to simulation time so that the flyover seems animated.

vec3 rayOrigin = vec3(0., 12, iDate.w*5.);

Normalize the screenspace coords. We’ll need this in a couple places later on.

vec2 pnt = fragCoord.xy/iResolution.xy*2.-1.;

The ray direction. Unlike the ray origin, the ray direction varies slightly for each fragment–spreading out from -1 to 1 on the xy plane. Note z points positive 1.

vec3 rayDir = vec3(pnt, 1.);

Define the final color that we’ll assign to this particular fragment.

vec3 color;

The purpose of depth is to scale the ray direction vector.

float depth = 0.;

Okay, we reached the raymarching loop.

for(float i = .0; i < MAX_STEPS; ++i) { 

Since the top part of the screen space is empty, it makes sense to add a check early on to exit if we know the ray won’t actually hit anything. Of course this might need to be adjusted if the rayOrigin changes.

  if(pnt.y > 0.){
    break;
  }

Pretty standard raymarching stuff. We start with the origin and scale the ray direction a bit longer every iteration. The first iteration depth is zero, so we’d just be using the rayOrigin.

  vec3 p = rayOrigin + (rayDir * depth);

Okay, now we finally come to something that’s necessarily the ‘voxel’ part of the rendering. By flooring p we are ‘snapping’ the calculated raymarched point to the nearest 3D box/grid/cell.
Scaling the floored value by a smaller number will yield a higher ‘resolution’.

  vec3 flr = floor(p) * 0.25;

Okay, let’s take our attention to the wave-like pattern of the terrain. There are peaks and there are valleys. These are of course generated by using cos() and sin(). The waves created are the interactions/addition of the functions along the space.

This is where we decide if we are going to set the color of the fragment. If the floored y value is less then the cos()+sin(), then we want to color the fragment. Otherwise it means the y is higher so keep iterating.

  if(flr.y < cos(flr.z) + sin(flr.x)){
  
  frct = fract(p);
  if(frct.y > GrassHeight){
    color = Grass;
  }
  else{
    color = Dirt;
  }

If we hit a voxel and set the color, there’s no need to keep iterating. Break out of the loop.

    break;
  }// close the if

Depth is used to scale the ray direction, so we increment it a tiny bit every iteration. Since i ranges from 0 to 1000 and since we are scaling i here, the ray direction will scale from .0 to .1

  depth += i * (.1/MAX_STEPS);
}

We need to add some lighting, even if really fake to give the illusion of distance. It also helps to hide some of the aliasing that happens at the very far points in the terrain.

If we exited the loop early on, depth will be a relatively small value which means the fragments will end up being bright.
The farther it took to hit something, the darker that fragment will be. Play around with this value for some interesting lighting effects.

  color *= 1./(depth/5.);
  fragColor = vec4(color, 1);
}

If you understood most of this try substituting the cos() and sin() functions with noise values. It will make some really fun variation in the terrain.

Making Infinity

i2.gif

Creating an infinite-like shape is really easy. Set the x component of the position of the object to be cos(time). This will make the object oscillate back and fourth along the x axis. Then set the y component to use sin(time*2). By multiplying by 2, it doubles the frequency. At this point, you’ll probably want to scale down the amplitude of the y since the infinity shape is longer than it is tall. Try values between 2 or 3:
y = sin(time*2)/2.5

What happens if you set x=cos(time) and y=sin(time)? Try it out! Play around with the values to see what other fun patterns you can make!

mix()

ezgif-5-b890251356.gif

mix() is a glsl function that allows you to linearly interpolate between two values. The math behind it is straightforward and I use it a lot for colors and vectors–but when applied to SDFs—well it’s almost magic the way it shape-shifts between two objects. With just 1 line of code!

Take a look:
mix(Box(p, size), Sphere(p, rad), sin(time));

The third variable in mix can be all sorts of interesting things: time, fractional part of time, sin, cursor position, z position in a scene,….It’s fun to play around with values that might not seem like they should be passed into the function. 😛

You can learn more about mix() on the Khronos reference pages:
https://www.khronos.org/registry/OpenGL-Refpages/gl4/html/mix.xhtml