【Three.js × GLSL】Glitch Effects : Starting from the Basics 🔰
This article is the December 19th post of the Money Forward Engineers Advent Calendar 2025 🎅
This article is also available in Japanese: Read the Japanese version of this article
Introduction
In this article, I explain how to implement glitch effects using shaders, while covering the basics of Three.js and GLSL.
The images used in this article are borrowed from unsplash.
Although this topic is not directly related to my current work, I often worked on animation in my previous job and have long been interested in creative coding.
One day, I heard from an engineer in the company that they shared a similar interest, which inspired me to write this article 🎅
Intended Audience
- Those who understand the basics of Three.js but are beginners to shaders
- Those who have just started learning shaders and want to deepen their understanding by creating simple effects
- Those who are interested in glitch effects and want to try them while understanding how they work
What This Article Does Not Cover
This article does not go into detail about introductory shader concepts.
If you are new to shaders, I think it will be easier to follow if you have a basic understanding of the following topics:
- What shaders are
- The difference in roles between vertex shaders and fragment shaders
- The basic structure of the rendering pipeline
- Coordinate transformations (model space, world space, view space, clip space, etc.)
- What UV coordinates are
- Basic GLSL syntax
I studied shaders through a lot of trial and error, but the resource that I personally found the easiest to understand was three.js journey.
It is a paid course, but it provides well-organized introductory content and practical knowledge that is very helpful for learning shaders.
Notes
There are many different ways to implement glitch effects.
In this article, I introduce just one example approach.
I’m sure there are better implementations out there, so I hope you’ll treat this article as a reference to help you take the first step.
Also, before getting into glitch effects, this article spends a bit more time on the warm-up section.
I believe that understanding this part will make it much easier to work on implementing glitch effects later on.
What Is a Glitch
Before starting the implementation, let’s first briefly go over what a “glitch” is.

