Привет! На связи Юрий Петров, Flutter Team Lead в Friflex. Мы разрабатываем кроссплатформенные мобильные приложения для бизнеса и специализируемся на Flutter. В этой серии статей я поделюсь опытом, как с помощью шейдеров на фреймворке разрабатывать приложения с привлекательным и стильным визуалом, которые понравятся заказчику и клиентам.
Общее понятие о шейдерах
Flutter – это мощный фреймворк для создания мобильных приложений, который позволяет разработчикам создавать красивые и уникальные интерфейсы. Одним из важных инструментов, которые предоставляет Flutter, являются шейдеры. Шейдеры — это программы, которые запускаются на GPU (Графическом процессоре устройства) и используются для отрисовки графики на экране. Шейдеры написаны на языке GLSL (OpenGL Shading Language) и выполняются непосредственно на GPU. Они позволяют реализовать продвинутые визуальные эффекты такие как тени, градиенты и сложные текстуры.
Во Flutter уже встроены несколько шейдеров. Они используются в классах LinearGradient, RadialGradient, SweepGradient, ImageShader и т. д. Данные шейдеры мы можем извлечь из этих объектов и использовать их в таких классах, как ShaderMask или CustomPaint. CustomPaint может использоваться в сочетании с шейдерами, чтобы создать различные визуальные эффекты. Например, мы можем создать CustomPainter, который использует шейдер SweepGradient для создания градиента на фоне, и использовать CustomPaint для нанесения этого градиента на канвас. Также мы можем использовать CustomPaint для создания различных эффектов на тексте таких, как тени или изменение цвета. Эти шейдеры создаются с конкретными параметрами и могут использоваться для достижения широкого спектра визуальных эффектов. Умелое использование шейдеров во Flutter может придать вашим приложениям привлекательный и стильный визуал, который будет выделять их среди остальных. Вы сможете создавать уникальные градиенты, теневые эффекты, реалистичные анимации и многое другое.
Шейдеры — мощный инструмент для разработчиков, который позволяет создавать яркие и запоминающиеся приложения с помощью Flutter.
Использование встроенных шейдеров
Чтобы лучше разобраться в работе шейдеров, для примера, попробуем создать и отрисовать красивый градиент с помощью класса CustomPaint. Вы уже должны быть знакомы с Flutter. Я не буду детально описывать стандартные методы для работы.
Ниже приведен код, который нарисует на весь экран SweepGradient.
Код рисующий на весь экран SweepGradient
import 'package:flutter/material.dart'; void main() { runApp(MaterialApp( home: Scaffold( body: Center( child: CustomPaint(painter: _MySweepPainter()), )), )); } class _MySweepPainter extends CustomPainter { _MySweepPainter(); @override void paint(Canvas canvas, Size size) { const rect = Rect.largest; const gradient = SweepGradient( colors: [Colors.red, Colors.orange, Colors.green], ); final paint = Paint()..shader = gradient.createShader(rect); canvas.drawRect(rect, paint); } @override bool shouldRepaint(CustomPainter oldDelegate) => true; }
Результат
Реализация шейдера класса SweepGradient
В данном коде нет ничего необычного. Мы создаем виджет CustomPaint, в него передаём объект _MySweepPainter. Здесь хочу обратить ваше внимание на часть кода:
final paint = Paint()..shader = gradient.createShader(rect); canvas.drawRect(rect, paint);
В данной части кода мы получаем шейдер из ранее созданного градиента с помощью метода createShader(). И рисуем его на канвасе. Таким образом, вы можете быстро извлекать уже существующие шейдеры. Но что если вы хотите сделать анимированный шейдер или написать свой?
Создание собственных шейдеров
Шейдеры — это программы, которые работают очень быстро и параллельно. Для создания собственных шейдеров, разработчик должен иметь необходимые знания языка GLSL и понимать основные концепции шейдинга, включая входные и выходные переменные, векторы, матрицы, текстуры, цвета и так далее. Не переживайте, на самом деле все не так страшно.
На момент написания этой статьи последней версией Flutter является 3,7 и Dart SDK 2,19. Но до выхода последнего релиза, чтобы создать свой шейдер, необходимо было сделать несколько манипуляций.
-
Написать свой шейдер на языке glsl и поместить его в проект.
-
Скомпилировать шейдер внешним компиллятором в файл SPIR‑V.
-
Загрузить файл SPIR‑V во Flutter.
-
Скомпилировать во Flutter SPIR‑V файл.
-
Создать шейдер из ранее скомпилированного файла SPIR‑V.
-
Передать этот шейдер в CustomPaint.
И это была боль для разработчика! Не всегда получается правильно скомпилировать файл glsl или spriv. Это крайне неудобная и трудозатратная процедура.
С выходом Flutter 3,7 многое поменялось. Был очень упрощено API для работы с шейдерами, и самое главное, был добавлен внутренний компилятор файлов glsl.
Теперь для создания своего шейдера необходимо сделать всего три шага:
-
Написать свой шейдер на языке glsl и поместить его в проект.
-
Создать из фала glsl шейдер (встроенным во Flutter компилятором).
-
Передать это шейдер в CustomPaint
Как написать свой шейдер на языке GLSL
Для написания шейдера вам необходимо создать файл в проекте с расширением.glsl.
Вот пример простого шейдера:
shader.glsl
uniform float iTime; uniform vec2 iResolution; out vec4 fragColor; void main() { vec2 sp = gl_FragCoord.xy / iResolution; vec3 color = cos(iTime + sp.xyx + vec3(0, 1, 5)); fragColor = vec4(color, 1); }
Разберем код построчно:
-
Создаем входную переменную iTime. Модификатор uniform означает, что она не будет меняться при выполнении кода. Float— тип переменной. Эта переменная будет зависеть от входного временного параметра, который мы будем отправлять в шейдер из фреймворка Flutter.
-
Создаем входную переменную iResolution. Это параметр типа vec2. Это означает, что вектор состоит из двух цифр. Сюда мы будем передавать размер контейнера для правильного расчета якорной точки.
-
Создаем выходную переменную fragColor. Эта переменная будет отправляться из шейдера в фреймворк Flutter. Тип этой переменной vec4 — вектор из четырех цифр. Данный вектор будет описывать цвет в RGB формате + альфа канал. Почему название переменных именно такие? Просто так принято при написании шейдеров.
-
Пропуск.
-
Точка входа в шейдер.
-
Создаем переменную sp типа вектора из двух чисел, которая описывает начальную, якорную точку. Здесь для расчета мы используем объект gl_FragCoord (входит в пакет glsl)и iResolution (принимаем из Flutter) для расчета частного.
-
Создаем переменную color типа вектора из трех чисел, которая описывает RGB цвет. Здесь мы берем формулу для расчета косинуса угла, и самое важное мы передаем в расчет входную переменную iTime. Соответственно с каждой итерацией изменения переменной iTimeмы будем производить перерасчет цветов.
-
Присваиваем переменной fragColor типа вектора из четырех чисел, которая описывает RGB цвет+ альфа канал.
На этом все, шейдер готов! Но я бы вам рекомендовал обязательно попробовать изменить параметры расчетов и посмотреть, как меняется расчет шейдеров. Это будет полезно для понимания, как работают математические функции в GLSL.
Инициализация шейдера во Flutter
Убедились, что используем Flutter 3,7 и Dart SDK 2,19 или выше.
environment: sdk: '>=2.19.0 <3.0.0'
Указываем путь к вашему шейдеру.
... flutter: shaders: - shader.glsl ...
Теперь необходимо немного изменить наш класс _MySweepPainter таким образом, чтобы он мог принимать шейдер в конструктор как параметр. И удалим получение шейдера из градиента.
main.dart
class _MySweepPainter extends CustomPainter { _MySweepPainter(this.shader); final Shader shader; @override void paint(Canvas canvas, Size size) { const Rect rect = Rect.largest; final Paint paint = Paint()..shader = shader; canvas.drawRect(rect, paint); } @override bool shouldRepaint(CustomPainter oldDelegate) => true; }
Теперь реализуем весь наш код на Flutter.
main.dart
void main() { runApp(const MyApp()); } class MyApp extends StatefulWidget { const MyApp({Key? key}) : super(key: key); @override State<MyApp> createState() => _MyAppState(); } class _MyAppState extends State<MyApp> with TickerProviderStateMixin { var updateTime = 0.0; @override void initState() { super.initState(); createTicker((elapsed) { updateTime = elapsed.inMilliseconds / 1000; setState(() {}); }).start(); } @override Widget build(BuildContext context) { return FutureBuilder<FragmentProgram>( future: _initShader(), builder: (context, snapshot) { if (snapshot.hasData) { final shader = snapshot.data!.fragmentShader() ..setFloat(0, updateTime) ..setFloat(1, 300) ..setFloat(2, 300); return CustomPaint(painter: _MySweepPainter(shader)); } else { return const Center( child: CircularProgressIndicator(), ); } }, ); } Future<FragmentProgram> _initShader() { return FragmentProgram.fromAsset("shader.glsl"); } }
Разберем этот код построчно и остановимся на важных моментах.
17. Создаем переменную updateTime, которая изначально равна 0. Эта переменная в дальнейшем будет передаваться в шейдер (как параметр iTime).
22. Создаем специальный таймер, который будет обновлять переменную updateTime на каждый цикл итерации и присваивать ей значения частного elapsed.inMilliseconds / 1000.
30. Создаем FutureBuilder, так как создание шейдера — это асинхронная операция.
49. Через специальный класс FragmentProgram создаем шейдер из файла shader.glsl.
34. После успешного получения Future<FragmentProgram> мы инициализируем шейдер с помощью метода fragmentShader() и передаем в шейдер переменную updateTime и два числа, которые в шейдере будут сопоставляться с входной переменной iResolution.
38. И, наконец, передаем готовый шейдер в класс _MySweepPainter.
В итоге получаем такой результат.
Работа шейдера
Хотите поделиться своим опытом работы с шейдерами? Жду ваши вопросы в комментариях!
В следующей части мы разберем, как импортировать готовые шейдеры с сайта https://glslsandbox.com/ и как управлять шейдерами из Flutter.
ссылка на оригинал статьи https://habr.com/ru/company/friflex/blog/713298/
Добавить комментарий