30 C
New York
Thursday, July 18, 2024

Tips on how to Create Distortion and Grain Results on Scroll with Shaders in Three.js

Tips on how to Create Distortion and Grain Results on Scroll with Shaders in Three.js


With shaders, we are able to, with just some traces of code, a little bit of math, and loads of experimenting and trial and error success, create wonderful stuff. On this tutorial, I’m going to indicate you my setup and the way I acquired an attention-grabbing distortion and grain impact engaged on scroll utilizing shaders in Three.js.

For fairly a while, I had a giant block in creating these results as a result of I merely didn’t get easy methods to sync the place of native HTML photos with WebGL. Seems (as all the time) that when I figured it out, it’s fairly easy.

Setup

By now, I’ve my very own little boilerplate with Nuxt.js the place I gather such base functionalities, however I’ll attempt to clarify briefly what’s happening for the demo with out Nuxt.

Media and meshes

The premise of how I handle the media and meshes is the next operate:

// this will get all picture html tags and creates a mesh for every
const setMediaStore = (scrollY) => {
  const media = [...document.querySelectorAll('[data-webgl-media]')]

  mediaStore = media.map((media, i) => {
    observer.observe(media)

    media.dataset.index = String(i)
    media.addEventListener('mouseenter', () => handleMouseEnter(i))
    media.addEventListener('mousemove', e => handleMousePos(e, i))
    media.addEventListener('mouseleave', () => handleMouseLeave(i))

    const bounds = media.getBoundingClientRect()
    const imageMaterial = materials.clone()

    const imageMesh = new THREE.Mesh(geometry, imageMaterial)

    let texture = null

    texture = new THREE.Texture(media)
    texture.needsUpdate = true

    imageMaterial.uniforms.uTexture.worth = texture
    imageMaterial.uniforms.uTextureSize.worth.x = media.naturalWidth
    imageMaterial.uniforms.uTextureSize.worth.y = media.naturalHeight
    imageMaterial.uniforms.uQuadSize.worth.x = bounds.width
    imageMaterial.uniforms.uQuadSize.worth.y = bounds.top
    imageMaterial.uniforms.uBorderRadius.worth = getComputedStyle(media).borderRadius.exchange('px', '')

    imageMesh.scale.set(bounds.width, bounds.top, 1)

    if (!(bounds.prime >= 0 && bounds.prime <= window.innerHeight)) {
      imageMesh.place.y = 2 * window.innerHeight
    }

    scene.add(imageMesh)

    return {
      media,
      materials: imageMaterial,
      mesh: imageMesh,
      width: bounds.width,
      top: bounds.top,
      prime: bounds.prime + scrollY,
      left: bounds.left,
      isInView: bounds.prime >= -500 && bounds.prime <= window.innerHeight + 500,
      mouseEnter: 0,
      mouseOverPos: {
        present: {
          x: 0.5,
          y: 0.5
        },
        goal: {
          x: 0.5,
          y: 0.5
        }
      }
    }
  })
}

This script collects all photos (that we would like in WebGL) in an array, provides occasion listeners to them (so we are able to manipulate uniforms later), creates the meshes with Three.js, and provides vital uniforms like dimensions. There are extra particulars to it, like checking if the pictures are in view or if they’ve a border radius, however you may verify these within the code your self. The code has grown over time and could be very useful to attenuate the hassle wanted. Basically, we simply wish to say, “use this picture and let me apply a shader.”

Digicam and geometry

The digicam and geometry setup can be somewhat simple:

// digicam
const CAMERA_POS = 500
const calcFov = (CAMERA_POS) => 2 * Math.atan((window.innerHeight / 2) / CAMERA_POS) * 180 / Math.PI

const digicam = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 10, 1000)
digicam.place.z = CAMERA_POS
digicam.fov = calcFov(CAMERA_POS)
digicam.updateProjectionMatrix()

// geometry and materials
geometry = new THREE.PlaneGeometry(1, 1, 100, 100)
materials = new THREE.ShaderMaterial({
  uniforms: {
    uResolution: { worth: new THREE.Vector2(window.innerWidth, window.innerHeight) },
    uTime: { worth: 0 },
    uCursor: { worth: new THREE.Vector2(0.5, 0.5) },
    uScrollVelocity: { worth: 0 },
    uTexture: { worth: null },
    uTextureSize: { worth: new THREE.Vector2(100, 100) },
    uQuadSize: { worth: new THREE.Vector2(100, 100) },
    uBorderRadius: { worth: 0 },
    uMouseEnter: { worth: 0 },
    uMouseOverPos: { worth: new THREE.Vector2(0.5, 0.5) }
  },
  vertexShader: effectVertex,
  fragmentShader: effectFragment,
  glslVersion: THREE.GLSL3
})

