В мобильной разработке мы постоянно имеем дело с векторной графикой: ячейки — прямоугольные, аватарки — круглые, текст — это векторные формы. В пиксели это всё превращается как-то само. Но есть ещё и мир растровых эффектов — когда какая-то трансформация происходит с каждым пикселем по отдельности. С таким почти не приходится работать или используются уже готовые решения. Например, тени — типичный растровый эффект. Или блюр, который стал популярен, начиная с iOS 7, — именно тогда он стал одним из стандартных эффектов. В iOS 17 пришло значимое обновление — теперь добавить шейдер можно к любой View, а значит, вся интеграция стала проще и растровых эффектов появится больше. Давайте разбираться, что за чудо-код надо написать, как это подключить и как в целом погрузиться в тему. Начнём с простых градиентов, а закончим сложным примером «как в Air Drop на iOS 17». Статья будет из двух частей: сначала разберёмся, что за шейдеры и где применять, а потом посмотрим на пару сложных примеров. Процессор выполняет команды по очереди: процесс может быть сложным, в несколько потоков, управлять разными частями компьютера, но он всё равно остаётся достаточно линейным. Именно такой код обычно мы и пишем. Тут хорошо подойдёт иллюстрация из игры Human Resource Machine: У видеокарты иначе: много маленьких процессоров (несколько тысяч), каждый из них выполняет небольшую программу — шейдер. Шейдер применяется к каждому пикселю одновременно. Раз работу они выполняют параллельно, то можно добиться невероятной производительности и обрабатывать все пиксели на экране 60-120 раз в секунду. Это сотни миллионов операций в секунду для всего экрана. Сравните прошлое видео и новое из игры Billion Humans (тот же разработчик): Обычно шейдеры бывают двух видов: текстурные и вершинные. Текстурные управляют пикселями в картинке: меняют цвет или положение пикселя. Вершинные нужны для трёхмерной графики: 3d-модели состоят из небольших треугольников с вершинами в трёхмерных координатах. Можно написать программу-шейдер, которая будет как-то влиять на эти вершины. А ещё шейдеры могут комбинироваться между собой. На этих комбинациях построены буквально все современные игры. Если вы встречали код шейдеров, то, скорее всего, он был непонятный: много странных операций, необычный нейминг и т.д. Это нормально: все вычисления происходят с матрицами, поверх необычного фреймворка (Open GL или Metal). Но для всего нужна практика, и простые вещи разберём в этой статье. В SwiftUI появились новые модификаторы, которые позволяют легко вызвать код для шейдера и применить его к вьюшке. Нам нужно лишь указать metal-функцию, вычисление которой применится к каждому пикселю. Есть три эффекта: colorEffect — функция для расчёта цвета каждого пикселя внутри View. На вход передаётся координата пикселя и его цвет; distortionEffect — функция для расчёта координаты каждого пикселя. На вход передаётся только текущая координата; layerEffect — функия для расчёта цвета пикселя, но с доступом к цвету соседних пикселей по их координате. Полезно, например, для блюра, когда нужно рассчитать средний цвет между соседними пикселями. Применяется очень просто: Теперь посмотрим примеры из iOS. Обычно, если вы не понимаете, как кто-то смог создать необычный эффект, то это шейдер :D colorEffect Apple Pay: смешивая влияние разных категорий покупок, можем получить итоговый аккуратный градиент. Лупа: в самой простой реализации для увеличения картинки шейдер даже не нужен, но если вы хотите сделать искажения по краям стекла, то без шейдеров не обойтись. Siri: не уверен, но это очень похоже на гиперболический парабалоид. Можно рассчитать его положение в трёхмерном пространстве, а потом спроецировать на экран — получим мягкие градиенты и анимацию. distortionEffect Passbook shredder: слегка загнуть кусочки проще всего тоже через шейдер. layerEffect blur и vibrance: пример для layerEffect, когда мы считаем не только значение текущего пикселя по какой-то формуле, но и опираемся на значения соседних пикселей, чтобы усреднить значение. iBooks: Перелистывание страничек можно сделать через шейдер: по координате касания считать насколько должны изогнуться пиксели у страницы. Это layerEffect, потому что для текущего пикселя надо не только взять значение какого-то другого, но еще и тень добавить.  Рисование. Перед Новым годом мы сделали небольшой интерактив — замораживали интерфейс и давали на нём немного порисовать. Мы рассказывали о том, как сделать это через рисование кривыми Безье, но то же самое можно повторить через шейдер, будет работать очень быстро. Дизеринг позволяет готовить любую цветную картинку к печати на термо-принтере. Анимации эффектов. Например, можно нарисовать HDR-звёзды и анимировать их движение текстурой с шумом и так же развлечься в тёмной теме, когда пиццерия закрыта. Генерируемые индикаторы загрузки, чтобы скрасить ожидание человека. Псевдо 3D-пиццы. Например, брать фотографию пиццы, применять к ней карту нормалей, а по положению гироскопа как бы задавать положение источника света. Так в играх все текустуры делают, чтобы получить более реалистичное изображение. Модифицировать картинки. Например, мы хотим вращать картинку пиццы, но у нас огромный каталог картинок с тенями. Довольно просто написать шейдер, который отделит тень от пиццы. Итак, шейдер — это функция, которая вызывается для каждого пикселя на экране. Код для шейдера нужно писать в специальном .metal-файле. В файле нам нужно писать на C++ примерно такие функции: Необычными выглядят типы float2 и half4. Это вектор, т.е. упорядоченный набор чисел, в конце указано, сколько чисел в нём содержится. Интересно, что к таким типам обращаются не через квадратные скобки, как к массиву, а через переменные, в смысловом порядке: x, y, z или r, g, b, a — выбор названия переменной влияет только на читаемость кода, а фактически мы обращаемся к значению по его порядковому номеру. Вызвать шейдер в SwiftUI достаточно просто с помощью модификатора .colorEffect: Название шейдера в SwifUI не работает с автоподстановкой, вы должны указать его правильно сами, интеграция происходит через Dynamic Member Lookup. SwiftUI-шейдеры нельзя протестировать через брейкпойнт, потому что он вызывается миллионы раз в секунду. Но можно открыть оба файла в split-view и открыть в SwiftUI Preview: после изменения .metal-файла нажмите ⌘+S — превью скомпилируется и обновится, даже фокус не нужно ставить в SwiftUI-код. Все математические операции выполняются немного непривычно. Если вы добавляете к вектору значений единицу, то она добавится к каждому значению в векторе. Если вы один вектор прибавляете к другому, то значения сложатся по номерам. Например, (1, 2, 3) + (4, 5, 6) = (1+4, 2+5, 3+6) = (5, 7, 9). Тоже самое со всеми остальными операциями: вычитанием, деление, вычислением синуса и т.д. В metal очень много уникальных стандартных функций. Полный список можно посмотреть в Metal Shading Language Specification, а в наших примерах будет встречаться несколько типовых: clamp(x, 0, 100) — ограничит значение x, чтобы оно было в диапазоне, например 0 до 100. Эквивалент для max(0, min(100, x)) ; saturate(x) — более короткий вызов для clamp(t, 0, 1). Чаще всего используется для цветов, ведь именно они должны попадать в диапазон от 0 до 1; length(xy) — считает гипотенузу, если представить, что координаты это катеты. Удобно для расчетов удаленности относительно центра координат; sin(t) и cos(t) часто используются для создания цикличных плавных размытий. В качестве параметра мы будем передавать время. Программировать шейдеры начнём с простых примеров — нарисуем градиент через весь спектр цветов. Нам нужно вернуть 4 значения для RGB-цвета, но удобнее задавать градиент в пространстве HSB, так мы сможем менять только один параметр — hue. Формулу для конвертации можно подсмотреть в википедии, а функция saturate просто ограничивает диапазон каждого значения от 0 до 1, чтобы оно оставалось в корректном диапазоне. На самом деле, цвета могут быть и больше 1, но это уже в сторону HDR. Значения из переменной HSB извлекаются через свойства x, y и z, но на самом деле их стоит читать как x=hue, y=saturation, z=brightness. Затем мы конвертируем цвет из HSB в RGB и укажем, что alpha должна быть равна одному. Код для шейдера будет простой: в качестве hue возьмём горизонтальную координату пикселя, но в процентном соотношении к ширине элемента, а ширину передадим в качестве параметра функции. Такое приведение значение диапазона к интервалу от 0 до 1 часто называют нормализацией. В качестве компонент saturation и brightness просто поставим 1, а в конце все сконвертируем в rgb. Получится такой результат — по горизонтали меняется hue: Осталось лишь вызвать этот шейдер из SwiftUI. В прошлых примерах на вход функции подавались только стандартные параметры, нам их даже не видно из swift-кода, но можно передавать и дополнительные, например, я добавил ширину элемента. В SwiftUI такой параметр надо прокинуть явно: указать его тип и значение. Обратите внимание, что конвертация типов происходит через функцию .float(), а стандартные position и color передавать не нужно. Анимируем градиент. Для этого передадим в функцию ещё и время. В SwiftUI нужно завернуть элементы в TimelineView, тогда обновление экрана будет происходить каждый кадр: Шейдер похож на предыдущий, но с несколькими отличиями. Сначала посмотрите на полный код, а затем разберём. Время, которое мы передали, будет постоянно увеличиваться. Если мы хотим зациклить анимацию, то нам нужна круговая функция. Синус или косинус для этого хорошо подходят: на вход мы передаём радианы, функция пересчитывает их в значение от -1 до 1. Разница только в том, что вернётся, если передать на вход 0: синус начнёт возвращать значения с 0, а косинус — с единицы. При этом нам надо сдвинуть диапазон [-1, 1] до диапазона [0, 1]. Для этого мы добавим 1 (диапазон сместится в [0, 2]) и разделим на 2 (иногда умножают 0.5, это чуть быстрее), чтобы получился [0, 1]. Вычисления могут показаться сложными, но их легко проводить, думая о краевых значениях по отдельности. Для анимации градиента мы начинаем его сдвигать на получившееся время. Но нам нужно, чтобы значения были в пределах от 0 до 1, поэтому используем функцию модуля abs (от английского absolute), чтобы отбросить знак у числа: Готово: градиент будет перемещаться назад и вперёд. В качестве упражнения можно подумать, какой математикой можно заставить его двигаться только в одну сторону. На этом первая статья заканчивается. Мы посмотрели что нового принесла iOS 17, какие базовые операции есть и чуть-чуть попрограммировали. Этого ещё мало, чтобы начать писать свои эффекты с нуля, но уже достаточно, чтобы понять, как работают шейдеры в статье от hackingwithswift.com. В следующей статье разберём, как сделать эффект CD-диска. Код уже можно посмотреть на гитхабе, но мы пройдёмся по нему шаг за шагом, чтобы лучше понимать, как создавать подобные эффекты. Подписывайтесь на канал DodoMobile в Телеграме. Там же вакансии — мы собираемся устроить большой редизайн приложения, присоединяйтесь, если очень любите заниматься интерфейсами. Бонус — моя бесплатная книга «Про доступность iOS» Примеры Искажения по краям стекла hackingwithswift.com Эффект СД-диска Документация Metal Shading Language Specification Displaying HDR Content in a Metal Layer Википедия Дизеринг Карта нормалей Вектор HSB Другие статьи как мы изморозь на стекле сделали Про Swift Dynamic Member Lookup   Head of Mobile at Dodo Engineering