Torn Vignette Portal Effect (Three.js + GLSL)
published: ⏰ 5A real-time WebGL vignette portal shader with animated FBM noise edge wobble, fog masking, and an interactive control panel.
What it does
The effect renders a rectangular image inside a rounded-rectangle window whose edges are continuously distorted by layered fractal noise. A volumetric fog bleeds outward beyond the window, its shape independently modulated by a second noise field. The result is an organic, portal-like vignette that looks torn and alive.
Move the mouse over the canvas to shift the camera and get a subtle parallax. All knobs are on the control panel below the image.
Requirements
| Component | Description |
|---|---|
| Browser | Modern browser with WebGL 2.0 support |
| GLSL | ES 3.00 (WebGL 2) or ES 1.00 (WebGL 1 fallback) |
How the shader works
The fragment shader is built from three composable layers on a single fullscreen plane:
Image mask – sdRoundBox
A signed-distance function for a rounded rectangle defines the “window” shape. Points inside the box have negative distance, points outside are positive. The smoothstep between innerFade and outerFade turns this distance into a 0–1 alpha:
float d = sdRoundBox(p, vec2(uSize * uAspect, uSize), uRadius);
float imgMask = 1.0 - smoothstep(uInnerFade, uOuterFade, d);Edge wobble – FBM noise
A 5-octave fractal Brownian motion (FBM) is sampled at each pixel and multiplied by uWobble to modulate the SDF distance. The noise is evaluated in normalized image-space coordinates so it stays anchored to the picture rather than scrolling with the viewport:
float n1 = fbm(nc * 4.0 * uNoiseScale + vec2(uTime * 0.18, -uTime * 0.12));
float wob = (n1 - 0.5) * uWobble;
float imgMask = 1.0 - smoothstep(uInnerFade, uOuterFade, d + wob);The two different time coefficients on X and Y create an asymmetric drift.
Volumetric fog
A second FBM pass at a different frequency (7.0 × uNoiseScale) drives the fog. The fog fades in as you move outside the window and fades out at a distance set by outerEdge:
float fog = smoothstep(-0.30, 0.08, d + wob)
* (1.0 - smoothstep(0.05, uOuterEdge, d + (n2 - 0.5) * 0.30));
fog = clamp(fog * (0.6 + 0.4 * n2) * uFogDensity, 0.0, 1.0);The (0.6 + 0.4 * n2) factor adds a density ripple so the fog isn’t flat.
Pad field
The pad uniform expands the render plane beyond the image boundary. This creates free space where fog can grow without clipping the photo. The image UVs are scaled: uvImg = (vUv - 0.5) * k + 0.5 where k = 1.0 + uPad.
Compositing
float fogMix = clamp(fog + (1.0 - imgMask), 0.0, 1.0);
vec3 color = mix(texture2D(uTexture, uvImg).rgb, uFogColor, fogMix);Where the image is opaque (fog ≈ 0, imgMask ≈ 1), the result is the image. Where fog takes over (fog > 0 or imgMask < 1), the color shifts toward uFogColor. A final borderFade smooths the canvas edges.
Tuning parameters
| Parameter | Default | Effect |
|---|---|---|
pad | 0.30 | Free zone around the image; more room for fog to expand without clipping |
size | 1.00 | Window size; 1.0 = frame exactly at image boundary |
radius | 0.12 | Corner roundness of the window shape |
outerEdge | 0.30 | How far fog extends beyond the window edge |
innerFade | -0.20 | Distance inside the window where image fade begins |
outerFade | 0.06 | Distance outside the window where image fade ends |
wobble | 0.35 | Edge distortion amplitude from FBM noise |
speed | 1.0 | Animation time multiplier |
noiseScale | 1.0 | FBM noise frequency; higher = finer detail |
fogDensity | 1.0 | Overall fog opacity |
parallax | 1.0 | Camera movement strength on mouse move |
Full shader code
The vertex shader is minimal – it only passes the UV coordinates through:
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}The fragment shader in full – helper functions hash, noise, fbm, sdRoundBox and the main block:
uniform sampler2D uTexture;
uniform float uTime;
uniform vec3 uFogColor;
uniform float uFogDensity;
uniform float uNoiseScale;
uniform float uPad;
uniform float uSize;
uniform float uRadius;
uniform float uOuterEdge;
uniform float uInnerFade;
uniform float uOuterFade;
uniform float uWobble;
uniform float uAspect;
varying vec2 vUv;
float hash(vec2 p) { return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123); }
float noise(vec2 p) {
vec2 i = floor(p), f = fract(p);
vec2 u = f * f * (3.0 - 2.0 * f);
return mix(mix(hash(i), hash(i + vec2(1.0, 0.0)), u.x),
mix(hash(i + vec2(0.0, 1.0)), hash(i + vec2(1.0, 1.0)), u.x), u.y);
}
float fbm(vec2 p) {
float v = 0.0, a = 0.5;
mat2 rot = mat2(0.8, 0.6, -0.6, 0.8);
for (int i = 0; i < 5; i++) { v += a * noise(p); p = rot * p * 2.0; a *= 0.5; }
return v;
}
float sdRoundBox(vec2 p, vec2 b, float r) {
vec2 q = abs(p) - b + r;
return length(max(q, vec2(0.0))) + min(max(q.x, q.y), 0.0) - r;
}
void main() {
float k = 1.0 + uPad;
vec2 uvImg = (vUv - 0.5) * k + 0.5;
vec2 p = (uvImg * 2.0 - 1.0) * vec2(uAspect, 1.0);
vec2 nc = uvImg * vec2(uAspect, 1.0);
float n1 = fbm(nc * 4.0 * uNoiseScale + vec2(uTime * 0.18, -uTime * 0.12));
float n2 = fbm(nc * 7.0 * uNoiseScale - vec2(uTime * 0.10, uTime * 0.22));
float d = sdRoundBox(p, vec2(uSize * uAspect, uSize), uRadius);
float wob = (n1 - 0.5) * uWobble;
float imgMask = 1.0 - smoothstep(uInnerFade, uOuterFade, d + wob);
float fog = smoothstep(-0.30, 0.08, d + wob)
* (1.0 - smoothstep(0.05, uOuterEdge, d + (n2 - 0.5) * 0.30));
fog = clamp(fog * (0.6 + 0.4 * n2) * uFogDensity, 0.0, 1.0);
vec2 pe = abs(vUv * 2.0 - 1.0);
float borderFade = smoothstep(0.0, 0.15, 1.0 - max(pe.x, pe.y));
float fogMix = clamp(fog + (1.0 - imgMask), 0.0, 1.0);
vec3 color = mix(texture2D(uTexture, uvImg).rgb, uFogColor, fogMix);
float alpha = clamp(max(imgMask, fog), 0.0, 1.0) * borderFade;
if (alpha < 0.02) discard;
gl_FragColor = vec4(color, alpha);
}Drop the code into any Three.js ShaderMaterial with the uniforms from the table above.
Download the full code
| File | Purpose |
|---|---|
| vfx-vignette-portal.js | Ready-to-run demo scene: shader, Three.js scene, lil-gui panel, parallax |
| vfx-vignette-portal.css | Control panel styling (dark theme, control grid) |
Variants and ideas
- Solid color background – set
bgColorin the GUI. White background + black fog is the default; try#0a0a1afor a dark-void look. - Still image – set
speedto 0. The noise stays frozen; the wobble is baked from the initial time seed. - Circular portal – replace
sdRoundBoxwithlength(p) - uSizeand dropuRadius. - Multiple portals – render several planes at different depths, each with their own uniforms and time offsets.