Рваная виньетка-портал (Three.js + GLSL)
опубликовано: ⏰ 6Виньеточный эффект-портал на WebGL с анимированной деформацией края на основе FBM-шума, туманной маской и интерактивной панелью управления.
Что делает эффект
Изображение отображается внутри скруглённого прямоугольника, края которого непрерывно деформируются слоистым фрактальным шумом. За пределами окна – объёмный туман, форма которого управляется вторым полем шума. В результате получается органичный, живой виньеточный портал с эффектом рваного края.
Двигайте мышью по холсту для параллакса камеры. Все параметры – на панели управления под изображением.
Требования
| Компонент | Описание |
|---|---|
| Браузер | Современный браузер с поддержкой WebGL 2.0 |
| GLSL | ES 3.00 (WebGL 2) или ES 1.00 (запасной режим для WebGL 1) |
Как работает шейдер
Фрагментный шейдер состоит из трёх слоёв, которые смешиваются на одной плоскости:
Маска изображения – sdRoundBox
Функция расстояния со знаком (SDF) для скруглённого прямоугольника задаёт форму «окна». Точки внутри имеют отрицательное расстояние, снаружи – положительное. smoothstep между innerFade и outerFade превращает расстояние в альфа-канал 0–1:
float d = sdRoundBox(p, vec2(uSize * uAspect, uSize), uRadius);
float imgMask = 1.0 - smoothstep(uInnerFade, uOuterFade, d);Деформация края – FBM-шум
5-октавный фрактальный броуновский шум (FBM) вычисляется в каждой точке и умножается на uWobble, модулируя расстояние SDF. Шум берётся в нормализованных координатах изображения, чтобы остаться привязанным к картинке, а не к вьюпорту:
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);Разные коэффициенты времени по X и Y создают асимметричный дрейф.
Объёмный туман
Второй проход FBM на другой частоте (7.0 × uNoiseScale) управляет туманом. Туман нарастает за пределами окна и затухает на расстоянии, заданном 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);Множитель (0.6 + 0.4 * n2) добавляет пульсацию плотности, чтобы туман не был плоским.
Поле pad
Параметр pad расширяет плоскость отрисовки за границы изображения. Это создаёт свободное пространство, где туман может расти без обрезки фото. UV изображения масштабируются: uvImg = (vUv - 0.5) * k + 0.5, где k = 1.0 + uPad.
Смешивание слоёв
float fogMix = clamp(fog + (1.0 - imgMask), 0.0, 1.0);
vec3 color = mix(texture2D(uTexture, uvImg).rgb, uFogColor, fogMix);Где изображение непрозрачно (fog ≈ 0, imgMask ≈ 1) – результат – это изображение. Где берёт верх туман (fog > 0 или imgMask < 1) – цвет смещается к uFogColor. Финальный borderFade сглаживает края холста.
Параметры настройки
| Параметр | По умолч. | Эффект |
|---|---|---|
pad | 0.30 | Свободная зона вокруг изображения; больше = туман не обрезается |
size | 1.00 | Размер окна; 1.0 = рамка точно по границе изображения |
radius | 0.12 | Скругление углов формы окна |
outerEdge | 0.30 | Насколько далеко туман уходит за край окна |
innerFade | -0.20 | Расстояние внутри окна, где начинается растворение |
outerFade | 0.06 | Расстояние снаружи окна, где заканчивается растворение |
wobble | 0.35 | Амплитуда деформации края от FBM-шума |
speed | 1.0 | Множитель скорости анимации |
noiseScale | 1.0 | Частота FBM-шума; выше = мельче детали |
fogDensity | 1.0 | Общая непрозрачность тумана |
parallax | 1.0 | Сила движения камеры при движении мыши |
Полный код шейдера
Вершинный шейдер – минимум, достаточно передать координаты UV:
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}Фрагментный шейдер целиком – вспомогательные функции hash, noise, fbm, sdRoundBox и основной блок main:
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);
}Код можно вставить в любой ShaderMaterial Three.js с соответствующими юниформами из таблицы выше.
Скачивание полного кода
| Файл | Назначение |
|---|---|
| vfx-vignette-portal.js | Готовая демо-сцена: шейдер, сцена Three.js, панель lil-gui, параллакс |
| vfx-vignette-portal.css | Оформление панели управления (тёмная тема, сетка контролов) |
Вариации и идеи
- Фон другого цвета – настройте
bgColorв панели. Белый фон + чёрный туман – стандарт; попробуйте#0a0a1aдля тёмной бездны. - Статичный кадр – установите
speedв 0. Шум застынет; деформация сохранит вид, «запечённый» из начального значения времени. - Круглый портал – замените
sdRoundBoxнаlength(p) - uSizeи уберитеuRadius. - Несколько порталов – отрисуйте несколько плоскостей на разных глубинах, каждая со своими юниформами и смещением времени.