Создание эффекта квадратной линзы, следующей за мышью, с помощью Three.js и GLSL | Кодропс

Создание эффекта квадратной линзы, следующей за мышью, с помощью Three.js и GLSL | Кодропс



Примечание редактора: Мы рады, что Томоюки Наката, креативный разработчик из baqemono, присоединился к нашему небольшому празднованию Three.js в преддверии самой первой конференции Three.js в Париже. В рамках нашего марафона Three.js он представляет нам этот фантастический эффект линзы, следующей за мышью, созданный с помощью Three.js и GLSL. Мы очень рады, что он будет с нами в поездке! Наслаждаться!

🇫🇷 Подождите… у вас все еще нет билета? Самая первая конференция Three.js приближается в Париж, и билеты не будут ждать вечно. Использовать код CODROPS для скидка 15% и хватайте билеты, пока они не распроданы →

Идея

Что произойдет, если объединить изображение в оттенках серого, цветное изображение и эффект линзы, следующий за мышью? В этом уроке мы создадим эффект квадратной линзы, следующей за мышью, используя Three.js и GLSL.

Эффект накладывает два изображения: изображение в оттенках серого покрывает экран, а цветное изображение отображается через квадратную область, следующую за указателем мыши. Внутри этого квадрата мы добавляем искажение линзы и радиальный сдвиг RGB, в то время как изображение в оттенках серого имеет свои собственные тонкие волны и шумоподобное движение.

Хотя конечный результат выглядит довольно сложным, эффект строится из небольшого количества простых деталей. Мы создадим квадратную маску, сохраним ее квадратной независимо от размера области просмотра, применим эффекты линзы и сдвига RGB во фрагментном шейдере и плавно анимируем квадрат, когда он следует за мышью. Мы также добавим графический интерфейс, чтобы различные параметры можно было регулировать в режиме реального времени.

Идея этого проекта возникла, когда я просматривал Pinterest и нашел дизайн, в котором часть изображения в оттенках серого кажется вырезанной цветным квадратом с эффектом линзы. Вы можете увидеть оригинальную ссылку здесь. Это выглядело как что-то, что можно было бы воссоздать с помощью WebGL, и я подумал, что результат был бы еще интереснее, если бы квадрат следовал за мышью, а изображение в оттенках серого на переднем плане имело отдельный эффект. Поэтому я решил воссоздать его с помощью Three.js и GLSL.

Что мы строим

Конечный эффект состоит из следующих элементов:

  • Изображение в оттенках серого, занимающее весь экран.
  • Цветное изображение видно только внутри квадрата.
  • Искажение в стиле линзы CC, выступающее наружу из центра квадрата.
  • Сдвиг RGB, который становится сильнее к краям квадрата.
  • Квадратная маска, которая остается квадратной независимо от размера экрана.
  • Плавное взаимодействие с мышью с небольшой задержкой
  • Волновые и случайные искажения, примененные к изображению в оттенках серого
  • Графический интерфейс для настройки параметров в реальном времени.

Хотя результат может выглядеть немного сложным, он не использует постобработку с целью рендеринга или какими-либо 3D-моделями. Вместо этого мы создаем весь эффект внутри фрагментного шейдера, комбинируя приведенные выше элементы.

Структура проекта

Файлы и их роли организованы следующим образом:

src/
└── scripts/
    ├── webgl/
    │   ├── glsl/
    │   │   ├── chunks/
    │   │   │   ├── ccLens.glsl
    │   │   │   ├── coverUv.glsl
    │   │   │   └── random3.glsl
    │   │   ├── frag/
    │   │   │   └── frag.glsl
    │   │   └── vert/
    │   │       └── vert.glsl
    │   ├── mesh/
    │   │   └── Mesh.ts
    │   ├── stage/
    │   │   └── Stage.ts
    │   └── Webgl.ts
    └── index.ts
  • glsl: chunks содержит повторно используемые функции, frag содержит фрагментные шейдеры и vert содержит вершинные шейдеры.
  • mesh (Mesh): Управляет размером окна, загрузкой текстур, созданием сетки, ее размером, однородными обновлениями и соответствующими задачами.
  • stage (Stage): Управляет сценой, ее размером, созданием камеры и средства визуализации, а также их обновлениями.
  • Webgl: Создает и инициализирует Mesh и Stage классы, подключает графический интерфейс, регистрирует события и управляет циклом рендеринга.

Структура классов довольно традиционна, поэтому в этой статье основное внимание уделяется реализации фрагментного шейдера.

Настройка сцены

Начнем с Stage сорт. Этот класс создает сцену, камеру, средство визуализации и другие необходимые элементы для работы с Three.js, а также настраивает обработку рендеринга и изменения размера. По большей части это стандартно, но стоит пояснить одну деталь: как устанавливается положение камеры по оси Z.

