Read Time:6 Minute, 11 Second

While dancing to one of my favorite dj last weekend, I was looking around some VFX projection that followed the music and got lost into thinking how they were build. They are many ways to create those kind of video-loops, but I will use what I love the most: 2 triangles and and a fragment shader. For the purists, please note that we will use texture in this article, not that I doubt once could achieve creativity without them, but because they save a lot of times when exploring new ideas for outputs.

We will build some concept to produce to create those type of animations.

### The ground we will explore

Let’s start by reviewing some concepts before diving in. First let’s precise where we are into the whole rendering pipeline: We are in the fragment shader, meaning that rasterization has been done on all the vertices of our buffer(We could indeed push it further into whole 3D scene, but this might me be for a later post).

I am dealing here with a simple of 2 triangles filling the whole screen.

This article is not about the fragment shader, but rather how to use it to create those kind of effect showed in the article header. The idea is simply to deal with the whole screen as a painting canvas to start painting stuff.

So for a simple all-white screen, we start with this code:

void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
vec2 uv = fragCoord/iResolution.xy;
vec3 col = vec3(1.0f);
fragColor = vec4(col,1.0);
}

At this point, we have UV, and that’s pretty much all we need to start building our creative imagery! Let’s show those uv by putting them into the col variable.

To show the uv.x ->

void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
vec2 uv = fragCoord/iResolution.xy;
vec3 col = vec3(uv.x);
fragColor = vec4(col,1.0);
}

To show the uv.y ->

void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
vec2 uv = fragCoord/iResolution.xy;
vec3 col = vec3(uv.y);
fragColor = vec4(col,1.0);
}

### Remap uv and query texture

For UV

This part is quite straigth to the point, we simply want to remap the UV from -1 to 1 instead of 0 to 1. This way, our (0,0) coordinate will be at the center of the screen.

To show all the uvs I made the X and Y according to the red and green channel of the output color, thus I added a blue constant.

void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
vec2 uv = (fragCoord - .5 * iResolution.xy) /iResolution.y;
vec3 col = vec3(uv.x, uv.y,.12);
fragColor = vec4(col,1.0);
}

Texture sampling

Let’s query a texture, I will use the BrickTexture from Shadertoy and place it in iChannel0.

To query it into code:

void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
vec2 uv = (fragCoord - .5 * iResolution.xy) /iResolution.y;
vec3 col = vec3(uv.y);
vec3 theTexture = texture(iChannel0, uv).rgb;

fragColor = vec4(theTexture,1.0);
}

Note: When sampling a texture, you should use 0 to 1 UV from the surface you want to cover with a texture, as we modified the UV we are using to query the texture, we do not have the same result, but thus got the texture on screen. For creating creative looping animation, it might not impact the general idea of the creative pipeline, as we are using texture to explore and create, but remember the idea into a more “production client” project.

Also, remember that UV are packed differently from solution to solution, especially if you are dealing with lower level API. This image show a way UV might be store in the vertex buffer.

Anyways, let’s get back to our business, here is how the texture is now showed to the screen.

Awesome, we now have a texture 🙂 On the screen, with dynamics UV on wich we can sample it!

### Make it Polar Coordinate

To make those kind of circular motion, we want to use polar coordinate: If you are new to those concept, you here are some links:

https://www.ronja-tutorials.com/post/053-polar-coordinates/

In short, Polar Coordinates gives us a way to deal with angle and distance from the center on wich we measure those. As we dealt with the center of the screen as being (0,0), we can create new sets of U and V (polar coordinates one) using the following code:

vec2 polarUV = vec2(atan(uv.x, uv.y), length(uv));

We now have a new set of UV, generated by what we computed in part 1.

Let’s see what it gives us:

We can then query our texture with those new PolarCoordinated UV.

void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
vec2 uv = (fragCoord - .5 * iResolution.xy) /iResolution.y;
vec2 polarUV = vec2(atan(uv.x, uv.y), length(uv));
vec3 theTexture = texture(iChannel0, polarUV).rgb;

vec3 col = vec3(theTexture);

fragColor = vec4(col,1.0);
}

We are now reaching a bit more what we are building. By using Polar Coordinates, it then makes circular texture sampling a bit more oriented on what we are wanting to build.

Here is the result of the shader we are building!

### Moving the texture by changing the UV

Let’s change the sampling of the texture by applying some time modification, meaning that when time increase, the texture is sampled differently.

void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
vec2 uv = (fragCoord - .5 * iResolution.xy) /iResolution.y;
vec2 polarUV = vec2(atan(uv.x, uv.y), length(uv));
float movingFactor = .15;
vec3 theTexture = texture(iChannel0, polarUV -(iTime * movingFactor)).rgb;

vec3 col = vec3(theTexture);

fragColor = vec4(col,1.0);
}

We now have a decent loop to work with!

### Masking unwanted Pixel

While we have a looping and dynamic sampling, we now want to mask some of the pixel… The idea is to remove some colors that are over or under a certain spot.

void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
vec2 uv = (fragCoord - .5 * iResolution.xy) /iResolution.y;
vec2 st = vec2(atan(uv.x, uv.y), length(uv));
vec3 tt = texture(iChannel0, st - (iTime * .15)).rgb;
//vec3 col = tt * vec3(length(uv * 2.9 + ((tt.xy * sin(iTime)) * cos(iTime))));
vec3 col = tt;
fragColor = vec4(vec3(1.2, 1., .9) - (col * 2.5),1.0);
}

We can now see the technique being applied in this line:

    vec3 col = tt;
fragColor = vec4(vec3(1.2, 1., .9) - (col * 2.5),1.0);

We crank the color texture and remove from it some lower RGB values.

At this point, it is all about creativity. When it gets to zero it’s black, and when it’s over 1 it’s white, to all the channel.

What about making a boosting effect from an object ?

Let’s make a fire effect from a spaceship into space.

Full code:

vec2 rotate(vec2 pos, float angle)
{
float c = cos(angle);
float s = sin(angle);

return mat2(c,s,-s,c) * pos;

}

void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
vec2 uv = (fragCoord - .5 * iResolution.xy) /iResolution.y;

uv.x += sin(iTime) * .5;
uv.y += sin(iTime) * .2;
uv = rotate(uv, iTime);

vec2 st = vec2(atan(uv.x , uv.y), length(uv));
st -= .02;
vec3 tt = texture(iChannel0, st - (iTime * .55)).rgb;
vec3 col = tt * vec3(length(st * 1.5 + ((tt.xy * sin(iTime)) * cos(iTime))));
fragColor = vec4(vec3(1.2, 1., .9) - (col * 2.5),1.0);
} 5 Star
0%
4 Star
0%
3 Star
0%
2 Star
0%
1 Star
0%