A glitch is a visual effect created by intentionally introducing distortion or noise.
It can be used to evoke atmospheres such as retro, cool, cyberpunk, or horror.
Before diving into implementing glitch effects, one thing I personally find important is understanding that there are multiple types of glitches.
Even effects that look complex at first glance are often created by combining several simple effects.
As a starting point, I recommend watching this YouTube video to get a rough overview of different types of glitches.
Although it’s a video for After Effects, it’s very helpful for understanding the underlying ideas behind these visual expressions.
The video introduces nine different glitch effects, but after writing this article, I realized that explaining all of them would result in an overwhelming amount of content.
So in this article, I focus on the following three effects:
- Base Scan Line
- Chromatic Aberration
- Deform Line
That said, once you’re able to implement these, you should be able to create other glitch effects by applying the same ideas with a bit of extra knowledge—so I encourage you to give it a try 💪
I’ve also included an advanced CodePen example at the end of the article.
Preparation
1. Set Up the Development Environment
I’ve prepared a simple development environment.
A single plane with a texture applied is placed to fill the entire screen.
I’ll explain only the key points of the setup 🙏
Camera
this.camera = new THREE.OrthographicCamera(
...省略
);
Since we are not using any 3D-style effects in this example, we use an OrthographicCamera.
It is also possible to implement this with a PerspectiveCamera. In that case, you need to align window coordinates with WebGL coordinates. For a concrete approach, this Qiita article is a helpful reference.
By using this method, you can align DOM elements and 3D objects at the pixel level, which is a useful concept to know when creating expressions that combine HTML and three.js.
TIME_SCALE
const TIME_SCALE = 0.1;
...
this.uniforms.uTime.value += delta * TIME_SCALE;
uTime represents the elapsed time passed to the shader as a uniform.
If the raw value is used as-is, the animation becomes too fast, so a factor called TIME_SCALE is used to control the speed at which time progresses. The value 0.1 is just an arbitrary choice.
By changing the value of TIME_SCALE, you can easily adjust the overall speed of the animation.
Sampling Texture Colors in a Shader
void main() {
vec3 color = texture2D(uTexture, vUv).rgb;
gl_FragColor = vec4(color, 1.0);
}
texture2D is a function used to sample color from an image (texture).
By retrieving the color at the position specified by vUv and assigning the result to gl_FragColor, the color is rendered on the screen for each pixel.
2. Make the Texture Behave Like background-image: cover;
Right now, the texture is stretched to match the window size.
To achieve behavior similar to CSS background-image: cover;, we’ll fix this issue.
In short, we compare the aspect ratio of the viewport and the image, determine which side overflows, and then scale the UVs around the center (0.5, 0.5).
This makes the shorter side fit the screen, while the longer side is cropped (trimmed) from the center—just like cover.
Since this idea can be hard to visualize from text alone, the diagram in this YouTube Shorts video makes it easier to understand.
Now, using the exact same idea from the video, let’s add updateCoverScale().
async loadTexture() {
...
this.createMesh();
+ this.updateCoverScale();
this.animate();
}
...
+updateCoverScale() {
+ if (!this.texture.image) return;
+ const w = window.innerWidth;
+ const h = window.innerHeight;
+ const viewportAspect = w / h;
+ const imageAspect = this.texture.image.width / this.texture.image.height;
+ let scaleX = 1.0;
+ let scaleY = 1.0;
+ if (imageAspect > viewportAspect) {
+ // Image is wider than canvas → fit by height, crop horizontally
+ scaleX = viewportAspect / imageAspect;
+ scaleY = 1.0;
+ } else {
+ // Image is taller than canvas → fit by width, crop vertically
+ scaleX = 1.0;
+ scaleY = imageAspect / viewportAspect;
+ }
+ // Pass resolution and aspect correction to shader
+ this.uniforms.uResolution.value.set(w, h, scaleX, scaleY);
+}
...
setupEvents() {
window.addEventListener("resize", () => {
...
if (this.mesh) {
...
+ this.updateCoverScale();
}
});
}
Now that we’ve created uResolution, we’ll pass it to the fragment shader.
createMesh() {
...
this.uniforms = {
uTime: { value: 0 },
+ uResolution: { value: new THREE.Vector4() },
uTexture: { value: this.texture },
};
...
}
uniform sampler2D uTexture;
+ uniform vec4 uResolution;
varying vec2 vUv;
void main() {
+ vec2 newUv = (vUv - vec2(0.5)) * uResolution.zw + vec2(0.5);
- vec3 color = texture2D(uTexture, vUv).rgb;
+ vec3 color = texture2D(uTexture, newUv).rgb;
gl_FragColor = vec4(color, 1.0);
}
As long as the underlying idea is the same, I think any approach is fine.
Another method you’ll often see is explained in this Qiita article, so feel free to use it as a reference.
Now the image will no longer stretch to match the window size!
I’ll paste the code up to this point below.
HTML
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Glitch Effect</title>
<link rel="stylesheet" href="./style.css" />
</head>
<body>
<canvas></canvas>
<script type="module" src="./script.js"></script>
</body>
</html>
CSS
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
margin: 0;
padding: 0;
overflow: hidden;
width: 100vw;
height: 100%;
}
canvas {
display: block;
width: 100vw;
height: 100%;
}
JS
import * as THREE from "three";
import vertexShader from "./shaders/vertex.glsl";
import fragmentShader from "./shaders/fragment.glsl";
const TIME_SCALE = 0.1; // speed of time
class App {
/**
* Initialize the application
* Get canvas element and start initialization process
*/
constructor() {
this.canvas = document.querySelector("canvas");
this.init();
}
init() {
this.setupScene();
this.setupCamera();
this.setupRenderer();
this.loadTexture();
this.setupEvents();
}
/**
* Set up scene and clock
*/
setupScene() {
this.scene = new THREE.Scene();
this.clock = new THREE.Clock();
}
/**
* Set up orthographic camera
*/
setupCamera() {
const width = window.innerWidth;
const height = window.innerHeight;
this.camera = new THREE.OrthographicCamera(
-width / 2,
width / 2,
height / 2,
-height / 2,
0.1,
1000
);
this.camera.position.z = 1;
}
/**
* Set up WebGL renderer
*/
setupRenderer() {
this.renderer = new THREE.WebGLRenderer({
canvas: this.canvas,
antialias: true,
});
this.renderer.setSize(window.innerWidth, window.innerHeight);
this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
}
/**
* Load texture asynchronously
* After loading, create mesh and start animation
*/
async loadTexture() {
const loader = new THREE.TextureLoader();
this.texture = await new Promise((resolve) => {
const tex = loader.load("/1.webp", () => {
resolve(tex);
});
});
this.createMesh();
this.updateCoverScale();
this.animate();
}
/**
* Set up geometry, uniforms, and custom shader material
*/
createMesh() {
const geometry = new THREE.PlaneGeometry(
window.innerWidth,
window.innerHeight
);
this.uniforms = {
uTime: { value: 0 },
uResolution: { value: new THREE.Vector4() },
uTexture: { value: this.texture },
};
const material = new THREE.RawShaderMaterial({
vertexShader,
fragmentShader,
uniforms: this.uniforms,
});
this.mesh = new THREE.Mesh(geometry, material);
this.scene.add(this.mesh);
}
/**
* Update scale to cover the viewport by texture
*/
updateCoverScale() {
if (!this.texture.image) return;
const w = window.innerWidth;
const h = window.innerHeight;
const viewportAspect = w / h;
const imageAspect = this.texture.image.width / this.texture.image.height;
let scaleX = 1.0;
let scaleY = 1.0;
if (imageAspect > viewportAspect) {
// Image is wider than canvas → fit by height, crop horizontally
scaleX = viewportAspect / imageAspect;
scaleY = 1.0;
} else {
// Image is taller than canvas → fit by width, crop vertically
scaleX = 1.0;
scaleY = imageAspect / viewportAspect;
}
// Pass resolution and aspect correction to shader
this.uniforms.uResolution.value.set(w, h, scaleX, scaleY);
}
/**
* Animation loop
* Update time uniform and render the scene
*/
animate() {
requestAnimationFrame(() => this.animate());
const delta = this.clock.getDelta();
this.uniforms.uTime.value += delta * TIME_SCALE;
this.renderer.render(this.scene, this.camera);
}
/**
* Update camera frustum based on window size
* Adjust orthographic camera boundaries and update projection matrix
*/
updateCameraPosition(width, height) {
this.camera.left = -width / 2;
this.camera.right = width / 2;
this.camera.top = height / 2;
this.camera.bottom = -height / 2;
this.camera.updateProjectionMatrix();
}
/**
* Set up event listeners
* Handle window resize: update camera, renderer, and mesh geometry
*/
setupEvents() {
window.addEventListener("resize", () => {
const width = window.innerWidth;
const height = window.innerHeight;
this.updateCameraPosition(width, height);
this.renderer.setSize(width, height);
this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
if (this.mesh) {
this.mesh.geometry.dispose();
this.mesh.geometry = new THREE.PlaneGeometry(width, height);
this.updateCoverScale();
}
});
}
}
new App();
fragment shader
precision mediump float;
uniform float uTime;
uniform sampler2D uTexture;
uniform vec4 uResolution;
varying vec2 vUv;
void main() {
// Correct UV coordinates for aspect ratio
vec2 newUv = (vUv - vec2(0.5)) * uResolution.zw + vec2(0.5);
// get color from texture
vec3 color = texture2D(uTexture, newUv).rgb;
gl_FragColor = vec4(color, 1.0);
}
Warm-up
As a warm-up, we’ll create a few simple expressions to build intuition. (7 in total)
To make the behavior easier to understand, we’ll temporarily avoid using the texture and display only a white plane.
We’ll also use the UV coordinates before applying any aspect-ratio correction.
// vec2 newUv = (vUv - vec2(0.5)) * uResolution.zw + vec2(0.5);
vec2 newUv = vUv;
// vec3 color = texture2D(uTexture, newUv).rgb;
vec3 color = vec3(1.0, 1.0, 1.0);
1. Make the Plane Gray

