Shader Adventures: 2D basics

By Victor Gridnevsky
Dec. 17, 2019

In this article, I will discuss basic operations on 2D space, which are easy to write and useful when one needs to manipulate output image.

First, we need some background to change. Let's calculate it instead of using a texture; I will use a grid to show the examples. Doing this will tell us more about the math we will need later.

By manipulating Cartesian coordinate values, we will scale, rotate and shift our grid. Possible with shifting pixel coordinates, but less convenient.

We should not forget about aspect ratio. Our screen's width and height is different. So when we have our formulas for X and Y to get the cells, these will appear stretched.

wrong_aspect_ratio.png

To fix that, we should calculate UV coordinates differently from ShaderToy's basic example.

We divide wider area by the narrower and multiply the narrower one. Our screens are horizontally wide, so UV calculation code changes.

// horizontal resolution is iResolution.x
// vertical resolution is iResolution.y
uv.x = uv.x * (horizontal_resolution / vertical_resolution);

We can also write normalized coordinates vector with aspect ratio considered from the beginning, though it is not as compact as before.

vec2 uv = vec2(
    fragCoord.x / iResolution.y,
    fragCoord.y / iResolution.y
);

Now that we fixed the aspect ratio, you might wonder how do we even draw the grid.

And to do that, we check if a given square inside our UV coordinates, i.e. X and Y, is even or odd, or black and white colors.

Let's take a look at the Cartesian coordinates we are working with (original image taken from Wikipedia article).

600px-Cartesian-coordinate-system.svg.png

We can use only horizontal or vertical coordinate, for example, 2 for (2, 3), but that's how we'll get stripes. Instead, we can simply sum X and Y coordinates and check if the number is odd or even by dividing the result by two and checking the remainder.

If the division remainder, i.e. the result of modulo operator is zero, it's an even number.

// Even / odd detector for coloring a grid cell
float f = mod( p.x + p.y, 2.0 );

By using a floor operator, we take integer part of the coordinates. If it's written like floor(uv), and we'll only have our first square separated from everything else.

vec2 p = floor(uv);
vec3 col = vec3(p.x);
2_values.png

So instead, we write multiply uv inside floor operator like this to get more different integer values: floor( uv * 10.0 ).

Let me show you the new range we get by dividing integer values, but I won't focus on that too much.

vec2 p = floor(uv*10.0);
vec3 col = vec3((p.x+p.y)*0.035);
range.png

Now we have a basic idea about drawing black-and-white grid.

void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
    // UV with aspect ratio checked
    vec2 uv = vec2(
        fragCoord.x / iResolution.y,
        fragCoord.y / iResolution.y
    );
    // Multiplied integer values for each point
    // on Cartesian coordinates
    vec2 p = floor(uv*10.0);
    // Even / odd detector for coloring a grid cell
    float f = mod( p.x + p.y, 2.0 );
    // Color output
    fragColor = vec4(vec3(f), 1.0);
}
grid.png

We might also want to make colors more interesting. So, I picked a color and make its brightness shift. I picked a minimum “level” of this color, 0.6 and control the remainder (0.4) by multiplying with a sin function from time.

vec3 col = vec3(0.27,0.46,0.43) * (0.6 + abs(sin(iTime))*0.4);

In the end, we have this minimal code:

void mainImage( out vec4 fragColor, in vec2 fragCoord ) {
    // Normalized pixel coordinates
    vec2 uv = vec2(
        fragCoord.x / iResolution.y,
        fragCoord.y / iResolution.y
    );
    // Multiplied integer values for each point
    // on Cartesian coordinates
    vec2 p = floor(uv*10.0);
    // Even / odd detector for coloring a grid cell
    float f = mod( p.x + p.y, 2.0 );
    // Selects fragment shader color
    vec3 col = vec3(1.,1.,0.1) * (0.6 + abs(sin(iTime))*0.4);
    // Store point's color
    fragColor.rgb = vec3( f*col );
}

