Морфинг геометрических фигур с SDF в фрагментных шейдерах GLSL и визуализацией на Jetpack Compose

от автора

Создание динамичных визуальных эффектов для мобильных приложений требует от разработчиков не только творческого подхода, но и соблюдения требований к производительности. Одной из наиболее эффективных техник для реализации плавных переходов и трансформаций объектов является использование шейдеров, которые позволяют выполнять сложные параллельные вычисления на GPU. Это не только обеспечивает плавность анимаций, но также может снизить нагрузку на CPU, делегируя ресурсоемкие задачи графическому процессору в определенных сценариях, что особенно важно для мобильных устройств с ограниченными ресурсами.

В данной статье будет рассмотрен пример реализации плавной анимации морфинга геометрических фигур с использованием SDF (Signed Distance Functions) и GLSL для графического рендеринга.

Основы SDF

Прежде чем приступать к реализации плавного морфинга между геометрическими фигурами, необходимо сначала разобраться в принципах работы с SDF. SDF (Signed Distance Function) — это математическая модель, которая определяет расстояние от точки до ближайшей поверхности объекта. Каждая фигура, будь то круг, квадрат или многоугольник, может быть описана своей уникальной SDF, что позволяет выполнять такие операции, как пересечения, объединения и разности фигур. В контексте морфинга между двумя фигурами это дает возможность создать непрерывный переход, интегрируя дополнительные эффекты, такие как деформации или сглаживание.

Фигуры с использованием SDF

Для иллюстрации работы SDF, рассмотрим несколько примеров геометрических фигур, которые могут быть использованы:

1. Круг (sdCircle)

Функция sdCircle рассчитывает расстояние от точки до окружности радиусом r. Расстояние вычисляется как разница между длиной вектора, соединяющего точку с центром окружности, и радиусом.

  • length(p) — длина вектора p, который представляет собой координаты точки относительно центра окружности.

  • r — радиус окружности.

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

2. Квадрат (sdSquare)

Функция sdSquare рассчитывает расстояние от точки до ближайшей границы квадрата с длиной стороны 2 * r, где центр квадрата находится в начале координат.

  • abs(p)— функция, которая преобразует координаты в их абсолютные значения, симметрично отображая их в первый квадрант системы координат.

  • max(p.x, p.y)— максимальная величина среди координат x и y в точке, что соответствует самой дальней точке от центра внутри квадрата.

  • r — половина длины стороны квадрата.

3. Ромб (sdDiamond)

Функция sdDiamond рассчитывает расстояние до ромбовидной фигуры с размером r и аспектом aspect, который контролирует степень растяжения ромба по одной из осей.

  • abs(p) — функция, которая преобразует координаты в их абсолютные значения, симметрично отображая их в первый квадрант системы координат.

  • p.x *= aspect — растягивает координату x в зависимости от параметра aspect, что изменяет форму ромба.

  • p.x + p.y - r — сумма координат, откорректированная на величину r для получения границы ромба.

4. Регулярный многоугольник (sdRegularPolygon)

Функция sdRegularPolygon вычисляет расстояние до регулярного многоугольника с n сторонами и радиусом, который определяет расстояние от центра до вершин.

  • float an = 3.141593 / float(n) — угловая величина для каждой стороны многоугольника.

  • vec2 acs = vec2(cos(an), sin(an)) — направляющий вектор для одной стороны многоугольника.

  • atan(p.x, p.y) — вычисление угла для координат точки относительно оси X.

  • mod(atan(p.x, p.y), 2.0 * an) - an — возвращает угол относительно ближайшей стороны многоугольника.

  • p = length(p) * vec2(cos(bn), abs(sin(bn))) — нормализация точки в новую систему координат, ориентированную на сторону многоугольника.

В дополнение к рассмотренным ранее функциям для различных геометрических фигур, стоит отметить, что квадрат можно также рассматривать как регулярный многоугольник с четырьмя сторонами (n = 4). В этом случае мы используем функцию sdRegularPolygon, но необходимо масштабировать координаты для соответствия квадрату.

5. Звезда (sdStar)

Функция sdStar вычисляет расстояние от точки до звезды с 5 лучами. В отличие от других геометрических фигур, таких как круг, квадрат и ромб, звезда имеет более сложную структуру из‑за наличия нескольких лучей, которые нужно точно вычислить.

  • const vec2 k1 = vec2(0.809016994375, -0.587785252292); — определение первого направляющего вектора k1, который соответствует углу 36° (одна из граней звезды).

  • const vec2 k2 = vec2(‑k1.x, k1.y); — второй направляющий вектор k2, который является зеркальным отражением k1 относительно оси Y.

  • p.x = abs(p.x); — приведение координаты x к положительному значению.

  • p ‑= 2.0 * max(dot(k1, p), 0.0) * k1; — коррекция координаты точки относительно первого направляющего луча. Если точка лежит за лучом, она отражается относительно луча. (также для k2)

  • p.y ‑= r; — смещение по оси y на радиус звезды r, чтобы учесть размер самой фигуры.

  • vec2 ba = rf * vec2(‑k1.y, k1.x) — vec2(0, 1); — определение вектора, который будет использоваться для расчетов дальнейших отражений в зависимости от радиуса и коэффициента растяжения rf.

  • float h = clamp(dot(p, ba) / dot(ba, ba), 0.0, r); — коэффициент расстояния для корректировки проекции точки на определенное направление, ограниченное значением радиуса.

  • return length(p — ba * h) * sign(p.y * ba.x — p.x * ba.y); — возвращаем расстояние от точки до звезды, учитывая все преобразования и отражения. Также используется знак для определения стороны, на которой точка находится относительно звезды.