Настройка положения камеры по оси Z

Положение камеры по оси Z устанавливается с помощью функции под названием calcViewportDistance.

const calcViewportDistance = (height: number, fov: number): number => {
  return height / (2 * Math.tan((fov * Math.PI) / 360))
}

Не вдаваясь в математические детали, этот расчет определяет расстояние, на котором видимая высота камеры соответствует height. Масштабируя плоскость 1 x 1 по ширине и высоте области просмотра, вся сетка точно вписывается в поле зрения камеры (FOV).

Класс сетки

Далее кратко рассмотрим Mesh сорт. Он обрабатывает все, что связано с сеткой, включая размер окна, загрузку текстур, создание сетки с геометрией и материалом, определение размера сетки, а также управление и обновление унифицированных форм. Как Stage class, его структура достаточно стандартна, поэтому отсюда мы шаг за шагом создадим окончательный внешний вид посредством реализации шейдера.

Вершинный шейдер

Поскольку этот эффект не деформирует никакие вершины, вершинный шейдер прост: он передает UV-координаты геометрии фрагментному шейдеру и преобразует положения вершин плоскости в координаты экранного пространства, которые учитывают масштаб сетки, положение камеры, поле зрения и соответствующие настройки.

precision highp float;

attribute vec3 position;
attribute vec2 uv;

uniform mat4 modelViewMatrix;
uniform mat4 projectionMatrix;

varying vec2 v_uv;

void main() {
  v_uv = uv;

  gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}

Фрагментный шейдер

Отображение текстур в полноэкранном режиме

Теперь перейдем к фрагментному шейдеру. Мы начнем с отображения двух изображений, используемых в этом эффекте, на весь экран. Следующий код в Mesh класс setTexture метод загружает их: texture1 — это цветное изображение, показанное внутри квадрата, а texture2 — это изображение в оттенках серого, отображаемое в области за его пределами.

const loader = new TextureLoader()
const texture1Path = this.$target.dataset.texture1Path
const texture2Path = this.$target.dataset.texture2Path

if (!texture1Path || !texture2Path) return

const [texture1, texture2] = await Promise.all([
  loader.loadAsync(texture1Path),
  loader.loadAsync(texture2Path)
])

this.uniforms.u_texture1.value = texture1
this.uniforms.u_texture2.value = texture2
this.uniforms.u_textureSize1.value.set(
  texture1.image.width,
  texture1.image.height
)
this.uniforms.u_textureSize2.value.set(
  texture2.image.width,
  texture2.image.height
)

Во фрагментном шейдере мы берем цвета из обоих изображений (текстур) и сначала проверяем, что каждое из них заполняет экран. Два изображения, использованные здесь, имеют одинаковое соотношение сторон, поэтому u_textureSize1 и u_textureSize2 можно объединить в одно значение. Однако мы рассчитываем их отдельно, используя размеры каждой текстуры, чтобы поддерживать изображения с разными соотношениями сторон.

precision highp float;

uniform sampler2D u_texture1;
uniform sampler2D u_texture2;
uniform vec2 u_meshSize;
uniform vec2 u_textureSize1;
uniform vec2 u_textureSize2;

varying vec2 v_uv;

#include "../chunks/coverUv.glsl"

void main() {
  vec2 texture1Uv = getCoverUv(v_uv, u_meshSize, u_textureSize1);
  vec2 texture2Uv = getCoverUv(v_uv, u_meshSize, u_textureSize2);

  vec4 insideColor = texture2D(u_texture1, texture1Uv);
  vec4 outsideColor = texture2D(u_texture2, texture2Uv);

  gl_FragColor = insideColor;
}

getCoverUv Используемая здесь функция предотвращает растяжение изображения, если его соотношение сторон отличается от окна просмотра. Вместо использования v_uv напрямую, он создает UV-координаты, которые сохраняют соотношение сторон изображения и обрезают его из центра, как CSS. background-size: cover.

vec2 getCoverUv(vec2 uv, vec2 meshSize, vec2 textureSize) {
  vec2 meshRatio = vec2(meshSize.x / meshSize.y, meshSize.y / meshSize.x);
  vec2 textureRatio = vec2(textureSize.x / textureSize.y, textureSize.y / textureSize.x);

  vec2 resolutionRatio = vec2(
    min(meshRatio.x / textureRatio.x, 1.0),
    min(meshRatio.y / textureRatio.y, 1.0)
  );

  return (uv - 0.5) * resolutionRatio + 0.5;
}

Цветное изображение теперь заполняет экран, как показано ниже.

Вы можете найти код до этого момента в 01-display-color-image.glsl.

Как только цветное изображение станет видимым, замените последнюю строку следующим кодом и убедитесь, что изображение в оттенках серого также отображается.

