Tuesday, June 25, 2024
HomeWeb developmentForm Lens Blur Impact with SDFs and WebGL

Form Lens Blur Impact with SDFs and WebGL


Ever since discovering them via The Guide of Shaders, I’ve been captivated by the facility of Signed Distance Capabilities (SDFs). These features are extremely environment friendly for rendering complicated geometries and shapes, and for creating dynamic visible results. They simplify crucial calculations for edge detection and form manipulation, making them excellent for superior graphic purposes.

Though I’ll do my greatest to clarify it additional via utility, I extremely suggest trying out shapes half of The Guide of Shaders and the glorious sources from Inigo Quilez to study extra about SDFs.

On this tutorial, we’ll learn to leverage the facility of SDFs by drawing a easy form (a rounded rectangle) and discover how we will manipulate the properties and parameters of SDF features to create a “Lens Blur” impact on interplay. This requires some information of Three.js or any WebGL framework, in addition to familiarity with GLSL shading language for writing shaders.

Setup

To start, we’ll arrange a fundamental WebGL scene utilizing Three.js. This scene will embody a airplane, which can function our canvas the place the shader and subsequent results shall be utilized.

const scene = new THREE.Scene();

let width = window.innerWidth;
let top = window.innerHeight;

const side = width / top;
const digicam = new THREE.OrthographicCamera(-aspect, side, 1, -1, 0.1, 1000);

const renderer = new THREE.WebGLRenderer();
doc.physique.appendChild(renderer.domElement);

const geo = new THREE.PlaneGeometry(1, 1);  // Scaled to cowl full viewport
const mat = new THREE.ShaderMaterial({
  vertexShader: /* glsl */`
    various vec2 v_texcoord;
    void principal() {
        gl_Position = projectionMatrix * modelViewMatrix * vec4(place, 1.0);
        v_texcoord = uv;
    }`,
  fragmentShader: /* glsl */`
    various vec2 v_texcoord;
    void principal() {
        gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
    }`
});
const quad = new THREE.Mesh(geo, mat);
scene.add(quad);

digicam.place.z = 1;  // Set appropriately for orthographic

const animate = () => {
  requestAnimationFrame(animate);
  renderer.render(scene, digicam);
};
animate();

Then we will begin writing our fragment shader.

various vec2 v_texcoord;
void principal() {
    vec2 st = v_texcoord;
    vec3 colour = vec3(st.x, st.y, 1.0);
    gl_FragColor = vec4(colour.rgb, 1.0);
}
Our first quad with a fundamental shader

Draw form with SDF operate

At its core, SDF is predicated on the idea of calculating the shortest distance from any level in house to the floor of a form. The worth returned by an SDF is optimistic if the purpose is outdoors the form, destructive if inside, and 0 precisely on the floor.

Visible instance of pixel size for circle SDF (from this video)

Right here is how we outline an SDF operate for a rounded rectangle, tailored from Inigo Quilez’s strategies discovered on his distance features tutorials:

