Рваная виньетка-портал (Three.js + GLSL)

опубликовано: ⏰ 6

Виньеточный эффект-портал на WebGL с анимированной деформацией края на основе FBM-шума, туманной маской и интерактивной панелью управления.

Loading demo…
Двигайте мышью для параллакса. Панель управления -- под изображением.

Что делает эффект

Изображение отображается внутри скруглённого прямоугольника, края которого непрерывно деформируются слоистым фрактальным шумом. За пределами окна – объёмный туман, форма которого управляется вторым полем шума. В результате получается органичный, живой виньеточный портал с эффектом рваного края.

Двигайте мышью по холсту для параллакса камеры. Все параметры – на панели управления под изображением.

Требования

КомпонентОписание
БраузерСовременный браузер с поддержкой WebGL 2.0
GLSLES 3.00 (WebGL 2) или ES 1.00 (запасной режим для WebGL 1)

Как работает шейдер

Фрагментный шейдер состоит из трёх слоёв, которые смешиваются на одной плоскости:

Маска изображения – sdRoundBox

Функция расстояния со знаком (SDF) для скруглённого прямоугольника задаёт форму «окна». Точки внутри имеют отрицательное расстояние, снаружи – положительное. smoothstep между innerFade и outerFade превращает расстояние в альфа-канал 0–1:

Glsl
float d = sdRoundBox(p, vec2(uSize * uAspect, uSize), uRadius);
float imgMask = 1.0 - smoothstep(uInnerFade, uOuterFade, d);

Деформация края – FBM-шум

5-октавный фрактальный броуновский шум (FBM) вычисляется в каждой точке и умножается на uWobble, модулируя расстояние SDF. Шум берётся в нормализованных координатах изображения, чтобы остаться привязанным к картинке, а не к вьюпорту:

Glsl
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:

Glsl
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.

Смешивание слоёв

Glsl
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 сглаживает края холста.

Параметры настройки

ПараметрПо умолч.Эффект
pad0.30Свободная зона вокруг изображения; больше = туман не обрезается
size1.00Размер окна; 1.0 = рамка точно по границе изображения
radius0.12Скругление углов формы окна
outerEdge0.30Насколько далеко туман уходит за край окна
innerFade-0.20Расстояние внутри окна, где начинается растворение
outerFade0.06Расстояние снаружи окна, где заканчивается растворение
wobble0.35Амплитуда деформации края от FBM-шума
speed1.0Множитель скорости анимации
noiseScale1.0Частота FBM-шума; выше = мельче детали
fogDensity1.0Общая непрозрачность тумана
parallax1.0Сила движения камеры при движении мыши

Полный код шейдера

Вершинный шейдер – минимум, достаточно передать координаты UV:

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

Фрагментный шейдер целиком – вспомогательные функции hash, noise, fbm, sdRoundBox и основной блок main:

Glsl
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.
  • Несколько порталов – отрисуйте несколько плоскостей на разных глубинах, каждая со своими юниформами и смещением времени.

Ссылки

Webgl Three.js Glsl Shader Vignette