An necessary side is the small math trick to calculate the sector of view (FOV), which permits us to set the positions of the meshes primarily based on the HTML dimensions and bounds. Whereas I can’t go into each element right here, I extremely encourage you to check the file and observe alongside step-by-step. It’s fairly manageable and will provide you with a deeper understanding of the method.

The render loop

What’s left is the render loop:

// render loop
const render = (time = 0) => {
  time /= 1000

  mediaStore.forEach((object) => {
    if (object.isInView) {
      object.mouseOverPos.present.x = lerp(object.mouseOverPos.present.x, object.mouseOverPos.goal.x, 0.05)
      object.mouseOverPos.present.y = lerp(object.mouseOverPos.present.y, object.mouseOverPos.goal.y, 0.05)

      object.materials.uniforms.uResolution.worth.x = window.innerWidth
      object.materials.uniforms.uResolution.worth.y = window.innerHeight
      object.materials.uniforms.uTime.worth = time
      object.materials.uniforms.uCursor.worth.x = cursorPos.present.x
      object.materials.uniforms.uCursor.worth.y = cursorPos.present.y
      object.materials.uniforms.uScrollVelocity.worth = scroll.scrollVelocity
      object.materials.uniforms.uMouseOverPos.worth.x = object.mouseOverPos.present.x
      object.materials.uniforms.uMouseOverPos.worth.y = object.mouseOverPos.present.y
      object.materials.uniforms.uMouseEnter.worth = object.mouseEnter
    } else {
      object.mesh.place.y = 2 * window.innerHeight
    }
  })

  setPositions()

  renderer.render(scene, digicam)

  requestAnimationFrame(render)
}

On this half, I replace the uniforms and set the positions of the meshes to sync them with the native HTML photos. You’ll discover that I solely replace uniforms for meshes which are in view and place these out of view outdoors the viewport. These enhancements developed over time, so that you don’t must undergo the identical trial and error course of 😀

You could find your entire file to check, copy, and modify right here.

The shader magic

Let’s get to the enjoyable half! Let’s discover the 2 base shader recordsdata.

Base vertex shader

uniform vec2 uResolution; // in pixel
uniform float uTime; // in s
uniform vec2 uCursor; // 0 (left) 0 (prime) / 1 (proper) 1 (backside)
uniform float uScrollVelocity; // - (scroll up) / + (scroll down)
uniform sampler2D uTexture; // texture
uniform vec2 uTextureSize; // dimension of texture
uniform vec2 uQuadSize; // dimension of texture component
uniform float uBorderRadius; // pixel worth
uniform float uMouseEnter; // 0 - 1 (enter) / 1 - 0 (depart)
uniform vec2 uMouseOverPos; // 0 (left) 0 (prime) / 1 (proper) 1 (backside)

#embody './sources/utils.glsl';

out vec2 vUv;  // 0 (left) 0 (backside) - 1 (prime) 1 (proper)
out vec2 vUvCover;


void principal() {
  vUv = uv;
  vUvCover = getCoverUvVert(uv, uTextureSize, uQuadSize);

  gl_Position = projectionMatrix * modelViewMatrix * vec4(place, 1.0);
}

Right here you may see the uniforms I all the time embody to keep away from worrying in regards to the fundamental setup and interactions, permitting me to dive straight into experimenting. Not all of them are all the time wanted or used, however I’ll briefly clarify every one:

  • uResolution: The decision of the window dimension.
  • uTime: Represents the passage of time.
  • uCursor: The place of the cursor relative to the window.
  • uScrollVelocity: A worth from the Lenis library, usually used for easy scrolling.
  • uTexture: The picture.
  • uTextureSize: The scale of the picture, wanted to calculate an object-fit: cowl conduct, for instance.
  • uQuadSize: The scale of the displayed component, the size you see on the finish.
  • uBorderRadius: Permits making use of a border radius to pictures in HTML, which can be utilized within the shader (extra code within the fragment shader is required, hyperlink right here).
  • uMouseEnter: A worth that easily interpolates from 0 to 1 (on mouse enter of the picture) or 1 to 0 (on mouse depart of the picture). You could find how that is executed within the JS file and the way it’s handed to the shader.
  • uMouseOverPos: The place of the cursor relative to the picture.
  • getCoverUvVert: A operate to use an ‘object-fit: cowl’ conduct.

Base fragment shader

precision highp float;

uniform vec2 uResolution; // in pixel
uniform float uTime; // in s
uniform vec2 uCursor; // 0 (left) 0 (prime) / 1 (proper) 1 (backside)
uniform float uScrollVelocity; // - (scroll up) / + (scroll down)
uniform sampler2D uTexture; // texture
uniform vec2 uTextureSize; // dimension of texture
uniform vec2 uQuadSize; // dimension of texture component
uniform float uBorderRadius; // pixel worth
uniform float uMouseEnter; // 0 - 1 (enter) / 1 - 0 (depart)
uniform vec2 uMouseOverPos; // 0 (left) 0 (prime) / 1 (proper) 1 (backside)

