Шейдеры отлично подходят для симуляции материалов. В обычных интерфейсах мы управляем только цветом, но эффекты могут быть революционными. Например, блюр в iOS изменил многие мобильные интерфейсы и стал частью интерфейса Apple Vision Pro. Описать материал через шейдер сложно, потому что код совершенно другой, но создать динамичный эффект — ещё сложнее. В этой статье разберём пару примеров: переливающийся диск и нашумевший NameDrop в iOS 17. Золото — типичный материал, который невозможно передать одним цветом, это всегда будет градиент из желто-белого в черный. Посмотрите на платья: они отличаются не цветом, а материалом — свет под разными углами создаёт разный цвет. CD-диск тоже переливается разными цветами в зависимости от положения света и наблюдателя. Давайте разберём, как повторить такой эффект на примере кода от Daniel Kuntz. Если вы совсем новичок в теме, то начните с первой статьи, где мы учимся рисовать градиент. Для создания эффекта не обязательно погружаться в физику процесса, достаточно визуальной имитации. Вот общий план: сделаем мягкую полоску, проходящую через центр диска; наложим пару десятков таких полосок разного цвета друг на друга. В местах наложения цвет будет доходить до яркого белого, а в остальных местах будет оставаться цвет полоски; привяжем положение полосок к гироскопу, чтобы диск переливался в зависимости от наклона телефона. Первые два шага видно на скриншотах. Обратите внимание на то, как зеленый цвет левой полосы видно на правой картинке. Для отрисовки одной полоски пройдём четыре шага: подвинем координаты из угла View в центр видимой области; в качестве яркости используем значение косинуса и покажем только одну волну, которая проходит через центр диска; добавим цвет полоски к общей яркости (полоска — это streak); сконвертируем все из HSV в RGB. Алгоритм в виде шейдера: Теперь разберём каждый шаг по частям. Сначала надо сдвинуть координаты в центр View. Для этого мы переводим координаты в относительные значения, чтобы они менялись от 0 до 1 — просто разделим текущую координату на размер View. И координата, и размер — это вектор из двух значений, поэтому обычная операция деления применяется к каждой координате отдельно: координата X разделится на ширину,  координата Y разделится на высоту.  Текстурные координаты сохраняются в переменную UV — это типичное обозначение текстурных координат: X, Y и Z применяются для трёхмерных координат вершины, а координаты U и V для двухмерных координат текстуры, которая натягивается поверх трехмерной модели. 3D в нашем примере нет, а вот обозначение UV мы можем использовать. Затем мы сдвигаем координаты в центр, вычитая из каждой по 0.5, потому что изначально координаты начинаются в левом верхнем углу. После сдвига координата (0, 0) окажется в центре, а значение координат будут меняться от -0.5 до +0.5. Генерацию полоски разберём подробней и разделим на два шага: сгенерируем волну и отрежем от неё середину. Для генерации волны возьмём Y-координату, умножим её на ширину полосы и возьмём косинус от получившегося значения. Косинус нужен, потому что его нулевое значение даёт единицу, а значит, что яркость будет максимальная в центре окружности. Width будет регулировать шаг косинуса, тем самым влияя на ширину полосы. Затем мы ограничиваем максимальное значение яркости, ведь наша полоска будет накладываться на другие полосы, а значит, нужно дать пространство для увеличения яркости поверх базового значения одной полосы. В конце собираем итоговый цвет из входящего параметра color и посчитанного нами Brightness. Получится вот такой повторяющий паттерн. Мы применили его к Y-координате, поэтому он повторяется вертикально. Отрежем центральную часть, потому что все три полоски нам не нужны. Буквально будем смотреть, что координата выходит за диапазон от -33% до 33%, но с привязкой к передаваемой ширине полосы. В итоге умножаем цвет полосок на mask, т.е. определяем, будет у нас какой-то цвет на выходе или нет. После маскирования останется одна мягкая полоса. Сузим полосу к центру. Для этого мы можем рассчитать расстояние между координатами U и V с помощью функции length (как гипотенузу по теореме Пифагора). Получится, что чем ближе значения к центру, тем они меньше (потому что мы сдвинули координаты и значения в центре равны нулю) и тем менее яркий получится цвет. А чем ближе к краю, тем ярче. Самое яркое значение будет на центральной линии по краям при X = 0 и Y = 0.5, где длина тоже будет равна 0.5 (корень из 0^2 + 0.5^2). Всегда опирайтесь на краевые значения, чтобы лучше понимать, что вы хотите от кода. В итоге мы получаем мягкую полоску (она мягкая по высоте из-за косинуса) и более яркую к краям (из-за расчёта расстояния между координатами). Для полного эффекта переливания нужно привязать угол полосы к гироскопу и наложить сразу несколько полос. Для начала научим нашу полосу реагировать на вращение в зависимости от входного угла. Для этого используем матрицу поворота и применим её к координатам: Общий код для расчёта одной мягкой вращающейся полоски: Основная работа позади: мы посчитали всё, что нужно, — осталось добавить несколько полос и наложить их друг на друга. Код может показать сложным, но на самом деле всё просто: мы берём 25 полосок: for (float i = 0; i < 25; i++) { задаём им разную ширину: float width = clamp((sin(i+1)+1) * 100, 10., 180.); // [10, 180] задаём произвольный предварительный цвет (streakColor): float hue = sin(i*8.11)+1.0; // 220 is max radian, hue in [0, 2] задаём угол, под которым должна быть наша полоска. Угол будет зависеть от внешнего значения — accel: float angle = (9*i+sin(accel*i*3)); считаем саму полосу (это подробно разобрали раньше): float3 streakHSV = streak(uv, angle, streakColor, width, 1 - minBrightness); добавляем цвет от этой полосы к текущему пикселю. Важно правильно понять порядок действий: к каждому пикселю по-отдельности мы считаем насколько он попадает в эти 25 полос и какой вклад каждой полосы в этот конкретный пиксель: float o = saturate(1 - (width / 180.0)) * 0.9; // [0, 0.9] resultColor += HSVtoRGB(streakHSV) * o; Все это посыпано разными коэффициентами, но разбираться в каждом отдельно довольно долго — мы и не будем. В качестве параметра accel можно взять значения от гироскопа телефона или от координаты касания и при каждом изменении пересчитывать положение полосок для каждого пикселя. Получится анимированный интерактивный круговой градиент. Проверить работу можно прямо в SwiftUI Canvas. Репозиторий с примером Если код выше вам показался понятным, то можете самостоятельно попробовать разобраться в эффекте NameDrop. Несколько комментариев, которые могут помочь: эффект состоит из двух частей: заблюривания и взрыва, каждый ровно по 1 секунде; время изменяется от 0.01 до 2 секунд с шагом в 0.01; это шейдер, который умеет брать цвета соседних пикселей. Для этого используется функция layer.sample: мы рассчитываем координаты для смещения, берём значения другого пикселя и выводим как значение текущего. Так появляется ощущение волны; функция mix линейно смешивает две координаты с добавлением коэффициента.  функция smoothStep позволяет изменять значение в диапазоне не линейно, а не плавно; Подробнее про функции можно почитать в спецификации по Metal.   Если вы и с этим примером разобрались, то можете попробовать повторить эффект шторки в ванной или переворот книги в iBooks. Если получится — покажите код, а то я пока только разборы писать научился :D SwiftUI и iOS 17 — это хорошо, но как быть обычным разработчикам, которые поддерживают UIKit и более старые версии iOS? Всё просто: нужно сделать интеграцию через MTKView, он давно с нами. Единственное, что сделали в iOS 17 — упростили вызов и срезали часть возможностей, чтобы не париться с 3D и применять это все к 2D-интерфейсу. Сама тема тянет на отдельную статью, но начать вы можете тут: документация Creating a Custom Metal View; статья How to get started with Metal APIs— with UIView and SwiftUI . Репозиторий с кодом из статьи Коллекция шейдеров Лайвкодинг про шейдеры Транзишен через шейдер Переворот карточки в шахматном порядке Редактор шейдеров ShaderMania Лупа Небольшие эффекты Шум (нужен для создания других эффектов) Шторка Большая подборка про Metal Ага. Как создать анимированные шейдеры в Jetpack Compose Подписывайтесь на канал Dodo Mobile — скоро будем разбираться с многопоточностью, одновременным доступом и зачем нужны акторы в Swift.  Head of Mobile at Dodo Engineering