-11.2 C
New York
Wednesday, January 22, 2025

WebGL Shader Methods for Dynamic Picture Transitions


WebGL shaders give us unbelievable management over 3D objects, permitting us to creatively coloration every pixel and place vertices in 3D house.

On this article, we’ll harness that energy to create a novel picture reveal impact. We’ll break down the maths behind it into easy, manageable steps, constructing the ultimate outcome piece by piece.

SDF of the circle

Allow us to begin easy by merely drawing a circle on the display. Once we are coping with shaders we wish to take into consideration methods to explain a circle utilizing vectors.

We will use the vector equation of the circle, the place r is any level within the circle, and c is the middle of the circle.

What’s extra necessary than coding the equation, is to grasp the instinct of why this works.

If we give it some thought, a circle might be described as a geometrical object for which each distance ranging from the middle is equal. Because the distance (which is derived by Pythagorean theorem) is the magnitude of the r – c, we are able to use the size perform that’s supplied to us on GLSL language.

Let’s use our UV coordinates to attract a circle. We wish to draw our circle within the heart of the display, subsequently we are going to offset by the middle vector.

void primary() { 
    vec3 bg = vec3(254.0/255.0, 149.0/255.0, 2.0/255.0);
    vec3 fg = vec3(250.0 / 255.0, 229.0 / 255.0, 214.0 / 255.0);

    vec2 ratio = vec2(uSize.x / uSize.y, 1.0);
    
    vec2 c = vec2(0.5, 0.5) * ratio;
    vec2 r = vec2(vUv.x, vUv.y) * ratio;

    float circle = size(r - c); 
    vec3 coloration = combine(bg, fg, circle);

    gl_FragColor = vec4(coloration,  1.0);
}

If we merely visualize this utilizing coloration, we get the next radial gradient. The query is the place is our circle hiding?

We observe a radial gradient that seems darker on the heart and brighter in the direction of the perimeters. This is sensible when you think about how colours work in GLSL as 0 represents black and 1 represents white. Nearer distances to the middle lead to smaller values, which correspond to darker colours. However, bigger distances in the direction of the perimeters produce brighter colours.

We’re not aiming for a radial gradient, as a substitute, we would like a pointy circle. To realize this, we’ll use a step perform, which units all values under a sure threshold to 0 and all values above the edge to 1. This threshold successfully defines the radius of our circle.

void primary() { 
    vec3 bg = vec3(254.0/255.0, 149.0/255.0, 2.0/255.0);
    vec3 fg = vec3(250.0 / 255.0, 229.0 / 255.0, 214.0 / 255.0);

    vec2 ratio = vec2(uSize.x / uSize.y, 1.0);
    
    vec2 c = vec2(0.5, 0.5) * ratio;
    vec2 r = vec2(vUv.x, vUv.y) * ratio;

    float circle = size(r - c);
    circle = step(circle, .25);
    vec3 coloration = combine(bg, fg, circle);

    gl_FragColor = vec4(coloration,  1.0);
}

And right here is the output:

Circle Warping

For our impact, we don’t need a completely sharp circle, as a substitute, we would like a wavy sample alongside its perimeter. To realize this, we are able to apply a noise perform across the circle’s circumference.

Since we don’t require a fancy noise perform, we are able to use a easy and environment friendly mixture of sine and cosine waves. Let’s begin by making a noise perform by combining a number of sine and cosine waves.

Wave 1
Wave 2
Wave 3

Let’s see all of them collectively:

We wish to mix them in a single single perform, add all of them collectively and do some vary normalization. Right here’s what we get:

Our aim is so as to add noise alongside the perimeter of the circle. To do that, we’ll use some trigonometry and apply the perform based mostly on the angle every vector makes in polar coordinates.