vec3 color = vec3(0.5, 0.5, 0.5); // Gray
First, let’s make the plane gray.
When the same value is assigned to the R, G, and B channels, the result is grayscale.

vec3 color = vec3(0.8, 0.8, 0.8); // light gray
If you increase the values, such as using vec3(0.8, 0.8, 0.8), the color gets closer to vec3(1.0, 1.0, 1.0) (white), resulting in a brighter gray.
In other words, you can control the brightness by adjusting the numeric values.
2. Create a Black-to-White Gradient
We’ll create a gradient that is black at the bottom and gradually becomes white toward the top.

vec3 color = vec3(1.0, 1.0, 1.0) * newUv.y;
We make use of the UV coordinates.
Since newUv.y is 0.0 at the bottom of the screen and 1.0 at the top, using this value directly as brightness creates a gradient that becomes brighter toward the top.
3. Gradient Toward the Center from Both Sides
This is a gradient where the center is the brightest.
It’s easier to understand this gradient if you break the idea into two parts.

First, as shown in the image below, let’s set aside the top half for now and focus on making the area brighten from the bottom toward the center.

float gradation = 1.0;
gradation *= smoothstep(0.0, 0.5, newUv.y); // Gradient from the bottom toward the center
vec3 color = vec3(1.0, 1.0, 1.0) * gradation;
Here, the smoothstep function comes into play.
The key point of smoothstep is that its return value always stays within the range 0.0–1.0.
In this case, since we are using smoothstep(0.0, 0.5, newUv.y):
- When y ≤ 0.0 → 0.0
- When 0.0 < y < 0.5 → a smoothly changing value between 0.0 and 1.0
- When y ≥ 0.5 → 1.0
As a result, the area up to 0.5 (the center of the screen) becomes a gradient, and everything above that turns white.
Next, let’s do the same thing on the opposite side.
For now, we’ll ignore the bottom half and focus on making the area brighten from the top toward the center.

gradation *= smoothstep(1.0, 0.5, newUv.y); // gradient from the top toward the center
The expression for the opposite side looks like this.
In this case, it returns:
- When y ≤ 0.5 → 1.0
- When 0.5 < y < 1.0 → a value that smoothly changes from 1.0 to 0.0
- When y ≥ 1.0 → 0.0
Finally, by multiplying the two values together, you get a gradient that fades toward the center from both sides.
float gradation = 1.0;
gradation *= smoothstep(0.0, 0.5, newUv.y); // gradient from the bottom toward the center
gradation *= smoothstep(1.0, 0.5, newUv.y); // gradient from the top toward the center
vec3 color = vec3(1.0, 1.0, 1.0) * gradation;
gl_FragColor = vec4(color, 1.0);
4. Two Colors: Black and White
Instead of a gradient, we’ll create a crisp black-and-white border pattern.

float border = step(0.5, newUv.y); // returns either 0.0 or 1.0
vec3 color = vec3(1.0, 1.0, 1.0) * border;
This can be achieved using the step function.
The step function does not return smooth values—instead, it returns only 0.0 or 1.0.
In this example, since we use step(0.5, newUv.y):
- When y < 0.5 (the bottom half of the screen) → 0.0
- When y ≥ 0.5 (the top half of the screen) → 1.0
As a result, the bottom half of the screen becomes black, and the top half becomes white.
5. Two Colors: Gray and Black

float border = step(0.5, newUv.y) * 0.5; // turn the white areas into gray
vec3 color = vec3(1.0, 1.0, 1.0) * border;
We multiply by 0.5 because we want the areas that were previously output as white (1.0) to be rendered as gray instead.
6. Increase the Number of Lines (Create Repetition)
Let’s think about how to increase the number of lines, as shown below.
At first glance it looks like four lines, but if you consider “gray + black” as one set, it actually consists of two sets.

irst, let’s see what happens when we scale the UV coordinates.
We’ll try doubling only the y-coordinate.

+ newUv.y *= 2.0; // double the value
float border = step(0.5, newUv.y) * 0.5; // gray + black
vec3 color = vec3(1.0, 1.0, 1.0) * border;
gl_FragColor = vec4(color, 1.0);
Since it’s hard to see what’s happening, I’ve outlined the area with a pink frame as shown in the image below.
In the original state (Warm-up 5), the pattern ends around the middle of the screen, and the area above the center is stretched out as solid white.

However, it looks like if the result from the bottom half were directly repeated in the top half, we would end up with two sets (= four stripes).
In other words, it seems we just need to repeat the bottom half once more.

newUv.y *= 2.0; // double the value
+ newUv.y = fract(newUv.y); // extract only the fractional part
float border = step(0.5, newUv.y) * 0.5; // gray + black
vec3 color = vec3(1.0, 1.0, 1.0) * border;
gl_FragColor = vec4(color, 1.0);
This can be achieved using the fract function.
For example:
- When x = 1.3 → 0.3
- When x = 1.9 → 0.9
- When x = 2.9 → 0.9
- …
When you multiply newUv.y by 2, the value range becomes 0.0–2.0. By applying fract, you can bring it back into the 0.0–1.0 range. This process causes the same pattern of values to repeat.
By the way, if you multiply newUv.y by 100, the pattern will repeat 100 times vertically.