Shift

Now, we can finally start doing something with this code! Let’s try to shift the grid horizontally!

How to do that? Well, right after UV definition, we pick uv.x, a horizontal component of UV vector, and add something.

I am adding sin(iTime), because it will give more or less smooth shifts all the time. The line that interests us is:

// Sinusoidal shift by X axis
uv.x += sin(iTime);

It is also possible to get the absolute value for the sine function using abs(sin(iTime)), so there’s no negative output.

The full example looks like this:

void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
    // Normalized pixel coordinates
    vec2 uv = vec2(
        fragCoord.x / iResolution.y,
        fragCoord.y / iResolution.y
    );

    // Shifts
    uv.x += sin(iTime);
    // alternative:
    // uv.x += abs(sin(iTime));

    // Multiplied integer values for each point
    // on Cartesian coordinates
    vec2 p = floor(uv * 10.0);

    // Even / odd detector for coloring a grid cell
    float f = mod( p.x + p.y, 2.0 );

    // Selects fragment shader color
    vec3 col = vec3(1.,1.,0.1) * (0.6 + abs(sin(iTime))*0.4);
    // Store point's color
    fragColor = vec4(f * col, 1.0);
}

Now it's your turn to play around: try to change this code in ShaderToy. Use uv.y in pan, shifting an image using cos or any other periodic function.

Done? Good.

Circular shifting

Moreover, it's possible to shift the area around a point in a circular motion using basic trigonometry: sin for X axis value, cos for Y axis value and vice versa for the reverse direction.

void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
    // Normalized pixel coordinates
    vec2 uv = vec2(
        fragCoord.x / iResolution.y,
        fragCoord.y / iResolution.y
    );

    // Shifts
    uv.x += sin(iTime);
    uv.y += cos(iTime);

    // Multiplied integer values for each point
    // on Cartesian coordinates
    vec2 p = floor(uv * 10.0);
    // Even / odd detector for coloring a grid cell
    float f = mod( p.x + p.y, 2.0 );

    // Selects fragment shader color
    vec3 col = vec3(1.,1.,0.1) * (0.6 + abs(sin(iTime))*0.4);
    // Store point's color
    fragColor = vec4(f * col, 1.0);
}

Testing

Do you have any doubts about shifting the space using trigonometry? If so, let’s show the same concept with another piece of code. It has a function to draw a circle at a given center, it can draw both empty and filled circles.

It will move our circle using sin and cos from iTime.

This code is drawing a circle, trajectory and background separately and then returns the overlapped color for each viewport pixel.

// Draws a circle.
// Returns a float value for a given pixel color.
// Taken from https://www.shadertoy.com/view/MddfzN
float circle(vec2 uv, vec2 p, float r, float blur) {
    float d = length(uv-p);
    float c = smoothstep(r, r-blur, d);
    return c;
}

// Draws a disc using circles, based on a given circle center,
// radius and blur
float disc(vec2 uv, vec2 p, float r, float blur, float width) {
    float disc = circle(uv, p, r, blur) - circle(uv, p, r-width, blur);
    return disc;
}

void mainImage( out vec4 fragColor, in vec2 fragCoord ) {
    // Normalized pixel coordinates
    vec2 uv = vec2(
        fragCoord.x / iResolution.y,
        fragCoord.y / iResolution.y
    );

    // Center of image with
    // aspect ration taken into account
    vec2 center = vec2(
        0.5*(iResolution.x / iResolution.y),
        .5
    );
    
    // Draws a border to show circle movement
    float border = disc(uv, center, 0.26, 0.01, 0.008);
    
    // Indicator, positioned at sin and cos of iTime
    vec2 indicator_coord = center + vec2(sin(iTime)*0.25, cos(iTime)*0.25);
    float indicator = circle(uv, indicator_coord, 0.025, 0.01);
    
    // Resulting color
    vec3 col = vec3(.0) + border + indicator;
    
    // Store point's color
    fragColor.rgb = vec3( col );
}