float noise(vec2 level) {
    float frequency = 1.0;
    float angle = atan(level.y,level.x);

    float w0 = (cos(angle * frequency) + 1.0) / 2.0; // normalize [0 - 1]
    float w1 = (sin(2.*angle * frequency) + 1.0) / 2.0; // normalize [0 - 1]
    float w2 = (cos(3.*angle * frequency) + 1.0) / 2.0; // normalize [0 - 1]
    float wave = (w0 + w1 + w2) / 3.0; // normalize [0 - 1]
    return wave;
}

float circleSDF(vec2 pos, float rad) {
    float amt = 0.5;
    float circle = size(pos);
    circle += noise(pos) * rad * amt;
    return step(circle, rad);
}

We multiply right here by the radius, in order that the wave can vary between [0 – r] as a substitute of [0 – 1]. We will additional modify utilizing an quantity worth, on this case starting from 0 to most half of the circle radius.

And we get the next SDF, which we are able to tweak additional with the intention to get a sample which satisfies us visually.

Sample 1

If we mess around with the frequency and the radius a bit, we are able to get the next patterns:

Sample 2
Sample 3

I’m glad with the third sample, however now I wish to introduce some movement and add an natural really feel to the circle.

Including Natural Feeling

Rotation

Let’s begin by including some rotation to our circle. On this case, we are able to obtain rotation by offsetting the angle across the circle with the worth of time.

By including the time worth to the angle, the periodic nature of sine and cosine maintains the curve repetition as time will increase.

float noise(vec2 level) {
	//...similar as above
	float angle = atan(level.y,level.x) + uTime * 0.02;
	//...similar as above
}

Moreover, we are able to modify the circles warping by animating the noise depth over time.

float circleSDF(vec2 pos, float rad) {
    float a = sin(uTime * 0.2) * 0.25; // vary -0.25 - 0.25
    float amt = 0.5 + a;
    float circle = size(pos);
    circle += noise(pos) * rad * amt;
    return step(circle, rad);
}

We as soon as once more leverage the repetition of the sine wave, however this time we remap the output values to a spread that fits our visible preferences.

After making use of these steps, the outcome feels natural.

Merging the objects

We wish to mix a number of circles for our transition impact. Within the following sections, we’ll stroll by way of the way to obtain this.

Including A number of Circles

Let’s begin by including two organically shifting objects to the display with completely different offsets.

We will use the max perform to attract each circles.

// circle 1
vec2 o1 = vec2(0.35) * uSize;
float c1 = circleSDF(coords - o1, rad);
   
// circle 2
vec2 o2 = vec2(0.65) * uSize;
float c2 = circleSDF(coords - o2, rad);
	  
// Merging collectively
float circle = max(c1, c2);
vec3 coloration = combine(bg, fg, circle);

This can lead to one thing much like the next:

Smoothing the Merge

You’ll discover that the max perform is merging our shapes, but it surely leads to tough edges, nearly as if the objects are merely layered on prime of each other. Let’s visualize the merging perform on a graph to higher perceive this.

This perform represents the merging of two offset noise capabilities in cartesian coordinates. You possibly can see that the capabilities are merged, however the merging factors are sharp and spiky. We wish to obtain a smoother transition.

float softMax(float a, float b, float okay) {
    return log(exp(okay * a) + exp(okay * b)) / okay;
}

float softMin(float a, float b, float okay) {
    return -softMax(-a, -b, okay);
}

We’ll use an exponential factor-adjusted perform for our mushy min and mushy max capabilities. Whereas we gained’t dive deep into the small print of perform smoothing on this article, you’ll find the reference for the perform within the following article.

Listed below are our waves after merging them utilizing mushy minimums as a substitute:

// circle 1
vec2 o1 = vec2(0.35) * uSize;
float c1 = circleSDF(coords - o1, rad);
   
// circle 2
vec2 o2 = vec2(0.65) * uSize;
float c2 = circleSDF(coords - o2, rad);
	  
// Merging collectively
float circle = softMin(c1, c2, 0.01);
circle = step(circle, rad);

And listed here are our objects merging easily after making use of the mushy merge:

And if we add a zoom impact to our transition, right here’s what we get:

Including Extra objects