newUv.y *= 100.0; // repeat 100 times along the y-axis
7. Create Regions with Different Colors
Next, let’s consider how to assign a different color to each line, like this:

Intuitively, it seems like this would work if we divide the area into five horizontal sections and make it so that the same value is returned within each section. (See the illustration below.)

First, let’s try writing the code like this.
newUv.y *= 5.0; // multiply uv.y by 5
newUv.y = floor(newUv.y); // create five regions that each share the same value
As explained in Warm-up 6, this is what happens when we multiply the UV coordinates by 5.
Here, the floor function comes into play.
In this case, the function returns values like:
- When newUv.y = 1.0 → 1.0
- When newUv.y = 1.3 → 1.0
- When newUv.y = 2.9 → 2.0
- When newUv.y = 3.1 → 3.0
- When newUv.y = 3.4 → 3.0
As you can see, one key characteristic of floor is that it returns the same value within a certain range.
In this example, since newUv.y is multiplied by 5, the return value of floor(newUv.y) becomes one of the fixed values
0, 1, 2, 3, 4.
By using these values as identifiers for each region, we can assign the same value to each vertically divided section.
However, these values are still too large and not convenient to use directly as colors, so we need to convert them into a range between 0.0 and 1.0.
In addition, instead of using sequential values like 0.1, 0.2, 0.3, we can use pseudo-random values such as 0.6, 0.2, or 0.8 to produce more irregular-looking colors.
To do that, we’ll create a random1d function.
+ // Returns a pseudo-random value in the range 0.0–1.0
+ // Reference:
+ // https://thebookofshaders.com/10/
+ // https://gist.github.com/patriciogonzalezvivo/670c22f3966e662d2f83
+ float random1d(float x) {
+ return fract(sin(x) * 43758.5453123);
+ }
void main() {
vec2 newUv = vUv;
newUv.y *= 5.0; // multiply uv.y by 5
newUv.y = floor(newUv.y); // create five regions that each share the same value
+ float border = random1d(newUv.y); // return a pseudo-random value between 0.0 and 1.0
+ vec3 color = vec3(1.0, 1.0, 1.0) * border;
gl_FragColor = vec4(color, 1.0);
}
The random1d function is a pseudo-random function that always returns the same value in the range 0.0–1.0 for a given input.
I found the explanation of how this kind of random generation works in The Book of Shaders particularly easy to understand. In addition, many custom random and noise functions have been created by different people and are widely shared online (see this reference).
Because of this, when you pass the result of floor(newUv.y) as the input—as in this example—you get a different value for each region.
For example, it may return values like:
- When newUv.y = 0.0 → 0.3
- When newUv.y = 1.0 → 0.6
- When newUv.y = 2.0 → 0.5
- When newUv.y = 3.0 → 0.2
- When newUv.y = 4.0 → 0.9
(The numbers above are just illustrative examples.)
These values can then be used directly as colors.
Creating the Glitch Effects
Now that the warm-up is done, it’s finally time to create the glitch effects.
1. Scan Line
This effect overlays many thin horizontal lines on top of the image. Let’s create it.

To make the behavior easier to see, we’ll temporarily hide the texture and display only a white plane.
void main() {
vec2 newUv = (vUv - vec2(0.5)) * uResolution.zw + vec2(0.5);
// vec3 color = texture2D(uTexture, newUv).rgb;
vec3 color = vec3(1.0, 1.0, 1.0);
gl_FragColor = vec4(color, 1.0);
}
As explained in Warm-up 6, we can create a large number of stripes using the same approach.
This time, instead of a crisp black-and-white look, we want a slightly softer impression, so we’ll use the smoothstep function from Warm-up 3.

void main() {
vec2 newUv = (vUv - vec2(0.5)) * uResolution.zw + vec2(0.5);
// vec3 color = texture2D(uTexture, newUv).rgb;
vec3 color = vec3(1.0, 1.0, 1.0);
+ float baseScanLine = 1.0;
+ // Scale `uv.y` by 300 to create 300 lines.
+ vec2 scanLineUv = vUv * vec2(1.0, 300.0);
+ scanLineUv = fract(scanLineUv);
+ // Make the line pattern a gradient
+ baseScanLine = smoothstep(0.0, 0.5, scanLineUv.y);
+ baseScanLine *= smoothstep(1.0, 0.5, scanLineUv.y);
+ color *= baseScanLine;
gl_FragColor = vec4(color, 1.0);
}
Once you’ve confirmed the shape of the scan lines, bring the texture back.

vec3 color = texture2D(uTexture, newUv).rgb;
// vec3 color = vec3(1.0, 1.0, 1.0);
This would be fine as a finished result, but it looks a bit dark, so we’ll adjust the brightness.
This part is purely a matter of preference, so you can skip it if you like.