Rotation

In previous examples, the area spins, but it can spin more: we haven’t done the image rotation itself yet.

The way we are doing the rotation is not that different from the shifts before. While doing the rotation, we recalculate coordinates so position of grid sectors changes.

We define rotation for UV like that:

#define ROT(x) mat2(cos(x), -sin(x), sin(x), cos(x))
uv *= ROT(sin(iTime * .2)* PI);

Let’s apply it to the code:

#define PI 3.1415926535
#define ROT(x) mat2(cos(x), -sin(x), sin(x), cos(x))

void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
    // Normalized pixel coordinates
    vec2 uv = vec2(
        fragCoord.x / iResolution.y,
        fragCoord.y / iResolution.y
    );

    // Spin
    uv *= ROT(sin(iTime * .2)* 2.0*PI);

    // Multiplied integer values for each point
    // on Cartesian coordinates
    vec2 p = floor(uv * 10.0);
    // Even / odd detector for coloring a grid cell
    float f = mod( p.x + p.y, 2.0 );

    // Selects fragment shader color
    vec3 col = vec3(1.,1.,0.1) * (0.6 + abs(sin(iTime))*0.4);
    
    // Store point's color
    fragColor.rgb = vec3( f * col );
}

We are using , because trigonometric operators in GLSL expect radian values. For example, OpenGL manual tells us this about the sine function: sin returns the trigonometric sine of angle and now, about the angle itself: the quantity, in radians, of which to return the sine.

It is also easy to rotate around the center. To do that, we'd need to shift UV coordinates.

#define PI 3.1415926535
#define ROT(x) mat2(cos(x), -sin(x), sin(x), cos(x))

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

    // Spin
    uv *= ROT(iTime*0.1 * 2.0*PI);

    // Multiplied integer values for each point
    // on Cartesian coordinates
    vec2 p = floor(uv * 10.0);
    // Even / odd detector for coloring a grid cell
    float f = mod( p.x + p.y, 2.0 );

    // Selects fragment shader color
    vec3 col = vec3(1.,1.,0.1) * (0.6 + abs(sin(iTime))*0.4);
    // Store point's color
    fragColor.rgb = vec3(f * col);
}

Zoom

Now, we can also zoom an image by simply multiplying or dividing uv.x and uv.y values, used by our grid. This way, even / odd detector will calculate cell as wider or smaller, filling its tone accordingly.

Let’s go with the same principle by multiplying both uv.x and uv.y by sin(iTime). It will stretch our image by a given axis.

void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
    // Normalized pixel coordinates
    vec2 uv = vec2(
        fragCoord.x / iResolution.y,
        fragCoord.y / iResolution.y
    );

    // Zoom
    uv *= sin(iTime);

    // Multiplied integer values for each point
    // on Cartesian coordinates
    vec2 p = floor(uv * 10.0);
    // Even / odd detector for coloring a grid cell
    float f = mod( p.x + p.y, 2.0 );

    // Selects fragment shader color
    vec3 col = vec3(1.,1.,0.1) * (0.6 + abs(sin(iTime))*0.4);
    
    // Store point's color
    fragColor.rgb = vec3(f * col);
}

Stretching

Since I mentioned stretching above, I decided I'd add it. As I said, to stretch X or Y coordinate, you multiply it with a number. Let's use abs(sin(iTime)) for a moment, because time-dependent values from 0 to 1 are nice.

// Stretching
uv.x *= sin(iTime);
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
    // Normalized pixel coordinates
    vec2 uv = vec2(
        fragCoord.x / iResolution.y,
        fragCoord.y / iResolution.y
    );

    // Stretching
    uv.y *= sin(iTime);

    // Multiplied integer values for each point
    // on Cartesian coordinates
    vec2 p = floor(uv * 10.0);
    // Even / odd detector for coloring a grid cell
    float f = mod( p.x + p.y, 2.0 );

    // Selects fragment shader color
    vec3 col = vec3(1.,1.,0.1) * (0.6 + abs(sin(iTime))*0.4);
    
    // Store point's color
    fragColor.rgb = vec3(f * col);
}