Магические числа, такие как значения векторов k1 и k2, определяют основные углы звезды, обеспечивая корректную ориентацию лучей. Эти числа связаны с углами 36° и 72° которые используются для правильного формирования звезды.

Морфинг фигур

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

Пример функции с преобразованием:

где:

  • getShapeDistance — функция, которая возвращает расстояние до выбранной фигуры (круг, квадрат, и т. д.), в зависимости от переданных индексов фигур.

  • p — нормализованные координаты фрагмента в пространстве, где центр экрана — это точка (0, 0), а диапазоны координат по обеим осям (X и Y) ограничены значениями от -1 до 1.

  • d1 и d2 — значения SDF для двух фигур,

  • morphFactor — коэффициент морфинга. Когда morphFactor равен 0, отображается первая фигура, а при значении 1 — вторая. Плавное изменение этого коэффициента создает эффект преобразования одной фигуры в другую.

Цветовые эффекты в шейдерах

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

В данном фрагменте кода переменная col типа vec3 используется для хранения цвета. Внутри блока if (isColorMode) проверяется, активен ли режим работы с цветом. Если режим включен, выбирается внешний или внутренний цвет в зависимости от значения переменной d (расстояния до поверхности объекта). Если точка находится снаружи объекта (d > 0.0), используется внешний цвет (externalColor), если внутри — внутренний (internalColor).

При корректировки интенсивности цвета применяется выражение col *= 1.05 - exp(-6.0 * abs(d));, что создает эффект затухания цвета. Ближе к поверхности цвет становится ярче, а с увеличением расстояния тускнеет. Также добавляется динамическое изменение оттенков цвета с помощью косинусной функции col *= 0.8 + 0.2 * cos(110.0 * d); что придает эффект пульсации или изменения освещенности.

Для плавного перехода к белому цвету используется функция mix(col, vec3(1.0), 1.0 - smoothstep(0.0, 0.01, abs(d))); Это создает мягкий переход, особенно при малых значениях d, и добавляет эффект освещенности в местах, близких к поверхности объекта. В случае, если режим работы с цветом выключен, используется стандартный белый цвет vec3(1.0) для точек снаружи и черный vec3(0.0) для точек внутри объекта.

Интеграция шейдера на Android с OpenGL ES

После описания всех принципов построения шейдера для его дальнейшей интеграции в Android необходимо настроить рендеринг геометрических фигур с морфингом и цветовыми эффектами. В данном процессе важным шагом является создание среды OpenGL, загрузка шейдеров и передача параметров в эти шейдеры. Все это можно реализовать с использованием компонента GLSurfaceView для рендеринга, который обеспечивает доступ к OpenGL ES на устройствах Android. В качестве примера создадим класс MorphGLSurfaceView унаследованного от GLSurfaceView, где инициализируется рендерер через метод setRenderer. При этом, для обновления значений шейдера создается метод updateShaderValue, который позволяет передавать параметры, такие как значение слайдера морфинга, выбранные фигуры, режим работы с цветом и цвета для объектов:

Для подготовки рендеринга объявим класс MorphRenderer, который будет инициализировать шейдеры, передавать значения и обрабатывать изменения для отображения морфинга. Шейдеры загружаются с помощью функции loadShaderFromRawResource в методе onSurfaceCreated, из ресурсов с последующей компиляцией через loadShader. Также укажем в классе массив vertices, который представляет собой координаты вершин, определяющие геометрию объекта. Данные координаты записаны в виде массива, каждый из которых соответствует одной вершине. Вершины описываются тройками чисел, где каждая тройка представляет собой координаты в пространстве (X, Y, Z). В дальнейшем шейдеры будут использовать эти координаты для преобразования и визуализации объекта.

Когда поверхность изменяется (в методе onSurfaceChanged), необходимо установить область вывода для OpenGL:

Затем, в методе onDrawFrame, происходит отрисовка. Сначала очищается экран с помощью GLES20.glClear, после активируется шейдерная программа. Далее через униформы передаются все необходимые параметры, такие как разрешение, значение слайдера для морфинга, выбор фигуры и цветовые параметры:

Для обновления значений в процессе работы используется такие методы как значение слайдера морфинга, фигуры и цвета: updateSliderValue, updateSelectedShape, updateColorMode и updateColors

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

example morph

example morph

В результате интеграции шейдера с OpenGL ES на Android создается система, в которой морфинг геометрических фигур и динамическое изменение цвета объектов происходит в реальном времени с использованием GPU.

Полный код доступен по ссылке на GitHub.


ссылка на оригинал статьи https://habr.com/ru/articles/872728/


Комментарии

Добавить комментарий

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