baseScanLine *= smoothstep(1.0, 0.5, scanLineUv.y);
+ // Adjust the brightness
+ float minBrightness = 0.5;
+ baseScanLine = mix(minBrightness, 1.0, baseScanLine);
color *= baseScanLine;
There are many ways to approach this, but this time we’ll use the mix function.
For example:
- mix(red, blue, 0.8) → 20% red, 80% blue
- mix(red, blue, 0.3) → 70% red, 30% blue
This is how you can blend colors together.
For a more intuitive explanation of the mix function, the diagrams in The Book of Shaders may be helpful.
In this case, since we use baseScanLine = mix(0.5, 1.0, baseScanLine), the values 0.5 and 1.0 are blended according to the ratio given by baseScanLine.
As a result, the output range becomes 0.5–1.0, making the overall image brighter compared to the original 0.0–1.0 range.
With that, the Scan Line effect is complete 🎉
fragment shader
void main() {
vec2 newUv = (vUv - vec2(0.5)) * uResolution.zw + vec2(0.5);
vec3 color = texture2D(uTexture, newUv).rgb;
// vec3 color = vec3(1.0, 1.0, 1.0);
float baseScanLine = 1.0;
// scale uv.y by 300 to create 300 lines
vec2 scanLineUv = vUv * vec2(1.0, 300.0);
scanLineUv = fract(scanLineUv);
// make the line pattern a gradient
baseScanLine = smoothstep(0.0, 0.5, scanLineUv.y);
baseScanLine *= smoothstep(1.0, 0.5, scanLineUv.y);
// adjust the brightness
float minBrightness = 0.5;
baseScanLine = mix(minBrightness, 1.0, baseScanLine);
color *= baseScanLine;
gl_FragColor = vec4(color, 1.0);
}
2. Chromatic Aberration
Next, we’ll create a chromatic aberration effect.

For clarity, we’ll implement each effect separately in this article.
So first, we’ll start from a state with no effects applied.
Revert the fragment shader to the following state, and make sure the texture is displayed as-is.

void main() {
vec2 newUv = (vUv - vec2(0.5)) * uResolution.zw + vec2(0.5);
vec3 color = texture2D(uTexture, newUv).rgb;
gl_FragColor = vec4(color, 1.0);
}
Chromatic aberration can be achieved by sampling colors from UV coordinates that are slightly offset from their original positions.
In other words, conceptually, you simply add an offset to newUv, like this:
- vec3 color = texture2D(uTexture, newUv).rgb;
+ vec3 color = texture2D(uTexture, newUv + offset).rgb; // newUvを少しずらす
However, it doesn’t make sense to offset all color channels by the same amount.
If you do that, the entire image will just shift, and you won’t get any color separation.
Chromatic aberration occurs because the R, G, and B channels are sampled from slightly different positions, so you need to use different offsets for each channel.
To keep things simple, we’ll offset only the x-coordinate this time.
That’s why we add 0.0 to newUv.y.

// Original color
- vec3 color = texture2D(uTexture, newUv).rgb;
// Per-channel offsets
+ float offsetXR = 0.021;
+ float offsetXG = 0.015;
+ float offsetXB = 0.01;
// Each color channel
// Sample the color from a shifted position only along the x-axis
+ float r = texture2D(uTexture, newUv + vec2(offsetXR, 0.0)).r;
+ float g = texture2D(uTexture, newUv + vec2(offsetXG, 0.0)).g;
+ float b = texture2D(uTexture, newUv + vec2(offsetXB, 0.0)).b;
+ vec3 color = vec3(r, g, b);
The chromatic aberration is a bit too strong, so we’ll tone it down by multiplying it with an arbitrary factor.

+ float offsetXR = 0.021 * 0.6; // Reduce the strength of the effect by multiplying it with an arbitrary factor
+ float offsetXG = 0.015 * 0.6;
+ float offsetXB = 0.01 * 0.6;
With that, the Chromatic Aberration effect is complete!
fragment shader
void main() {
vec2 newUv = (vUv - vec2(0.5)) * uResolution.zw + vec2(0.5);
// vec3 color = texture2D(uTexture, newUv).rgb;
// Per-channel offsets
float offsetXR = 0.021 * 0.6;
float offsetXG = 0.015 * 0.6;
float offsetXB = 0.01 * 0.6;
// Each color channel
// Sample the color from a shifted position only along the x-axis
float r = texture2D(uTexture, newUv + vec2(offsetXR, 0.0)).r;
float g = texture2D(uTexture, newUv + vec2(offsetXG, 0.0)).g;
float b = texture2D(uTexture, newUv + vec2(offsetXB, 0.0)).b;
vec3 color = vec3(r, g, b);
gl_FragColor = vec4(color, 1.0);
}
3. Deform Line
Next, we’ll create the Deform Line effect.
Visually, this effect looks like only certain horizontal lines are shifted left and right.
First, let’s take a look at the finished result.
Once again, we’ll start with a clean fragment shader.
void main() {
vec2 newUv = (vUv - vec2(0.5)) * uResolution.zw + vec2(0.5);
vec3 color = texture2D(uTexture, newUv).rgb;
gl_FragColor = vec4(color, 1.0);
}
The Deform Line effect can be implemented by:
- Dividing the screen into regions along the vertical direction (6 sections in this case)
- For each region,
- Sampling colors from UV coordinates that are randomly shifted left or right
This approach is very similar to the mechanism we built in Warm-up 7.

First, to make debugging easier, we’ll comment out the texture and display a white plane.
void main() {
vec2 newUv = (vUv - vec2(0.5)) * uResolution.zw + vec2(0.5);
// vec3 color = texture2D(uTexture, newUv).rgb; // textureの色
vec3 color = vec3(1.0, 1.0, 1.0);
gl_FragColor = vec4(color, 1.0);
}
Next, just like in Warm-up 7, we’ll use floor and random1d to create regions where the same value is shared within each section.