in vec2 vUv; // 0 (left) 0 (backside) - 1 (proper) 1 (prime)
in vec2 vUvCover;

out vec4 outColor;


void principal() {
  // texture
  vec3 texture = vec3(texture(uTexture, vUvCover));

  // output
  outColor = vec4(texture, 1.0);
}

There’s nothing actually particular right here; it’s the identical uniforms. You possibly can see that I like to make use of GLSL3 as a result of the varyings are written with the in and out key phrases, and I’ve outlined the outColor variable.
With the vUvCover coordinates from the vertex shader, we are able to present the feel with the duvet conduct in simply two traces.

At this level, now we have HTML photos displayed with WebGL, and their positions are fully synced. Now we are able to apply no matter results we like.

It took a while, however I needed to clarify the behind-the-scenes a bit. Now, this stable base makes all the pieces any more far more enjoyable and pleasurable.

The impact

Apparently sufficient, this isn’t the longest a part of the tutorial, because the impact itself isn’t that complicated. However let’s stroll by way of it.

First, let’s give attention to the curve when scrolling. Since we have to regulate the vertices, we’ll work within the vertex shader.

vec3 deformationCurve(vec3 place, vec2 uv) {
  place.y = place.y - (sin(uv.x * PI) * uScrollVelocity * -0.01);
  
  return place;
}

This little operate does the magic. We modify the vertices with a sin() operate primarily based on the uv.x values. The multiplication with PI (must be outlined on the prime of the file with float PI = 3.141592653589793;) makes the curve precisely one excellent bow. Be happy to take away it or change it to 2*PI to see what it does.

To use the impact on scrolling, or primarily based on the scroll depth, we simply have to multiply it by the uScrollVelocity uniform and regulate the issue (and route with constructive/detrimental indicators). See how all that work earlier makes this tremendous simple now? Yay!

As a result of the distortion acquired actually huge when scrolling actually quick, I launched a most, which could be very simple to implement with the min() operate. The one factor to think about right here is that it will all the time output a constructive worth in the best way I used it, so I needed to multiply by the signal of the uScrollVelocity once more to get again the bow within the route of scrolling. GLSL has the signal() operate for that, so it’s easy.

place.y = place.y - (sin(uv.x * PI) * min(abs(uScrollVelocity), 5.0) * signal(uScrollVelocity) * -0.01);

Now we simply must subtract that calculated worth from the unique y place, and now we have the primary a part of our impact.

Right here’s the ultimate full vertex shader:

float PI = 3.141592653589793;

uniform vec2 uResolution; // in pixel
uniform float uTime; // in s
uniform vec2 uCursor; // 0 (left) 0 (prime) / 1 (proper) 1 (backside)
uniform float uScrollVelocity; // - (scroll up) / + (scroll down)
uniform sampler2D uTexture; // texture
uniform vec2 uTextureSize; // dimension of texture
uniform vec2 uQuadSize; // dimension of texture component
uniform float uBorderRadius; // pixel worth
uniform float uMouseEnter; // 0 - 1 (enter) / 1 - 0 (depart)
uniform vec2 uMouseOverPos; // 0 (left) 0 (prime) / 1 (proper) 1 (backside)

#embody './sources/utils.glsl';

out vec2 vUv;  // 0 (left) 0 (backside) - 1 (prime) 1 (proper)
out vec2 vUvCover;


vec3 deformationCurve(vec3 place, vec2 uv) {
  place.y = place.y - (sin(uv.x * PI) * min(abs(uScrollVelocity), 5.0) * signal(uScrollVelocity) * -0.01);

  return place;
}

void principal() {
  vUv = uv;
  vUvCover = getCoverUvVert(uv, uTextureSize, uQuadSize);

  vec3 deformedPosition = deformationCurve(place, vUvCover);

  gl_Position = projectionMatrix * modelViewMatrix * vec4(deformedPosition, 1.0);
}

Let’s transfer over to the fragment shader.

Right here we are going to work on the grain/noise impact. For the noise operate, you may seek for one on-line. I used this one.

We will create noise by merely calling the operate:

float noise = snoise(gl_FragCoord.xy);

And show it with:

vec3 texture = vec3(texture(uTexture, vUvCover));
outColor = vec4(texture * noise, 1.0);

To get the ultimate impact, I utilized the noise primarily based on a circle that follows the cursor place.

  // side ratio wanted to create an actual circle when quadSize isn't 1:1 ratio
  float aspectRatio = uQuadSize.y / uQuadSize.x;

  // create a circle following the mouse with dimension 15
  float circle = 1.0 - distance(
    vec2(uMouseOverPos.x, (1.0 - uMouseOverPos.y) * aspectRatio),
    vec2(vUv.x, vUv.y * aspectRatio)
  ) * 15.0;

