Connect with us

Разработка

Кастомные параметры и анимация с использованием шейдеров Metal

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

Опубликовано

/

     
     

Шейдеры Metal в SwiftUI позволяют нам выйти за рамки традиционного рендеринга на основе представлений и работать непосредственно на пиксельном уровне. Вместо описания того, как должно выглядеть представление, мы можем описать, как должен вести себя каждый отдельный пиксель.

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

Более подробную информацию об этом можно найти в предыдущей статье о шейдерах Metal.

Но настоящий творческий контроль начинается, когда мы выходим за рамки автоматической обработки и начинаем передавать собственные данные в шейдер.

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

Пользовательские параметры: шахматная доска

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

Но шейдеры становятся гораздо мощнее, когда мы начинаем передавать в них собственные данные.

SwiftUI упрощает это, позволяя передавать пользовательские параметры с помощью специальных методов-оберток: .float() для чисел и .color() для цветов. При передаче типа SwiftUI Color с помощью метода .color() он автоматически преобразуется в значение RGBA в формате half4, которое может использовать ваш шейдер. Крайне важно помнить о порядке параметров: ваши пользовательские параметры должны следовать за автоматическими и точно соответствовать порядку в сигнатуре функции шейдера.

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

Кастомные параметры и анимация с использованием шейдеров Metal

А теперь давайте посмотрим, как мог бы выглядеть код для этого:

#include <metal_stdlib>
#include <SwiftUI/SwiftUI.h>
using namespace metal;

[[ stitchable ]] half4 chessboard(float2 position, half4 currentColor, float tileSize, half4 fillColor) {
    uint2 gridCoords = uint2(position.x / tileSize, position.y / tileSize);
    
    bool shouldFill = (gridCoords.x ^ gridCoords.y) & 1;
    
    return shouldFill ? fillColor * currentColor.a : half4(0.0, 0.0, 0.0, 0.0);
}

Обратите внимание на два новых параметра после currentColor:

  • float tileSize: размер каждого квадрата шахматной доски в пикселях
  • half4 fillColor: цвет, используемый для закрашенных квадратов

Математика создания шахматной доски

Ключ к созданию шахматной доски — определить, к какой «ячейке» принадлежит каждый пиксель, а затем определить, следует ли эту ячейку окрашивать или нет.

Сначала мы делим позицию пикселя на размер квадрата, чтобы определить, в какой ячейке мы находимся:

uint2 gridCoords = uint2(position.x / tileSize, position.y / tileSize);

Например, при tileSize=10 пиксель в позиции (15, 25) принадлежит ячейке (1, 2), а пиксель в позиции (5, 5) принадлежит ячейке (0, 0).

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

bool shouldFill = (gridCoords.x ^ gridCoords.y) & 1;

Оператор ^ (исключительное ИЛИ) в сочетании с & 1 создает именно такой чередующийся шаблон. Вам не нужно глубоко разбираться в побитовой математике, просто знайте, что он дает нам true для ячеек, которые должны быть окрашены, и false для ячеек, которые должны быть прозрачными.

В конце концов, мы возвращаем соответствующий цвет:

return shouldFill ? fillColor * currentColor.a : half4(0.0, 0.0, 0.0, 0.0);

Если ячейка должна быть окрашена, мы возвращаем fillColor (с учетом любой прозрачности). В противном случае мы возвращаем полностью прозрачный цвет.

Использование в SwiftUI

Вот здесь начинается отличие от предыдущих шейдеров, поскольку нам необходимо передавать эти пользовательские параметры:

import SwiftUI
import Metal

struct ChessboardView: View {
    var body: some View {
        Rectangle()
            .colorEffect(ShaderLibrary.chessboard(.float(30), .color(.yellow)))
            .frame(width: 300, height: 300)

    }
}

Обратите внимание на параметры внутри ShaderLibrary.chessboard():

  • .float(30): это передает значение 10.0 параметру float tileSize
  • .color(.yellow): это преобразует SwiftUI-цветColor.yellow в half4 и передает его параметру fillColor

Обратите внимание, что, как и в шейдере, порядок имеет значение и в реализации SwiftUI.

В шейдере наши параметры были: float2 position, half4 currentColor, float tileSize, half4 fillColor, но поскольку первые два обрабатываются автоматически в SwiftUI, нам нужно передать только параметры после автоматически добавляемых — в том же порядке — и присвоить им значения.

Анимации: шейдеры, основанные на времени

Самая интересная часть шейдеров — это создание анимаций. Чтобы анимировать шейдер, нам нужно передать ему постоянно изменяющееся значение, обычно время.