+ float random1d(float y) {
+ return fract(sin(x) * 43758.5453);
+ }
void main() {
vec2 newUv = (vUv - vec2(0.5)) * uResolution.zw + vec2(0.5);
// vec3 color = texture2D(uTexture, newUv).rgb;
+ vec2 uv = vUv;
+ uv.y *= 6.0; // Multiply uv.y by 6
+ uv.y = floor(uv.y); // Create six regions that share the same value
+ float deformLine = random1d(uv.y); // Return a pseudo-random value between 0.0 and 1.0
+ vec3 color = vec3(1.0, 1.0, 1.0) * deformLine;
gl_FragColor = vec4(color, 1.0);
}
With this, each horizontal line now has a different value.
Next, we’ll use these values to sample colors from horizontally shifted UV coordinates.
This idea is the same as what we used for Chromatic Aberration.

void main() {
vec2 newUv = (vUv - vec2(0.5)) * uResolution.zw + vec2(0.5);
- // vec3 color = texture2D(uTexture, newUv).rgb;
vec2 uv = vUv;
uv.y *= 6.0; // Multiply uv.y by 6
uv.y = floor(uv.y); // Create six regions that share the same value
float deformLine = random1d(uv.y); // Return a pseudo-random value between 0.0 and 1.0
+ vec3 color = texture2D(uTexture, newUv + vec2(deformLine, 0.0)).rgb; // Sample the color from a shifted position
- vec3 color = vec3(1.0, 1.0, 1.0) * deformLine;
gl_FragColor = vec4(color, 1.0);
}
The effect is a bit too strong, so we’ll reduce its intensity by multiplying it with an arbitrary facto