/* sdf operate for spherical rectangle */
float sdRoundRect(vec2 p, vec2 b, float r) {
  vec2 d = abs(p - 0.5) * 4.2 - b + vec2(r);
  return min(max(d.x, d.y), 0.0) + size(max(d, 0.0)) - r;
}
void principal() {
    vec2 st = v_texcoord;
    float roundness = 0.4;
    float sdf = sdRoundRect(st, vec2(dimension), roundness);;    
    vec3 colour = vec3(sdf);
    gl_FragColor = vec4(colour.rgb, 1.0);
}`

With the rounded rectangle SDF outlined, we will now manipulate its look by making use of stroke or fill results primarily based on the gap values returned by the SDF:

various vec2 v_texcoord;

float sdRoundRect(vec2 p, vec2 b, float r) {
  vec2 d = abs(p - 0.5) * 4.2 - b + vec2(r);
  return min(max(d.x, d.y), 0.0) + size(max(d, 0.0)) - r;
}

/* Signed distance drawing strategies from lygia shader library: 
- https://lygia.xyz/draw/fill
- https://lygia.xyz/draw/stroke
 */
float stroke(float x, float dimension, float w, float edge) {
    float d = smoothstep(dimension - edge, dimension + edge, x + w * 0.5) - smoothstep(dimension - edge, dimension + edge, x - w * 0.5);
    return clamp(d, 0.0, 1.0);
}
float fill(float x, float dimension, float edge) {
    return 1.0 - smoothstep(dimension - edge, dimension + edge, x);
}

void principal() {
    vec2 st = v_texcoord;
    
    /* sdf Spherical Rect params */
    float dimension = 1.0;
    float roundness = 0.4;
    float borderSize = 0.05;
    
    float sdf = sdRoundRect(st, vec2(dimension), roundness);
    sdf = stroke(sdf, 0.0, borderSize, 0.0);
    
    vec3 colour = vec3(sdf);
    gl_FragColor = vec4(colour.rgb, 1.0);
}`

This allows dynamic interplay, akin to adjusting the fill degree of the form in response to mouse motion. By means of these steps, you will note how manipulating the ‘cut-off’ distance of the SDF can alter the visible output to create our impact.

Bringing Interactivity

To make our SDF drawing interactive, we move the normalized mouse place into the shader. By calculating the gap between the mouse coordinates and the place throughout the shader, we will dynamically alter the fill parameters of the SDF drawing. The purpose is that because the mouse strikes additional away, the form turns into much less sharp, revealing extra of the distinct gradient attribute of the SDF:

const vMouse = new THREE.Vector2();

/* retailer mouse coordinates */
doc.addEventListener('mousemove', (e) => vMouse.set(e.pageX, e.pageY));

/* add uniforms within the shader */
uniforms: {
  u_mouse: { worth: vMouse },
  u_resolution: { worth: vResolution }
}

Simply as we drew our SDF rectangle, we will equally introduce a SDF circle, utilizing the mouse coordinates inside our shader to dynamically affect its place:

various vec2 v_texcoord;
uniform vec2 u_mouse;
uniform vec2 u_resolution;
uniform float u_pixelRatio;

float sdCircle(in vec2 st, in vec2 heart) {
  return size(st - heart) * 2.0;
}
float fill(float x, float dimension, float edge) {
    return 1.0 - smoothstep(dimension - edge, dimension + edge, x);
}
void principal() {
    vec2 st = v_texcoord;
    vec2 pixel = 1.0 / u_resolution.xy * u_pixelRatio;
    vec2 posMouse = vec2(1., 1.) - u_mouse * pixel;
  
    float circleSize = 0.3;
    float circleEdge = 0.5;
    float sdfCircle = fill(
        sdCircle(st, posMouse),
        circleSize,
        circleEdge
    );
    
    float sdf = sdfCircle;
    vec3 colour = vec3(sdf);
    gl_FragColor = vec4(colour.rgb, 1.0);
}

By combining the outcomes of the SDF circle with the parameters of the border, we obtain our remaining visible impact:

/* sdf spherical rectangle with stroke params adjusted by sdf circle */
float sdf;
sdf = sdRoundRect(st, vec2(dimension), roundness);
sdf = stroke(sdf, 0.0, borderSize, sdfCircle) * 4.0;

vec3 colour = vec3(sdf);
gl_FragColor = vec4(colour.rgb, 1.0);

Totally different shapes and past

Utilizing the identical rules utilized in our preliminary examples, this system might be prolonged to nearly any form, providing limitless potentialities with SDFs. I once more suggest revisiting Inigo Quilez’s articles for a wide range of shapes that may be simply applied. Moreover, these might be creatively utilized to icons or textual content, additional increasing the chances.

Try some completely different variations within the demo.

RELATED ARTICLES

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Most Popular

Recent Comments