We’d like the side ratio of the displayed picture as a result of we all the time need an ideal circle (not an ellipse) if the picture doesn’t have a 1:1 side ratio. We will create a circle fairly merely with a distance operate. If we move within the uMouseOverPos to calculate the gap from, it’ll mechanically observe the cursor place. You possibly can all the time “debug” the steps by outputting, for instance, the circle worth as outColor to see what it does and the way it behaves.

Now we solely want to mix the noise with the circle and apply it to the feel:

  vec2 texCoords = vUvCover;
  
  // modify texture coordinates
  texCoords.x += combine(0.0, circle * noise * 0.01, uMouseEnter + uScrollVelocity * 0.1);
  texCoords.y += combine(0.0, circle * noise * 0.01, uMouseEnter + uScrollVelocity * 0.1);

  // texture
  vec3 texture = vec3(texture(uTexture, texCoords));

  // output
  outColor = vec4(texture, 1.0);

I do know these traces look a bit complicated at first, however let’s break it down; it’s not too exhausting.

On the finish, we wish to modify the UVs coming from the vertex shader as vUvCover. The impact itself is just circle * noise * 0.01. You possibly can strive making use of simply that, and the impact will all the time be utilized and visual. To make it seen solely on scroll and/or mouse over, we are able to leverage the combo operate (I really like that operate :D).

Mainly, we are saying both add 0, or circle * noise * 0.01 to the texCoords primarily based on the third parameter. Now, we solely want this third parameter to mirror the interplay we would like. Because of our excellent preparation of the uniforms, we are able to simply use uMouseEnter (which interpolates to 1 on enter) and uScrollVelocity as this parameter.

Now, if we both enter the picture with the cursor or scroll the web page, the combo operate will end in circle * noise * 0.01, and we’ll get the impact we would like. If we don’t work together, it’ll end in 0, thus not including something to the texCoords, and we’ll see solely the picture.

Right here’s the ultimate full fragment shader:

precision highp float;

uniform vec2 uResolution; // in pixel
uniform float uTime; // in s
uniform vec2 uCursor; // 0 (left) 0 (prime) / 1 (proper) 1 (backside)
uniform float uScrollVelocity; // - (scroll up) / + (scroll down)
uniform sampler2D uTexture; // texture
uniform vec2 uTextureSize; // dimension of texture
uniform vec2 uQuadSize; // dimension of texture component
uniform float uBorderRadius; // pixel worth
uniform float uMouseEnter; // 0 - 1 (enter) / 1 - 0 (depart)
uniform vec2 uMouseOverPos; // 0 (left) 0 (prime) / 1 (proper) 1 (backside)

in vec2 vUv; // 0 (left) 0 (backside) - 1 (proper) 1 (prime)
in vec2 vUvCover;

#embody './sources/noise.glsl';

out vec4 outColor;


void principal() {
  vec2 texCoords = vUvCover;

  // side ratio wanted to create an actual circle when quadSize isn't 1:1 ratio
  float aspectRatio = uQuadSize.y / uQuadSize.x;

  // create a circle following the mouse with dimension 15
  float circle = 1.0 - distance(
    vec2(uMouseOverPos.x, (1.0 - uMouseOverPos.y) * aspectRatio),
    vec2(vUv.x, vUv.y * aspectRatio)
  ) * 15.0;

  // create noise
  float noise = snoise(gl_FragCoord.xy);

  // modify texture coordinates
  texCoords.x += combine(0.0, circle * noise * 0.01, uMouseEnter + uScrollVelocity * 0.1);
  texCoords.y += combine(0.0, circle * noise * 0.01, uMouseEnter + uScrollVelocity * 0.1);

  // texture
  vec3 texture = vec3(texture(uTexture, texCoords));

  // output
  outColor = vec4(texture, 1.0);
}

Disclaimer

I feel this can be a very strong and stable base for creating such results, however I additionally know that there are a lot of extra gifted builders on the market. When you have any concepts to optimize issues or know higher methods, I’m all the time open to listening to about them. Likewise, if one thing isn’t clear or if I may also help with something, I’m right here for that as properly.

Actual-World-Shader Challenge

Thanks for studying this tutorial! I’m additionally presently engaged on a enjoyable little challenge I name Actual-World-Shader, the place I purpose to gather examples of good shader results which are helpful in actual shopper initiatives, multi functional place. I’d love to listen to your suggestions and solutions, and even contributions. Be happy to test it out right here: https://real-world-shader.jankohlbach.com/



Supply hyperlink

Related Articles

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Latest Articles