Thursday, June 12, 2025
HomeProgrammingMaking a Bulge Distortion Impact with WebGL

Making a Bulge Distortion Impact with WebGL


Distortion in WebGL will be a whole lot of enjoyable. Not too long ago, I needed to create a particular impact known as “Bulge” for a mission on the Upperquad.com web site. Here’s a brief video showcasing it.

The thought was to create a distortion that originates from some extent and pushes all the pieces round it. In “Adobe After Results,” you may obtain this utilizing the “Bulge Impact.” It lets you “develop” a particular space of a picture, much like blowing right into a bubble to make it broaden. This impact can create some amusing and attention-grabbing visuals.

You can too reverse this impact.

On this fast tutorial, we’ll discover how you can create a bulge impact on a texture utilizing OGL, a light-weight WebGL library developed by Nathan Gordon. To realize this impact, you’ll want a primary understanding of JavaScript, WebGL, and shaders. Nevertheless, for those who desire, you may as well obtain the demo straight and experiment with the code.

1. Getting began

First, let’s create a primary 2D WebGL scene utilizing OGL. You’ll be able to obtain and begin from this template repository, which incorporates all the pieces you want.

Inside this template, you have already got an HTML canvas tag with CSS to make it fullscreen. In JavaScript, you’ve gotten the setup of a Renderer, a Program with its shaders and uniforms, and a mesh that’s being rendered at roughly 60 frames per second. The template additionally features a resize perform. The shaders of this system show an animated gradient background. Right here they’re:

// src/js/glsl/most important.vert
attribute vec2 uv;
attribute vec2 place;
various vec2 vUv;

void most important() {
  vUv = uv;
  gl_Position = vec4(place, 0, 1);
}
// src/js/glsl/most important.frag
uniform float uTime;
uniform vec3 uColor;
uniform float uOffset;
various vec2 vUv;

void most important() {
  gl_FragColor.rgb = 0.3 + 0.3 * cos(vUv.xyx * uOffset + uTime) + uColor;
  gl_FragColor.a = 1.0;
}

PS: In OGL, Nathan Gordon’s concept is to create a fullscreen Mesh within the scene by drawing only one triangle as a substitute of two (as you’d sometimes do to create a rectangle). Then, the UVs are used to map the display of your window. Basically, it’s like having a sq. inside a triangle (as proven on the appropriate) to save lots of on factors:

Right here is the end result:

View the CodeSandbox

PS: You’ll be able to obtain an identical background scene in Three.js by utilizing a PlaneGeometry and a ShaderMaterial.

2. Displaying the feel in “background-cover”

First, let’s make a promise to load a texture. You’ll be able to select any picture you need and place it within the public/ folder. We’ll create a new Picture(), set the picture URL because the supply, and cargo it. Then, utilizing new Texture(this.#renderer.gl) from OGL, we’ll flip it right into a texture and fix the picture to it utilizing a brand new property known as .picture. We’ll use async/await to make sure that our texture finishes loading earlier than creating our WebGL program.

// src/js/elements/scene.js
// load our texture
const loadTexture = (url) =>
  new Promise((resolve) => {
    const picture = new Picture()
    const texture = new Texture(gl)

    picture.onload = () => {
      texture.picture = picture
      resolve(texture)
    }

    picture.src = url
  })

const texture = await loadTexture('./img/image-7.jpg')

Now, let’s add the feel to the uniforms of our program.

// src/js/elements/scene.js
this.#program = new Program(gl, {
  vertex,
  fragment,
  uniforms: {
    uTime: { worth: 0 },
    uTexture: { worth: texture },
  },
})

And in our fragment shader.

// src/js/glsl/most important.frag
precision highp float;

uniform sampler2D uTexture;
various vec2 vUv;

void most important() {
  vec4 tex = texture2D(uTexture, vUv);
  gl_FragColor.rgb = tex.rgb;
  gl_FragColor.a = 1.0;
}

View the CodeSandbox

Cool! As you may see, we’ve got our texture, however it seems stretched.

Ideally, we would like the feel to be “cropped” in our scene in the very best approach, no matter whether or not it’s in panorama or portrait format. In CSS, there’s a property known as “background-size: cowl” that magically achieves this impact, however in WebGL, it’s extra advanced to implement.

Thankfully, I’ve a small piece of code that may make the UVs behave like a “background-size: cowl” impact. Let’s use it. However first, we have to go the decision of the picture and the canvas to our uniforms.

// src/js/elements/scene.js
...
uTextureResolution: { worth: new Vec2(texture.picture.width, texture.picture.top) },
uResolution: { worth: new Vec2(gl.canvas.offsetWidth, gl.canvas.offsetHeight) },
...

Let’s add our UV crop perform within the vertex shader (./src/js/glsl/most important.vert)

attribute vec2 uv;
attribute vec2 place;

uniform vec2 uResolution;
uniform vec2 uTextureResolution;

various vec2 vUv;

vec2 resizeUvCover(vec2 uv, vec2 dimension, vec2 decision) {
    vec2 ratio = vec2(
        min((decision.x / decision.y) / (dimension.x / dimension.y), 1.0),
        min((decision.y / decision.x) / (dimension.y / dimension.x), 1.0)
    );

    return vec2(
        uv.x * ratio.x + (1.0 - ratio.x) * 0.5,
        uv.y * ratio.y + (1.0 - ratio.y) * 0.5
    );
}