There's also an interesting way to stretch our images: use initial coordinates inside a sine function to make stretching coordinate-dependent!

// Coordinate-dependent stretching
uv.x *= sin(iTime + uv.x);
// Coordinate-dependent stretching
uv.x *= sin(iTime + uv.y);
// Coordinate-dependent stretching
uv.y *= sin(iTime + uv.x);
// Coordinate-dependent stretching
uv.y *= sin(iTime + uv.y);

I am showing a full code for one of the results I like:

download (18).png
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
    // Normalized pixel coordinates
    vec2 uv = vec2(
        fragCoord.x / iResolution.y,
        fragCoord.y / iResolution.y
    );

    // Stretching
    uv.y *= sin(iTime + uv.x);

    // Multiplied integer values for each point
    // on Cartesian coordinates
    vec2 p = floor(uv * 10.0);
    // Even / odd detector for coloring a grid cell
    float f = mod( p.x + p.y, 2.0 );

    // Selects fragment shader color
    vec3 col = vec3(1.,1.,0.1) * (0.6 + abs(sin(iTime))*0.4);
    
    // Store point's color
    fragColor.rgb = vec3(f * col);
}
// Stretching
uv.y *= sin(iTime);

Tilt effect

It's very easy to add a tilt effect. Just shift each X point by Y coordinate.

I will use sin(iTime) multiplication to show this effect in more detail.

void mainImage( out vec4 fragColor, in vec2 fragCoord ) {
    // Normalized pixel coordinates
    vec2 uv = vec2(
        fragCoord.x / iResolution.y,
        fragCoord.y / iResolution.y
    );

    // Tilt
    uv.x += uv.y * sin(iTime);

    // Multiplied integer values for each point
    // on Cartesian coordinates
    vec2 p = floor( uv*10.0 );

    // Even / odd detector for coloring a grid cell
    float f = mod( p.x + p.y, 2.0 );

    // Selects fragment shader color
    vec3 col = vec3(1.,1.,0.1) * (0.6 + abs(sin(iTime))*0.4);
    
    // Store point's color
    fragColor.rgb = vec3(f * col);
}

Multiple manipulations

We can mix multiple manipulations

#define PI 3.1415926535
#define ROT(x) mat2(cos(x), -sin(x), sin(x), cos(x))

void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
    // Normalized pixel coordinates
    vec2 uv = vec2(
        fragCoord.x / iResolution.y,
        fragCoord.y / iResolution.y
    );

    // Spin
    uv *= ROT(sin(iTime * .2)* PI);
    // Tilt
    uv.x += uv.y * sin(iTime + 1.);
    // Zoom
    uv *= sin(iTime);
    // Shifts
    uv.x += sin(iTime);
    uv.y += cos(iTime);

    // Multiplied integer values for each point
    // on Cartesian coordinates
    vec2 p = floor(uv * 10.0);
    // Even / odd detector for coloring a grid cell
    float f = mod( p.x + p.y, 2.0 );

    // Selects fragment shader color
    vec3 col = vec3(1.,1.,0.1) * (0.6 + abs(sin(iTime))*0.4);
    // Store point's color
    fragColor.rgb = vec3(f * col);
}

Conclusion

We experimented with several effects you can apply for 2D.

We know that we can use these effects in shader graphics (stretching, zooming, rotating ans so on) using a texture similarly, i.e. by changing 2D coordinates we draw things on.

Shaders give you a lot of possibilities for 2D and 3D effects, so I can't recommend enough to look at ShaderToy sources for inspiration. For example, papdis looks fun: it stretches and rotates a texture with different parameters, shifting from the center.

To get back to Shader Adventures cycle, use this link.