Как создать shader в Flutter для эффектной анимации в приложении?

Hola, Amigos! Меня зовут Сергей Климович, я Mobile Team Lead агентства заказной разработки Amiga. В мире мобильной разработки Flutter выделяется своей гибкостью и простотой в создании красивых пользовательских интерфейсов. Однако, чтобы добавить дополнительные визуальные эффекты и повысить уровень графической привлекательности приложения, иногда необходимо выходить за рамки стандартных возможностей. И здесь на сцену выходят шейдеры.

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

861d259e71d18e43fef1d0fefbf742fb.gif

Во Flutter уже встроены несколько шейдеров, которые используются в классах LinearGradient, RadialGradient, SweepGradient, ImageShader и т.д. Данные шейдеры мы можем извлечь из этих объектов и использовать их в таких классах, как ShaderMask или CustomPaint. Но что если нужно создать уникальные градиенты, теневые эффекты или реалистичные анимации.

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

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

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

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

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

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

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

Чтобы создать шейдер, нам нужно написать код на GLSL. GLSL — это язык шейдеров высокого уровня, который используется для программирования графического процессора (GPU). Давайте пропустим часть кодирования GLSL и выберем уже закодированный GLSL и изменим его, чтобы он работал с нашим приложением Flutter. 

Чтобы было более предметно и красиво для примера я выберу использование эффекта ниже в шиммере:

51ea6f6922bb2e93aa84cdc0257fefe1.gif

Создание шейдера 

  1. Создадим файл shader.frag и скопируем код: https://www.shadertoy.com/view/fd33zn 

  2. Затем добавим в начало:

#include   // импорт среды выполнения Flutter
uniform vec2 uSize; // универсальная переменная, в которой хранится размер визуализируемого объекта
uniform float iTime; // универсальная переменная, в которой хранится время, прошедшее с момента запуска шейдера
vec2 iResolution; // переменная, в которой хранится разрешение экрана

out vec4 fragColor; // выходная переменная, в которой хранится окончательный цвет визуализируемого объекта
  1. Переименуем mainImage в main и заменим параметры на void.

  2. Добавим две переменные внутри:

iResolution = uSize;
   	vec2  fragCoord  = FlutterFragCoord();

Интеграция во Flutter

Чтобы не писать много кода для шиммера я просто форкну flutter_shimmer. Создадим отдельный компонент, который будет принимать дочерний элемент и шейдер. Затем мы наложим полученный шейдер поверх него.

class ShimmerFromShader extends StatefulWidget {
  final Widget child;
  final FragmentShader shader;

Далее создаем SingleChildRenderObjectWidget для получения объекта рендеринга, принадлежащий дочернему виджету.

Сам рендер будет происходить в _ShimmerFilter, где мы создаем ShaderMaskLayer и на него будем накладывать наш шейдер. В настройках таймера можно поиграть со скоростью вращения.

class _ShimmerFilter extends RenderProxyBox {
 FragmentShader shader;
 double _time;
 _ShimmerFilter(this.shader, this._time);


 @override
 ShaderMaskLayer? get layer => super.layer as ShaderMaskLayer?;


 @override
 bool get alwaysNeedsCompositing => child != null;


 set time(double newValue) {
   if (newValue == _time) {
     return;
   }
   _time = newValue;
   markNeedsPaint();
 }


 @override
 void paint(PaintingContext context, Offset offset) {
   if (child != null) {
     assert(needsCompositing);


     layer ??= ShaderMaskLayer();
     shader.setFloat(0, size.width);
     shader.setFloat(1, size.height);
     shader.setFloat(2, _time);
     layer!
       ..shader = shader
       ..maskRect = Offset.zero & size
       ..blendMode = BlendMode.srcIn;
     context.pushLayer(layer!, super.paint, offset);
   } else {
     layer = null;
   }
 }

Далее сам пример

Добавьте шейдер в Flutter pubspec.yaml:

flutter:
 shaders:
   - shaders/shader.frag

Создаем объект шейдера:

Future loadMyShader() async {
   final program = await FragmentProgram.fromAsset('shaders/shader.frag');
   shader = program.fragmentShader();
   return shader!;
 }

И далее передаем в созданный ранее виджет, используя в качестве placeholderов виджеты из форкнутого пакета:

ShimmerFromShader.fromShader(
                 shader: snapshot.data!,
                 child: const SingleChildScrollView(
                   physics: NeverScrollableScrollPhysics(),
                   child: Column(
                     crossAxisAlignment: CrossAxisAlignment.start,
                     mainAxisSize: MainAxisSize.max,
                     children: [
                       BannerPlaceholder(),
                       TitlePlaceholder(width: double.infinity),
                       SizedBox(height: 16.0),
                       ContentPlaceholder(
                         lineType: ContentLineType.threeLines,
                       ),
                       SizedBox(height: 16.0),
                       TitlePlaceholder(width: 200.0),
                       SizedBox(height: 16.0),
                       ContentPlaceholder(
                         lineType: ContentLineType.twoLines,
                       ),
                       SizedBox(height: 16.0),
                       TitlePlaceholder(width: 200.0),
                       SizedBox(height: 16.0),
                       ContentPlaceholder(
                         lineType: ContentLineType.twoLines,
                       ),
                     ],
                   ),
                 ));

Полный код можно посмотреть тут. 

Заключение

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

Хорошего всем кода!

© Habrahabr.ru