Привет! На связи Юрий Петров, 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. Но до выхода последнего релиза, чтобы создать свой шейдер, необходимо было сделать несколько манипуляций.

  1. Написать свой шейдер на языке glsl и поместить его в проект.

  2. Скомпилировать шейдер внешним компиллятором в файл SPIR‑V.

  3. Загрузить файл SPIR‑V во Flutter.

  4. Скомпилировать во Flutter SPIR‑V файл.

  5. Создать шейдер из ранее скомпилированного файла SPIR‑V.

  6. Передать этот шейдер в CustomPaint.

И это была боль для разработчика! Не всегда получается правильно скомпилировать файл glsl или spriv. Это крайне неудобная и трудозатратная процедура.

С выходом Flutter 3,7 многое поменялось. Был очень упрощено API для работы с шейдерами, и самое главное, был добавлен внутренний компилятор файлов glsl.

Теперь для создания своего шейдера необходимо сделать всего три шага:

  1. Написать свой шейдер на языке glsl и поместить его в проект.

  2. Создать из фала glsl шейдер (встроенным во Flutter компилятором).

  3. Передать это шейдер в 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);
}

Разберем код построчно:

  1. Создаем входную переменную iTime. Модификатор uniform означает, что она не будет меняться при выполнении кода. Float— тип переменной. Эта переменная будет зависеть от входного временного параметра, который мы будем отправлять в шейдер из фреймворка Flutter.

  2. Создаем входную переменную iResolution. Это параметр типа vec2. Это означает, что вектор состоит из двух цифр. Сюда мы будем передавать размер контейнера для правильного расчета якорной точки.

  3. Создаем выходную переменную fragColor. Эта переменная будет отправляться из шейдера в фреймворк Flutter. Тип этой переменной vec4 — вектор из четырех цифр. Данный вектор будет описывать цвет в RGB формате + альфа канал. Почему название переменных именно такие? Просто так принято при написании шейдеров.

  4. Пропуск.

  5. Точка входа в шейдер.

  6. Создаем переменную sp типа вектора из двух чисел, которая описывает начальную, якорную точку. Здесь для расчета мы используем объект gl_FragCoord (входит в пакет glsl)и iResolution (принимаем из Flutter) для расчета частного.

  7. Создаем переменную color типа вектора из трех чисел, которая описывает RGB цвет. Здесь мы берем формулу для расчета косинуса угла, и самое важное мы передаем в расчет входную переменную iTime. Соответственно с каждой итерацией изменения переменной iTimeмы будем производить перерасчет цветов.

  8. Присваиваем переменной 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.

Комментарии (4)


  1. afanasiev_dan
    30.01.2023 14:43
    +1

    Очень полезно, спасибо, как раз искал как реализовать анимированный bg)


  1. Freeamn1s1
    31.01.2023 08:19
    +1

    Мега круто, жду вторую часть!