void most important() {
  vUv = resizeUvCover(uv, uTextureResolution, uResolution);
  gl_Position = vec4(place, 0, 1);
}

Don’t overlook to replace the uResolution on resize in “./src/js/elements/scene.js”.

// src/js/elements/scene.js
handleResize = () => {
  this.#renderer.setSize(window.innerWidth, window.innerHeight)

  if (this.#program) {
    this.#program.uniforms.uResolution.worth = new Vec2(window.innerWidth, window.innerHeight)
  }
}

View the CodeSandbox

Now we’ve got our picture good and cropped, let’s Distort it!

3. Distort the picture with the Bulge impact

To create the bulge impact, we need to displace the UVs primarily based on the middle of the picture. UVs vary from 0 to 1, the place U represents the pixels from left to proper of the picture and V represents the pixels from backside to high.

To realize the “bulge” impact, we are able to multiply the UVs by the gap from the middle. The built-in perform size() in GLSL offers us the size between pixels, as defined in the e book of shaders. Let’s create a perform known as bulgeEffect() to deal with this calculation and apply the modified UVs to our texture within the fragment shader.

// src/js/glsl/most important.frag
vec2 bulge(vec2 uv) {
  
  float dist = size(uv); // distance from UVs high proper nook
  uv *= dist; 

  return uv;
}

void most important() {
  vec2 bulgeUV = bulge(vUv);
  vec4 tex = texture2D(uTexture, bulgeUV);
  gl_FragColor.rgb = tex.rgb;
  gl_FragColor.a = 1.0;
}

The distortion seems to be from the underside proper nook fairly than the middle. To repair this, let’s subtract 0.5 from the UVs earlier than performing the distortion calculation, after which add them again afterwards to middle our distortion.

// src/js/glsl/most important.frag
vec2 bulge(vec2 uv, vec2 middle) {
  uv -= middle;
  
  float dist = size(uv); // distance from UVs
  uv *= dist; 
  
  uv += middle;

  return uv;
}

void most important() {
  vec2 middle = vec2(0.5, 0.5);
  vec2 bulgeUV = bulge(vUv, middle);
  vec4 tex = texture2D(uTexture, bulgeUV);
  gl_FragColor.rgb = tex.rgb;
  gl_FragColor.a = 1.0;
}

View the CodeSandbox

That’s working, however the impact will not be very robust.

Utilizing energy capabilities may be very usefull to extend drastically an impact, and glsl have already got pow() for us. Let’s strive with 2.

  // src/js/glsl/most important.frag
  ...
  float dist = size(uv); // distance from UVs
  float distPow = pow(dist, 2.); // exponential
  uv *= distPow; 

View the CodeSandbox

Now, if you wish to revert the impact, you may divide the bulge impact by a radius to have extra management. Be at liberty to tweak these values till you obtain the specified impact.

// src/js/glsl/most important.frag
...
const float radius = 0.6;
const float energy = 1.1;

vec2 bulge(vec2 uv, vec2 middle) {
  uv -= middle;
  
  float dist = size(uv) / radius; // distance from UVs divided by radius
  float distPow = pow(dist, 2.); // exponential
  float strengthAmount = energy / (1.0 + distPow); // Invert bulge and add a minimal of 1)
  uv *= strengthAmount; 
  
  uv += middle;

  return uv;
}

View the CodeSandbox

4. Including interactivity

Lastly, let’s make this bulge impact monitor the mouse with a mouse transfer occasion. 🙂

Let’s use a uMouse uniform for that.

// src/js/elements/scene.js
...
// Within the uniform's Program
uMouse: { worth: new Vec2(0.5, 0.5) },
...

...
  // Add occasion listener
  window.addEventListener('mousemove', this.handleMouseMove, false)
...
...
// On mouse transfer we would like the this.#mouse worth to be
// between 0 and 1 X and Y axis
handleMouseMove = (e) => {
  const x = e.clientX / window.innerWidth
  const y = 1 - e.clientY / window.innerHeight

  this.#mouse.x = x
  this.#mouse.y = y

  // replace the uMouse worth right here or within the handleRAF
  this.#program.uniforms.uMouse.worth = this.#mouse
}
...
...

Then within the fragment shader, we simply have to exchange middle with our uMouse.

// src/js/glsl/most important.frag
uniform vec2 uMouse;
...
   vec2 bulgeUV = bulge(vUv, uMouse);
...

And that’s it 🙂 Hover along with your mouse to see it in motion:

View the CodeSandbox

In fact, from that, you may add any impact you need. For instance, you may play a GSAP animation to reset the impact when the mouse leaves the canvas, like within the demo, or add any impact you may think about 🙂

We’ve seen that it’s fairly simple to play with distortion in WebGL with only a easy fragment shader, and there are a whole lot of different results we are able to obtain. If you wish to learn to create one other sort of distortion utilizing a noise texture, I invite you to observe considered one of my different tutorials on the subject. Cheers!

References

RELATED ARTICLES

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Most Popular

Recent Comments