float deformLine = random1d(uv.y); // return a pseudo-random value between 0.0 and 1.0
+ deformLine *= 0.03; // adjust the intensity
Finally, we’ll make the lines continuously flow downward.
This can be achieved simply by adding uTime to uv.y.
- uv.y = floor(uv.y); // create six regions that share the same value
+ uv.y = floor(uv.y + uTime * 1.5); // create six regions that share the same value, and keep them moving
And that’s it—this completes the effect! 🎉
The full code is shown below.
fragment shader
// Pseudo-random number generator (returns 0.0 to 1.0)
// Reference:
// https://thebookofshaders.com/10/
// https://gist.github.com/patriciogonzalezvivo/670c22f3966e662d2f83
float random1d(float x) {
return fract(sin(x) * 43758.5453);
}
void main() {
vec2 newUv = (vUv - vec2(0.5)) * uResolution.zw + vec2(0.5);
vec2 uv = vUv;
uv.y *= 6.0; // multiply uv.y by 6
uv.y = floor(uv.y + uTime * 1.5); // create six regions that share the same value
float deformLine = random1d(uv.y); // return a pseudo-random value between 0.0 and 1.0
deformLine *= 0.03; // adjust the intensity
vec3 color = texture2D(uTexture, newUv + vec2(deformLine, 0.0)).rgb; // sample the color from a shifted position
gl_FragColor = vec4(color, 1.0);
}
4. Combining the Three Glitch Effects
Let’s combine all the glitch effects we’ve created so far and display them together.
For the Deform Line effect, dividing the screen into six sections felt a bit too fine, so I changed it to four sections instead.
fragment shader
precision mediump float;
uniform float uTime;
uniform sampler2D uTexture;
uniform vec4 uResolution;
varying vec2 vUv;
// Pseudo-random number generator (returns 0.0 to 1.0)
// Reference:
// https://thebookofshaders.com/10/
// https://gist.github.com/patriciogonzalezvivo/670c22f3966e662d2f83
float random1d(float x) {
return fract(sin(x) * 43758.5453);
}
void main() {
vec2 newUv = (vUv - vec2(0.5)) * uResolution.zw + vec2(0.5);
/**
* 1. Deform Line Effect (horizontal distortion)
*/
vec2 uv = vUv;
uv.y *= 4.0;
uv.y = floor(uv.y + uTime * 1.5);
float deformLine = random1d(uv.y);
deformLine *= 0.03;
vec2 distortedUv = newUv + vec2(deformLine, 0.0);
/**
* 2. Chromatic Aberration Effect
*/
float offsetXR = 0.021 * 0.5;
float offsetXG = 0.015 * 0.5;
float offsetXB = 0.01 * 0.5;
float r = texture2D(uTexture, distortedUv + vec2(offsetXR, 0.0)).r;
float g = texture2D(uTexture, distortedUv + vec2(offsetXG, 0.0)).g;
float b = texture2D(uTexture, distortedUv + vec2(offsetXB, 0.0)).b;
vec3 color = vec3(r, g, b);
/**
* 3. Chromatic Aberration Effect
*/
float baseScanLine = 1.0;
vec2 scanLineUv = vUv * vec2(1.0, 300.0);
scanLineUv = fract(scanLineUv);
baseScanLine = smoothstep(0.0, 0.5, scanLineUv.y);
baseScanLine *= smoothstep(1.0, 0.5, scanLineUv.y);
float minBrightness = 0.5;
baseScanLine = mix(minBrightness, 1.0, baseScanLine);
color *= baseScanLine;
gl_FragColor = vec4(color, 1.0);
}
Let’s Add More Glitch Effects
In the main explanation, we covered three glitch effects, but I also created a version with many more glitches layered together.
Some of these should be understandable using the concepts we’ve covered so far, so I encourage you to give it a try 💪
From here on, the effects include intense flickering, so I added a button to toggle the animation state.
Be careful with overly aggressive glitch effects 🫠 (a reminder to myself as well).
Five Glitch Effects
The following seven glitch effects are combined:
- Base Scan Line
- Chromatic Aberration
- Deform Line (two variations)
- Scan Line Down
- Ghost
fragment shader
precision mediump float;
uniform float uTime;
uniform sampler2D uTexture;
uniform vec4 uResolution;
uniform bool uGlitchEnabled;
varying vec2 vUv;
// https://thebookofshaders.com/10/
// https://gist.github.com/patriciogonzalezvivo/670c22f3966e662d2f83
float random1d(float x) {
return fract(sin(x) * 43758.5453123);
}
void main() {
vec2 newUv = (vUv - vec2(0.5)) * uResolution.zw + vec2(0.5);
/**
* 1. Deform Line Effects (horizontal distortion)
*/
float deformLine1 = 0.0;
float deformLine2 = 0.0;
if (uGlitchEnabled) {
{
// deformLine1 (slow-moving lines)
vec2 uv = vUv;
uv.y *= 4.0;
uv.y = floor(uv.y + uTime * 1.5);
deformLine1 = random1d(uv.y) - 0.5;
deformLine1 *= 0.03;
}
{
// deformLine2 (fast-moving lines)
vec2 uv = vUv;
uv.y = step(0.01, fract(uv.y + uTime * 2.5));
deformLine2 = random1d(uv.y) - 0.5;
deformLine2 *= 0.03;
}
}
vec2 distortedUv = newUv + vec2(deformLine1 + deformLine2, 0.0);
/**
* 2. Chromatic Aberration Effect
*/
float offsetXR = 0.021 * 0.5;
float offsetXG = 0.015 * 0.5;
float offsetXB = 0.01 * 0.5;
float r = texture2D(uTexture, distortedUv + vec2(offsetXR, 0.0)).r;
float g = texture2D(uTexture, distortedUv + vec2(offsetXG, 0.0)).g;
float b = texture2D(uTexture, distortedUv + vec2(offsetXB, 0.0)).b;
vec3 color = vec3(r, g, b);
/**
* 3. Ghost Effect
*/
if (uGlitchEnabled) {
float flickerTime = floor(uTime * 50.0);
float flicker = random1d(flickerTime);
float flickerX = random1d(flickerTime + 200.0);
float flickerY = random1d(flickerTime + 100.0);
float offsetY = (flickerY - 0.5);
offsetY *= 0.1;
float offsetX = (flickerX - 0.5);
offsetX *= 0.1;
vec3 ghostColor = texture2D(uTexture, distortedUv + vec2(offsetX, offsetY)).rgb;
float mixFactor = flicker > 0.8 ? 0.2 : 0.0;
color = mix(color, ghostColor, mixFactor);
}
/**
* 4. Base Scan Line Effect
*/
float baseScanLine = 1.0;
{
vec2 scanLineUv = vUv * vec2(1.0, 300.0);
scanLineUv = fract(scanLineUv);
baseScanLine = smoothstep(0.0, 0.5, scanLineUv.y);
baseScanLine *= smoothstep(1.0, 0.5, scanLineUv.y);
float minBrightness = 0.5;
baseScanLine = mix(minBrightness, 1.0, baseScanLine);
}
/**
* 5. Scan Line Down Effect (moving scan line with flicker)
*/
float scanLineDown = 1.0;
if (uGlitchEnabled) {
vec2 scanLineDownUv = vUv * vec2(1.0, 8.0);
scanLineDownUv.y = floor(scanLineDownUv.y + 5.0 * uTime);
scanLineDown = random1d(scanLineDownUv.y);
float isFlickerOn = step(0.0, sin(uTime * 1000.0));
float minBrightness = isFlickerOn == 1.0 ? 0.8 : 0.95;
scanLineDown = mix(minBrightness, 1.0, scanLineDown);
}
color = color * baseScanLine * scanLineDown;
gl_FragColor = vec4(color, 1.0);
}
Nine Glitch Effects
The following nine glitch effects are layered together:
- Base Scan Line
- Chromatic Aberration
- Deform Line (two variations)
- Scan Line Down
- Ghost
- Wave
- Wave Form Warp
- Block Noise
- Grain Noise
fragment shader
precision mediump float;
uniform float uTime;
uniform sampler2D uTexture;
uniform vec4 uResolution;
uniform bool uGlitchEnabled;
varying vec2 vUv;
// https://thebookofshaders.com/10/
// https://gist.github.com/patriciogonzalezvivo/670c22f3966e662d2f83
float random1d(float x) {
return fract(sin(x) * 43758.5453123);
}
// Author @patriciogv - 2015
// http://patriciogonzalezvivo.com
// random2d function (returns 0.0 to 1.0)
float random2d(vec2 st) {
return fract(sin(dot(st.xy, vec2(12.9898, 78.233))) * 43758.5453123);
}
// Simplex 2D noise
vec3 permute(vec3 x) {
return mod(((x * 34.0) + 1.0) * x, 289.0);
}
float snoise(vec2 v) {
const vec4 C = vec4(0.211324865405187, 0.366025403784439, -0.577350269189626, 0.024390243902439);
vec2 i = floor(v + dot(v, C.yy));
vec2 x0 = v - i + dot(i, C.xx);
vec2 i1;
i1 = (x0.x > x0.y) ? vec2(1.0, 0.0) : vec2(0.0, 1.0);
vec4 x12 = x0.xyxy + C.xxzz;
x12.xy -= i1;
i = mod(i, 289.0);
vec3 p = permute(permute(i.y + vec3(0.0, i1.y, 1.0)) + i.x + vec3(0.0, i1.x, 1.0));
vec3 m = max(0.5 - vec3(dot(x0, x0), dot(x12.xy, x12.xy), dot(x12.zw, x12.zw)), 0.0);
m = m * m;
m = m * m;
vec3 x = 2.0 * fract(p * C.www) - 1.0;
vec3 h = abs(x) - 0.5;
vec3 ox = floor(x + 0.5);
vec3 a0 = x - ox;
m *= 1.79284291400159 - 0.85373472095314 * (a0 * a0 + h * h);
vec3 g;
g.x = a0.x * x0.x + h.x * x0.y;
g.yz = a0.yz * x12.xz + h.yz * x12.yw;
return 130.0 * dot(m, g);
}
void main() {
vec2 newUv = (vUv - vec2(0.5)) * uResolution.zw + vec2(0.5);
/**
* 1. Wave Form Warp Effect
*/
float waveFormWarp = 0.0;
if (uGlitchEnabled) {
float glitchTime = uTime * 20.0 + random1d(vUv.y);
float glitch = sin(glitchTime) + sin(glitchTime * 3.24) + sin(glitchTime * 9.21);
glitch /= 3.0;
glitch = smoothstep(-0.5, 0.6, glitch);
glitch *= 0.005;
waveFormWarp = (random1d(vUv.y + uTime) - 0.5) * glitch;
}
/**
* 2. Wave Effect
*/
float wave = 0.0;
if (uGlitchEnabled) {
float flicker = sin(uTime * 20.0);
float noise = snoise(vec2(vUv.y, uTime * 90.0));
wave = flicker > 0.5 ? sin(noise * 20.0) * 0.03 : 0.0;
}
/**
* 3. Block Noise Effect
*/
float blockNoise = 0.0;
if (uGlitchEnabled) {
float noise1 = snoise(floor(vUv * vec2(10.0, 12.0)) + uTime);
float noise2 = snoise(floor(vUv * vec2(6.0, 4.0)) + uTime);
float noise3 = snoise(floor(vUv * vec2(3.0, 8.0)) + uTime);
blockNoise = noise1 * noise2 * noise3;
blockNoise = step(0.3, blockNoise);
float flickerTime = floor(uTime * 50.0);
float flicker = random1d(flickerTime);
blockNoise = flicker > 0.5 ? blockNoise : 0.0;
blockNoise *= 0.5;
}
/**
* 4. Deform Line Effects (horizontal distortion)
*/
float deformLine1 = 0.0;
float deformLine2 = 0.0;
if (uGlitchEnabled) {
{
// deformLine1 (slow-moving lines)
vec2 uv = vUv;
uv.y *= 4.0;
uv.y = floor(uv.y + uTime * 1.5);
deformLine1 = random1d(uv.y) - 0.5;
deformLine1 *= 0.03;
}
{
// deformLine2 (fast-moving lines)
vec2 uv = vUv;
uv.y = step(0.01, fract(uv.y + uTime * 2.5));
deformLine2 = random1d(uv.y) - 0.5;
deformLine2 *= 0.03;
}
}
vec2 distortedUv = newUv + vec2(blockNoise + waveFormWarp + deformLine1 + deformLine2 + wave, 0.0);
/**
* 5. Chromatic Aberration Effect
*/
float offsetXR = 0.021 * 0.5;
float offsetXG = 0.015 * 0.5;
float offsetXB = 0.01 * 0.5;
float r = texture2D(uTexture, distortedUv + vec2(offsetXR, 0.0)).r;
float g = texture2D(uTexture, distortedUv + vec2(offsetXG, 0.0)).g;
float b = texture2D(uTexture, distortedUv + vec2(offsetXB, 0.0)).b;
vec3 color = vec3(r, g, b);
/**
* 6. Ghost Effect
*/
if (uGlitchEnabled) {
float flickerTime = floor(uTime * 50.0);
float flicker = random1d(flickerTime);
float flickerX = random1d(flickerTime + 200.0);
float flickerY = random1d(flickerTime + 100.0);
float offsetY = (flickerY - 0.5);
offsetY *= 0.1;
float offsetX = (flickerX - 0.5);
offsetX *= 0.1;
vec3 ghostColor = texture2D(uTexture, distortedUv + vec2(offsetX, offsetY)).rgb;
float mixFactor = flicker > 0.8 ? 0.2 : 0.0;
color = mix(color, ghostColor, mixFactor);
}
/**
* 7. Base Scan Line Effect
*/
float baseScanLine = 1.0;
{
vec2 scanLineUv = vUv * vec2(1.0, 300.0);
scanLineUv = fract(scanLineUv);
baseScanLine = smoothstep(0.0, 0.5, scanLineUv.y);
baseScanLine *= smoothstep(1.0, 0.5, scanLineUv.y);
float minBrightness = 0.5;
baseScanLine = mix(minBrightness, 1.0, baseScanLine);
}
/**
* 8. Scan Line Down Effect (moving scan line with flicker)
*/
float scanLineDown = 1.0;
if (uGlitchEnabled) {
vec2 scanLineDownUv = vUv * vec2(1.0, 8.0);
scanLineDownUv.y = floor(scanLineDownUv.y + 5.0 * uTime);
scanLineDown = random1d(scanLineDownUv.y);
float isFlickerOn = step(0.0, sin(uTime * 1000.0));
float minBrightness = isFlickerOn == 1.0 ? 0.8 : 0.95;
scanLineDown = mix(minBrightness, 1.0, scanLineDown);
}
/**
* 9. Grain Noise Effect
*/
float grainNoise = 1.0;
if (uGlitchEnabled) {
grainNoise = random2d(vUv + uTime);
float brightness = 0.8;
grainNoise = mix(brightness, 1.0, grainNoise);
}
color = color * baseScanLine * scanLineDown * grainNoise;
gl_FragColor = vec4(color, 1.0);
}
In Closing
Thank you very much for reading this extremely long article all the way to the end.
Explaining shaders in words turned out to be far more difficult than I had imagined, and it made me once again appreciate and respect those who have shared detailed explanatory articles before me.
Tomorrow’s entry in the Money Forward Engineers Advent Calendar 2025 will be written by Takudai Kawasaki—look forward to it! 🎅🦌
Discussion