Skip to main content

Million Miles Technologies

How to Create Distortion and Grain Effects on Scroll with Shaders in Three.js

Creating visually engaging web experiences is a key component of modern web development. One way to achieve this is by using Three.js to create interactive visual effects like distortion and grain that respond to user scrolling. This tutorial will guide you through setting up a Three.js scene, implementing shaders, and using the scroll event to create dynamic distortion and grain effects.

What is Three.js?

Three.js is a powerful JavaScript library that simplifies the creation of 3D graphics for the web. It provides an abstraction layer over WebGL, making it easier to work with 3D content. With Three.js, you can create complex scenes, animations, and visual effects using a straightforward API.

Setting Up Your Environment

Before diving into the code, ensure you have the necessary tools and environment set up:

  1. Install Node.js and npm: If you haven’t already, download and install Node.js from the official website. npm (Node Package Manager) comes bundled with Node.js.
  2. Create a Project Directory: Create a new directory for your project and navigate into it using your terminal or command prompt.
  3. Initialize a New npm Project: Run the following command to initialize a new npm project:
    npm init -y
  4. Install Three.js: Install Three.js via npm:
    npm install three
  5. Set Up a Development Server: You can use a simple HTTP server to serve your project files. Install the http-server package:
    npm install -g http-server
    Distortion and Grain Effects

Setting Up the Basic Three.js Scene

First, create an index.html file to hold the basic HTML structure:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Three.js Distortion and Grain Effects</title>
<style>
body { margin: 0; }
canvas { display: block; }
</style>
</head>
<body>
<script src="main.js"></script>
</body>
</html>

Next, create a main.js file where we will write our Three.js code:

import * as THREE from ‘three’;

// Scene setup
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

// Basic geometry and material
const geometry = new THREE.PlaneGeometry(2, 2);
const material = new THREE.ShaderMaterial({
uniforms: {
time: { value: 1.0 },
scrollY: { value: 0.0 }
},
vertexShader: `…`, // Placeholder for vertex shader code
fragmentShader: `…` // Placeholder for fragment shader code
});

const plane = new THREE.Mesh(geometry, material);
scene.add(plane);
camera.position.z = 1;

const animate = function () {
requestAnimationFrame(animate);

// Update shader uniforms
material.uniforms.time.value += 0.05;

renderer.render(scene, camera);
};

animate();

Writing the Shaders

Shaders are small programs that run on the GPU. We’ll write custom vertex and fragment shaders to create the distortion and grain effects.

Distortion and Grain Effects

Vertex Shader

The vertex shader manipulates the vertices of the geometry. For this effect, we’ll keep it simple:

const vertexShader = `
varying vec2 vUv;

void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`;
material.vertexShader = vertexShader;


Fragment Shader

The fragment shader will handle the pixel-level manipulation to create distortion and grain effects. We’ll use Perlin noise for the distortion and a simple random function for the grain effect.

const fragmentShader = `
uniform float time;
uniform float scrollY;
varying vec2 vUv;

// Simplex noise function
vec3 permute(vec3 x) {
return mod((34.0 * x + 1.0) * x, 289.0);
}

vec3 taylorInvSqrt(vec3 r) {
return 1.79284291400159 – 0.85373472095314 * r;
}

vec2 fade(vec2 t) {
return t * t * t * (t * (t * 6.0 – 15.0) + 10.0);
}

float noise(vec2 P) {
vec3 Pi = floor(vec3(P, 0.0)) + vec3(0.0, 0.0, 0.0);
vec3 Pf = fract(vec3(P, 0.0)) – vec3(0.0, 0.0, 0.0);
vec3 f = fade(vec3(Pf.xy, 0.0));
vec3 i = permute(permute(Pi.xy + vec3(0.0, 0.0)) + vec3(0.0, 0.0)) + vec3(0.0, 0.0);
vec3 a = permute(permute(i.xy) + vec3(0.0, 0.0));
vec3 b = permute(permute(i.xy + vec3(1.0, 0.0)) + vec3(0.0, 0.0));
vec3 c = permute(permute(i.xy + vec3(0.0, 1.0)) + vec3(0.0, 0.0));
vec3 d = permute(permute(i.xy + vec3(1.0, 1.0)) + vec3(0.0, 0.0));
return mix(mix(mix( dot(a.xy, Pf.xy), dot(b.xy, Pf.xy – vec3(1.0, 0.0)), f.x),
mix( dot(c.xy, Pf.xy – vec3(0.0, 1.0)), dot(d.xy, Pf.xy – vec3(1.0, 1.0)), f.x), f.y), 0.0, 0.0);
}

float random(vec2 p) {
return fract(sin(dot(p.xy, vec2(12.9898,78.233))) * 43758.5453);
}

void main() {
vec2 uv = vUv;
float distortion = noise(uv * 10.0 + time * 0.5) * 0.1;
uv.y += distortion * scrollY;
vec3 color = vec3(uv, 0.5 + 0.5 * sin(time + uv.x * 3.0));
color += random(uv + time) * 0.1;
gl_FragColor = vec4(color, 1.0);
}
`;
material.fragmentShader = fragmentShader;


Adding Scroll Event Handling

To make the effect interactive, we’ll add an event listener for the scroll event and update the shader uniform based on the scroll position.

window.addEventListener('scroll', () => {
material.uniforms.scrollY.value = window.scrollY * 0.01;
});

Full Code Example

Here is the complete code combining all parts:

<!DOCTYPE html>
<html lang=”en”>
<head>
<meta charset=”UTF-8″>
<meta name=”viewport” content=”width=device-width, initial-scale=1.0″>
<title>Three.js Distortion and Grain Effects</title>
<style>
body { margin: 0; }
canvas { display: block; }
</style>
</head>
<body>
<script type=”module”>
import * as THREE from ‘three’;

// Scene setup
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

// Vertex shader
const vertexShader = `
varying vec2 vUv;

void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`;

// Fragment shader
const fragmentShader = `
uniform float time;
uniform float scrollY;
varying vec2 vUv;

vec3 permute(vec3 x) {
return mod((34.0 * x + 1.0) * x, 289.0);
}

vec3 taylorInvSqrt(vec3 r) {
return 1.79284291400159 – 0.85373472095314 * r;
}

vec2 fade(vec2 t) {
return t * t * t * (t * (t * 6.0 – 15.0) + 10.0);
}

float noise(vec2 P) {
vec3 Pi = floor(vec3(P, 0.0)) + vec3(0.0, 0.0, 0.0);
vec3 Pf = fract(vec3(P, 0.0

 
 
 

Table of Contents

Facebook
Twitter
LinkedIn