We wish our impact to characteristic extra circles on the display. Whereas we may merely add them utilizing hardcoded values for the offsets, which might work nice, I’ll make it extra fascinating by arranging them in a radial sample.

Creating Radial Circles

Let’s begin by drawing radial circles utilizing GLSL, I’ll begin with a fundamental method and can observe up with a efficiency enhancing methodology.

float radialCircles(vec2 ratio) {
    float circleRadius = 0.021;
    vec2 heart = vec2(0.5, 0.5) * ratio;
    vec2 pos = vUv * ratio;
    float d = circleSDF(pos - heart, circleRadius); // heart circle
    
    vec2 offsetVector = vec2(0.0);

    int layerCount = 4;
    int layerItemCount = 12;
    float itemOffsetAngle = (3.1415926535 * 2.0) / float(layerItemCount);
    float layerOffset = 0.5 / (float(layerCount));
    float r = 0.1;

    for (int i = 0; i < layerCount; i += 1) {
        vec2 curr = offsetVector;
        float angle = 0.0;

        for (int j = 0; j < layerItemCount; j += 1) {
            offsetVector = vec2(
                cos(angle) * r,
                sin(angle) * r
            );

            d = max(
                d,
                circleSDF(pos - offsetVector - heart, circleRadius)
            );

            angle += itemOffsetAngle;
        }
        
        r += layerOffset;
    }

    return d;
}

float circleSDF(vec2 pos, float rad) {
    float circle = size(pos);
    circle = step(circle, rad);
    return circle;
}

As proven within the code above, I offset and rotate a vector inside a sector utilizing two for loops. At every step, I draw a circle, ensuing within the following output:

Optimizing GPU Branching

The issue with the above code is that it makes use of a for loop for drawing the circles. Since we shouldn’t have too many circles that might not trigger any large points, however we are able to optimize by utilizing some math and leveraging symmetry:

float radialCircles(vec2 p, float o) {
    vec2 offset = vec2(o, o);

    float depend = 3.0;
    float angle = (2. * 3.1415926535)/depend;
    float s = spherical(atan(p.y, p.x)/angle);
    float an = angle * s;
    vec2 q = vec2(offset.x * cos(an), offset.y * sin(an));
    vec2 pos = p - q;
    float circle = circleSDF(pos, 25.0);
    return circle;
}

After drawing the specified quantity of circles in a radial sample, and including the time animation, we get the next outcome:

The ultimate step is to exchange the strong colours with a picture texture. We will obtain this by sampling the colours of a picture at particular pixels utilizing a texture in GLSL.

Right here is the ensuing animation after including the feel:

void primary() {
    vec4 bg = vec4(vec3(0.0), 1.0);
    vec4 texture = texture2D(uTexture,vUv);
    vec2 coords = vUv * uSize;
    vec2 o1 = vec2(0.1, 0.1) * uSize;

    float t = pow(uTime, 1.5); // easing
    float radius = 15.0;
    float rad = t * radius;
    float c1 = circleSDF(coords - o1, rad);

    vec2 p = (vUv - 0.5) * uSize;
    float r1 = radialCircles(p, 0.1 * uSize.x, 2.0);
    float r2 = radialCircles(p, 0.4 * uSize.x, 5.0);

    float circle = softMin(c1, r1, 0.01);
    circle = softMin(circle, r2, 0.01);

    circle = step(circle, rad);
    vec4 coloration = combine(bg, texture, circle);
    gl_FragColor = coloration;
}

Last Loading

And right here is our remaining utilization of the impact in a artistic setting, the place we reveal the photographs based mostly on the person scroll.

On this article, we explored creating circle SDFs, including noise alongside the circle’s perimeter, combining sine waves, merging objects with easy min and max capabilities, and incorporating textures—together with many different implementation particulars.

If this publish sparked your creativity or helped you degree up your abilities, observe me on LinkedIn and Bluesky for extra artistic content material. Additionally contemplate to purchase me a espresso!

Thanks for being superior!



Supply hyperlink

Related Articles

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Latest Articles