gl_FragColor = outsideColor;
Изображение в оттенках серого

Вы можете найти код до этого момента в 02-display-grayscale-image.glsl.

Значения цвета и изображения в оттенках серого называются insideColor и outsideColorсоответственно, чтобы облегчить понимание их роли в конечном результате.

Построение системы координат для квадрата

Далее мы создаем маску, которая переключается между полутоновыми и цветными изображениями. Чтобы облегчить работу с центром и размером квадрата, мы преобразуем v_uv к -1.0 to 1.0 диапазон и создайте систему координат с началом координат в центре экрана.

vec2 uvSquare = v_uv * 2.0 - 1.0;

Создание квадратной маски

Мы используем uvSquare координаты, созданные выше для построения маски. Лечение u_squareSize как половину длины одной стороны квадрата, мы определяем его левую, правую, нижнюю и верхнюю границы.

uniform float u_squareSize;

float squareHalfSize = u_squareSize;
float left = -squareHalfSize;
float right = squareHalfSize;
float bottom = -squareHalfSize;
float top = squareHalfSize;

Мы используем step чтобы проверить, находится ли текущий пиксель внутри каждой из четырех границ, затем умножьте четыре результата вместе. Это производит 1.0 внутри квадрата, где все условия соблюдены, и 0.0 везде еще.

float squareMask =
  step(left, uvSquare.x)
  * (1.0 - step(right, uvSquare.x))
  * step(bottom, uvSquare.y)
  * (1.0 - step(top, uvSquare.y));

Чтобы проверить форму маски, выводим squareMask значение непосредственно как цвет.

gl_FragColor = vec4(vec3(squareMask), 1.0);

Внутренняя часть маски выглядит белой, а внешняя — черной, образуя белый прямоугольник в центре.

Вы можете найти код до этого момента в 03-создать-квадратную-маску.glsl.

Исправление соотношения сторон

Теперь у нас есть прямоугольная маска, но ее соотношение сторон меняется при изменении размера области просмотра. Чтобы исправить это, мы используем размеры сетки для расчета поправочного коэффициента соотношения сторон для определения границ квадрата.。

vec2 squareAspectScale = vec2(
  min(u_meshSize.y / u_meshSize.x, 1.0),
  min(u_meshSize.x / u_meshSize.y, 1.0)
);

Мы делим координаты, используемые для квадратного теста, на этот поправочный коэффициент соотношения сторон, регулируя видимую область маски так, чтобы квадрат не растягивался по горизонтали или вертикали.

uvSquare /= squareAspectScale;

Вы можете найти код до этого момента в 04-правильная маска-соотношение сторон.glsl.

Композиция двух изображений

Мы используем squareMask значение для смешивания изображений в оттенках серого и цветных изображений. Где маска 0.0, outsideColor выбран; где это 1.0, insideColor выбран. В результате получается квадратная обрезка цветного изображения, отображаемого поверх изображения в оттенках серого.

vec4 finalColor = mix(outsideColor, insideColor, squareMask);

gl_FragColor = finalColor;

Вы можете найти код до этого момента в 05-композитные-изображения.glsl.

Применение искажения линзы CC

Далее мы применяем искажение в стиле CC Lens к цветному изображению внутри квадрата. Сначала мы конвертируем uvSquare к 0.0 to 1.0 диапазон для создания локальных UV-координат, называемых squareUv. Эти координаты позволяют рассчитать дисторсию линзы относительно центра квадрата, даже при изменении размера квадрата.

vec2 squareUv = uvSquare / (squareHalfSize * 2.0) + 0.5;

Затем мы передаем эти UV-координаты в getCCLensUv функция для создания УФ-координат, искаженных эффектом CC Lens. Функция определяется следующим образом:

uniform float u_lensDistortion;

float getCCLensScale(float distortion, float radius2) {
  if (distortion >= 0.0) {
    return 1.0 + distortion * radius2;
  }

  return 1.0 / (1.0 - distortion * radius2);
}

vec2 getCCLensUv(vec2 uv, vec2 resolution, float distortion) {
  vec2 centeredUv = uv - 0.5;
  vec2 aspectScale = vec2(resolution.x / resolution.y, 1.0);
  vec2 centeredPosition = centeredUv * aspectScale;

  float radius2 = dot(centeredPosition, centeredPosition);
  float lensScale = getCCLensScale(distortion, radius2);

  vec2 distortedPosition = centeredPosition * lensScale;
  vec2 distortedCenteredUv = distortedPosition / aspectScale;
  vec2 distortedUv = distortedCenteredUv + 0.5;
  vec2 distortionOffset = distortedUv - uv;

  return uv - distortionOffset;
}

Функция сначала использует uv - 0.5 переместить центр УФ…



Источник

Оставьте комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Прокрутить вверх