Компонент TimelineView из SwiftUI идеально подходит для этой цели: он обновляется по расписанию (например, каждый кадр), предоставляя нам доступ к текущему времени. Мы можем вычислить время, прошедшее с момента появления представления, передать его в наш шейдер в качестве параметра .float(), и шейдер будет использовать это значение для вычисления цветов, которые меняются со временем.

Например, когда пользователь взаимодействует с элементами управления, такими как ползунки, мы можем использовать переменные @State для передачи этих значений в наш шейдер таким же образом, заставляя шейдер реагировать на ввод пользователя в реальном времени.

Но пока давайте создадим шейдер, в котором цвета плавно меняются с помощью синусоидальных волн, без внешнего воздействия со стороны пользователя. Анимация будет происходить сама собой во времени.

Давайте рассмотрим реализацию:

#include <metal_stdlib>
#include <SwiftUI/SwiftUI.h>
using namespace metal;

[[ stitchable ]] half4 animatedColor(float2 position, half4 currentColor, float time) {
    half r = half((sin(time) + 1.0) * 0.5);
    half g = half((sin(time + 2.0) + 1.0) * 0.5);
    half b = half((sin(time + 4.0) + 1.0) * 0.5);
    
    return half4(r, g, b, 1.0);
}

Понимание синусоидальных волн

Синусоидальная функция (sin()) идеально подходит для анимации, поскольку она плавно колеблется по волнообразному образцу. Она естественным образом повышается и понижается со временем, создавая плавное, непрерывное движение.

Синусоидальные функции обычно выдают значения от -1 до 1, в то время как цветам требуются значения от 0 до 1. Поэтому необходимы корректировки:

half r = half((sin(time) + 1.0) * 0.5);

Формула (sin(время) + 1.0) * 0.5 преобразует диапазон от -1 до 1 в диапазон от 0 до 1. С течением времени это создает плавный пульсирующий эффект, при котором значение красного цвета плавно циклически меняется от тусклого к яркому и обратно.

Фазовый сдвиг

Обратите внимание, что каждый цветовой компонент использует различное смещение:

  • Красный: sin(time)
  • Зеленый: sin(time + 2.0)
    Синий: sin(time + 4.0)

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

Использование в SwiftUI с TimelineView

Для анимации шейдеров нам необходимо постоянно обновлять параметр времени. TimelineView в SwiftUI идеально подходит для этого:

import SwiftUI
import Metal

struct AnimatedColorShaderView: View {
    @State private var startTime = Date()
    
    var body: some View {
        TimelineView(.animation) { timeline in
            Rectangle()
                .colorEffect(
                    ShaderLibrary.animatedColor(
                .float(timeline.date.timeIntervalSince(startTime))
                    )
                )
                .frame(width: 300, height: 300)
        }
    }
}

Давайте разберем каждый элемент:

  • @State private var startTime = Date()
    Этот метод сохраняет время первого появления представления, что позволяет рассчитать прошедшее время.
  • TimelineView(.animation)
    Этот метод создает временную шкалу, которая обновляется каждый кадр (обычно 60 раз в секунду). Использование .animation обеспечивает непрерывные обновления для плавной анимации.
  • timeline.date.timeIntervalSince(startTime)
    Этот метод вычисляет, сколько секунд прошло с момента появления представления. Это значение постоянно увеличивается, что заставляет наши синусоидальные волны колебаться.

Цикл анимации работает следующим образом: TimelineView запускает обновление (60 раз в секунду), вычисляется новое прошедшее время, оно передается в шейдер с помощью .float(...), шейдер пересчитывает все цвета, используя новое значение времени, SwiftUI перерисовывает представление с новыми цветами, и затем весь цикл начинается заново.

В результате получается цветовая анимация, плавно циклически меняющая цветовой спектр.

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

На этом этапе у нас есть все необходимые строительные блоки: вычисления для каждого пикселя, пользовательские данные, передаваемые из SwiftUI на GPU, математическое управление цветом и движением, покадровая анимация с использованием TimelineView.

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

Шейдеры Metal требуют изменения мышления, от композиции представлений к композиции математических выкладок, но этот сдвиг открывает необычайные выразительные возможности. Как только вы освоите передачу данных и анимацию во времени, GPU перестанет быть «чёрным ящиком» и станет творческой площадкой.

А дальше всё решает экспериментирование.

Источник

Если вы нашли опечатку - выделите ее и нажмите Ctrl + Enter! Для связи с нами вы можете использовать info@apptractor.ru.
Telegram

Популярное

Сообщить об опечатке

Текст, который будет отправлен нашим редакторам: