3.7. CustomPainter: продвинутые концепции

Обрезка холста

У объекта Canvas есть методы для обрезки контента: clipRect, clipPath, clipRRect.

Например, это необходимо, чтобы ограничить зону отрисовки в пределах переданного размера. Допустим, нам нужно покрасить фон холста в определённый цвет. Для этого воспользуемся методом drawColor — и вот какой результат получится:

1@override
2void paint(Canvas canvas, Size size) {
3	canvas.drawColor(Colors.red, BlendMode.src);
4}

CustomPaint растянут на весь экран и обёрнут в SafeArea, но можно заметить что system bar (панель в верху экрана) тоже окрасился в красный. Обрежем зону отрисовки по размеру холста с помощью метода clipRect.

1@override
2void paint(Canvas canvas, Size size) {
3	// Координата левого верхнего угла холста
4  const leftTopCorner = Offset(0, 0);
5  // Координата правого нижнего угла холста
6  final rightBottomCorner = Offset(size.width, size.height);
7
8  // Прямоугольник за пределами которого контент холста будет обрезан
9  final rect = Rect.fromPoints(leftTopCorner, rightBottomCorner);
10
11  canvas.clipRect(rect);
12  canvas.drawColor(Colors.red, BlendMode.src);
13}

Как видите, зона отрисовки обрезалась по размеру холста.

Group

Group

Давайте сейчас чуть остановимся и посмотрим, что происходит «под капотом»:

  1. Сначала вызывается команда canvas.clipRect — что означает, что каждая следующая после него операция отрисовки будет обрезаться по переданному ограничению.
  2. Затем вызывается команда canvas.drawColor, которая окрашивает весь холст в один цвет.

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

Вот ещё пример. Мы используем операцию clipRRect, для обрезки накладываемых цветов в форме прямоугольника с закруглёнными углами.

1  @override
2  void paint(Canvas canvas, Size size) {
3    // Координата левого верхнего угла холста
4    const leftTopCorner = Offset(0, 0);
5    // Координата правого нижнего угла холста
6    final rightBottomCorner = Offset(size.width, size.height);
7
8    // Прямоугольник за пределами которого контент холста будет обрезан
9    final rect = Rect.fromPoints(leftTopCorner, rightBottomCorner);
10    final rrect = RRect.fromRectAndRadius(rect, const Radius.circular(150));
11
12    // Обрезает хост в форме прямоугольника с закруглёнными углами
13    canvas.clipRRect(rrect);
14
15    // На холсте рисуется синий, затем обрезается операцией [clipRRect]
16    canvas.drawColor(Colors.blue, BlendMode.srcATop);
17    // На холсте рисуется зелёный, затем обрезается операцией [clipRRect]
18    canvas.drawColor(Colors.green, BlendMode.srcATop);
19    // На холсте рисуется красный, затем обрезается операцией [clipRRect]
20    canvas.drawColor(Colors.red, BlendMode.srcATop);
21  }

Чтобы оптимизировать процесс отрисовки, воспользуемся слоями.

Слои

Если вы работали в фоторедакторе наподобие Photoshop, то имеете представление о слоях. Если нет, то вот простое объяснение:

Слой — это группа операций поверх холста. Соответственно, слои можно накладывать друг на друга или смешивать друг с другом.

Вот как можно оптимизировать обрезку холста с помощью слоёв — основная идея заключается в том, что мы откладываем применение обрезания и сначала мержим все операции рисования между собой в отдельном слое:

1@override
2void paint(Canvas canvas, Size size) {
3  // Координата левого верхнего угла холста
4  const leftTopCorner = Offset(0, 0);
5  // Координата правого нижнего угла холста
6  final rightBottomCorner = Offset(size.width, size.height);
7
8  // Прямоугольник за пределами которого контент холста будет обрезан
9  final rect = Rect.fromPoints(leftTopCorner, rightBottomCorner);
10  final rrect = RRect.fromRectAndRadius(rect, const Radius.circular(150));
11  // Сохраняем изначальное состояния холста до применения обрезания
12  canvas.save();
13  canvas.clipRRect(rrect);
14  // Сохраняем операцию обрезки
15  canvas.saveLayer(null, Paint());
16  // * Здесь операции будут обрабатываться только между собой
17
18  canvas.drawColor(Colors.blue, BlendMode.srcATop);
19  canvas.drawColor(Colors.green, BlendMode.srcATop);
20  canvas.drawColor(Colors.red, BlendMode.srcATop);
21
22  // Применяет обрезание
23  canvas.restore();
24  // * Здесь каждая операция будет обрезаться отдельно
25  // Востанавливает состояние холста накладывая слой с обрезанием сверху
26  canvas.restore();
27  // * Здесь не будет работать обрезание
28}

В приведённом коде видно, что помимо вызова clipRRect есть вызовы методов save, saveLayerи restore. Разберёмся с каждым из них подробнее.

save

Метод сохраняет состояние холста на момент вызова. Чтобы проще осознать это, можно представить, что вызов save создаёт скоуп, в котором можно применять различные модификации холста — обрезание, разворот и так далее. После вызова метода restore скоуп закрывается, и далее все трансформации не будут применяться к последующим операциям.

saveLayer

Метод сохраняет состояние холста на момент вызова в отдельный слой. Аналогично вызову save можно представить, что создаётся скоуп. Основное отличие в том, что данный скоуп — это новый чистый слой, к которому не будут применяться ранее использованные трансформации холста.

restore

Вызов метода restore объединяет новый слой из скоупа с ранее сохранённым состоянием холста — а значит, что также применяет все ранее использованные трансформации к новому слою.

С методами разобрались. Теперь, когда нам известно как работать со слоями, давайте рассмотрим ещё один механизм который на них основан — это трансформация холста.

Трансформация

Иногда может потребоваться изменить весть холст с уже нарисованными содержимым — например, повернуть готовую фигуру или немного увеличить её. У Canvas есть следующие методы для трансформации холста:

  • translate — сдвигает координатную сетку на переданные значения по осям X и Y.

  • rotate — поворачивает холст на переданный угол в радианах.

  • scale — умножает координаты холста на переданное значение.

    Например, прямоугольник размером 100х200, верхний левый угол которого находится в координатах (100; 150), после вызова scale(2) примет размеры 200х400 и будет находиться в координатах (200; 300).

  • skew — трансформирует холст, наклоняя его в 2D-пространстве.

  • transform — применяет матрицу трансформации к холсту.

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

Относительное смещение

Сделав какую-либо трансформацию, вы вероятно столкнётесь с ситуацией, что результат будет отличаться от того, что вы ожидали: фигура, которую вы хотели изменить, может переместиться совсем в другое место или вовсе исчезнуть с экрана.

Почему так происходит отлично иллюстрирует изображение ниже: все трансформации происходят левого верхнего края холста.

flt

Чтобы трансформация объекта выполнилась корректно, необходимо выполнять трансформации относительно его центра. Для этого необходимо с помощью метода translate сместить холст на координаты центра объекта и после использования методов трансформации вернуть холст в исходное положение, вызвав метод translate с отрицательными координатами.

1// Сдвигаем координатную плоскость к центру
2canvas.translate(center.dx, center.dy);
3// Поворачиваем холст
4canvas.rotate(math.pi / 2);
5// Сдвигаем координатную плоскость в исходное состояние
6canvas.translate(-center.dx, -center.dy);

Использование слоёв

Трансформации холста рекомендуется совершать в отдельном слое. Так трансформация не повлияет на последующие операции

1// Сохраняем слой
2canvas.save();
3
4// Трансформация холста
5canvas.translate(center.dx, center.dy);
6canvas.rotate(math.pi / 2);
7canvas.translate(-center.dx, -center.dy);
8
9final trianglePath = Path()
10      ..moveTo(center.dx, center.dy - radius)
11      ..lineTo(center.dx + radius, center.dy)
12      ..lineTo(center.dx - radius, center.dy)
13      ..close();
14
15// Рисуем синий треугольник
16canvas.drawPath(
17	trianglePath,
18	painter..color = Colors.blue.withOpacity(0.5),
19);
20
21// Востанавливаем слой
22canvas.restore();
23
24// Рисуем красный треугольник
25canvas.drawPath(
26	trianglePath,
27	painter..color = Colors.red.withOpacity(0.5),
28);
29

Group

Как можно заметить, мы рисуем оба треугольника в одних и тех же координатах, но в случае синего мы изменяем саму систему координат.

Трансформация на практике

Для наглядной иллюстрации использования трансформации приведём следующий пример — компас, полностью нарисованный с помощью CustomPainter. У нас есть два основных компонента: статичная подложка и изменяющая своё направление стрелка.

Ещё один аспект напрямую связанный со слоями — BlendMode. Это инструмент, который поможет контролировать, как разные слои накладываются друг на друга, и какие цвета получаются в итоге.

BlendMode

Строго говоря, BlendMode — это алгоритмы, для смешивания пикселей. Выбор конкретного алгоритма зависит от выбранного типа класса BlendMode. Это мы рассмотрим чуть позже, пока поговорим о том, как именно алгоритмы преобразуют цвета.

Каждый алгоритм имеет два входных параметра:

  • source — рисуемое изображение;
  • destination — исходное изображение, на которое компонуется source.

destination часто рассматривается как фон. Как источник, так и пункт назначения имеют четыре цветовых канала: красный, зеленый, синий и альфа-каналы.

Обычно они представлены в виде чисел в диапазоне от 0,0 до 1,0. Выходные данные алгоритма также имеют те же четыре канала со значениями, вычисленными из источника и пункта назначения.

Цель различных алгоритмов BlendMode — получить на вход 4 канала destination и 4 канала source и преобразовать их в 4 канала пикселя.

Для простоты представим задачу смешения пикселей абстрактным методом:

1Color? resolveColor(Color? source, Color? destination);

Тогда стратегию BlendMode.srcOver, — дефолтное значение объекта Paint, обычное наложение картинки на фон, — можно представить так:

1Color? resolveColor(Color? source, Color? destination) {
2	return destination ?? source;
3}

То есть если пиксель накладываемого изображения не пустой — возвращает его, иначе — возвращает пиксель фона.

Все стратегии BlendMode можно посмотреть на странице официальной документации, давайте перейдём к примеру.

BlendMode на практике

Рассмотрим пример использования BlendMode. Тут нам снова помогут слои:

1class BlendModePainter extends CustomPainter {
2  const BlendModePainter();
3
4  @override
5  void paint(Canvas canvas, Size size) {
6    // Координата Y середины холста
7    final verticalCenter = size.height / 2;
8    // Координата X середины холста
9    final horizontalCenter = size.width / 2;
10    // Середина холста
11    final center = Offset(horizontalCenter, verticalCenter);
12    // Радиус окружности
13    final radius = size.shortestSide / 6;
14    // Стратегия наложения слоёв
15    const blendMode = BlendMode.plus;
16
17    // Сохраняем исходный фон / Выделяем отрисовку кругов в отдельный слой
18    canvas.saveLayer(null, Paint());
19
20    // Рисуем красный круг
21    canvas.drawCircle(
22      center.translate(0, -radius),
23      radius,
24      // Т.к. слой новый, первый круг необязательно рисовать
25      // c BlendMode.exclusion
26      Paint()..color = const Color(0xFFFF0000),
27    );
28
29    // Рисуем зелёный круг
30    canvas.drawCircle(
31      center.translate(-radius / 2, 0),
32      radius,
33      Paint()
34        ..color = const Color(0xFF00FF00)
35        ..blendMode = blendMode,
36    );
37
38		// Выделяем слой для отрисовки синего круга
39    canvas.saveLayer(null, Paint()..blendMode = blendMode);
40    // Рисуем синий круг
41    canvas.drawCircle(
42      center.translate(radius / 2, 0),
43      radius,
44      Paint()..color = const Color(0xFF0000FF),
45    );
46    // Объединяем слой синего круга с остальными
47    canvas.restore();
48
49    // Объединяем круги с исходным фоном
50    canvas.restore();
51  }
52
53  @override
54  bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
55}

Group

В этом примере используется стратегия смешивания BlendMode.plus. Как можно понять из названия — этот алгоритм суммирует значения каналов, поэтому при наложении друг на друга кругов они образуют такую картину.

Также мы хотели бы акцентировать внимание читателя на то, как рисуется каждый из кругов в данном примере.

  • Красный круг рисуется без изменения BlendMode , так как это первое изображение на новом слое, и ему просто не с чем смешиваться.
  • Зелёный круг рисуется с использованием объекта Paint, у которого параметр blendMode равен BlendMode.plus.
  • Синий круг, рисуется в отдельном слое. Здесь стоит обратить внимание, что при вызове метода saveLayer объекту Paint также задаётся значение BlendMode.plus. То есть помимо синего круга в этом слое можно нарисовать что-нибудь ещё, и при вызове метода restore весь слой будет объединяться с предыдущим с использованием стратегии BlendMode.plus.

C BlendMode закончили. Далее в этом параграфе нам осталось только рассмотреть шейдеры — инструмент, с помощью которого можно получать новые эффекты, которых по умолчанию нет во Flutter.

Shaders

Шейдеры — это программы, написанные на особом языке (GLSL или HLSL).

Они определяют алгоритмы обработки изображений для разных стадий 3D-графики и используются видеокартами для расчёта окончательного изображения, которое видит пользователь.

Класс Paint так же позволяет рисовать шейдеры. Для этого нужно передать в параметр shader наследника класс Shader.

Встроенные шейдеры

Во Flutter уже реализован некоторый набор шейдеров: LinearGradient, RadialGradient, SweepGradient, ImageShader.

Стоит уточнить, что классы LinearGradient, RadialGradient, SweepGradient наследуемые от Gradient (package:flutter/src/painting/gradient.dart) — это не шейдеры. Чтобы создать непосредственно шейдер, необходимо вызвать метод createShader, который создаёт объект Gradient из библиотеки dart:ui — и вот уже он **является наследником класса Shader.

Вот небольшой пример — текст, раскрашенный с использованием шейдера линейного градиента.

1 GradientShaderPainter extends CustomPainter {
2  const GradientShaderPainter();
3
4  @override
5  void paint(Canvas canvas, Size size) {
6    // Координата Y середины холста
7    final verticalCenter = size.height / 2;
8    // Смещение
9    final offset = Offset(0, verticalCenter);
10
11    // Шейдер линейного градиента
12    final shader = const LinearGradient(
13      colors: [Colors.red, Colors.blue],
14    ).createShader(Rect.fromLTWH(0, 0, size.width, size.height));
15		
16		// Выделяем слой для отрисовки текста
17    canvas.saveLayer(null, Paint());
18
19    TextPainter(
20      text: const TextSpan(
21        text: 'Текст написан с использованием шейдеров',
22        style: TextStyle(color: Colors.black),
23      ),
24      textDirection: TextDirection.ltr,
25      textAlign: TextAlign.center,
26    )
27      // Расчёт размеров, который занимает текст
28      ..layout(minWidth: size.width, maxWidth: size.width)
29      // Отрисовка текста на холсте
30      ..paint(canvas, offset);
31
32		// Выделяем слой для градиента
33    canvas.saveLayer(null, Paint()..blendMode = BlendMode.srcATop);
34		// Рисуем на холсте Paint с градиентом
35    canvas.drawPaint(Paint()..shader = shader);
36		// Объединяем градиент с текстом
37    canvas.restore();
38		
39		// Объединяем текст с исходным фоном
40    canvas.restore();
41  }
42
43  @override
44  bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
45}

Group

BlendMode.srcATop — накладывает изображение (source) только на пересечение с фоном (destination). В данном случае source — это градиент, а destination — текст.

Собственные шейдеры

Во Flutter есть возможность загрузить свои собственные шейдеры, для этого необходимо выполнить следующие шаги:

  1. Поместить в проект файл шейдера в формате glsl (напомним, GLSL — язык для программирования шейдеров). Файл имеет расширение .frag.
  2. Добавить по аналогии с другими ассетами в pubspec.yaml секцию shaders и указать путь до файла шейдера.
  3. Создать объект FragmentProgram через именованный конструктор fromAsset, передав путь к файлу шейдера.
  4. Вызывать метод fragmentShader, у ранее созданного объекта FragmentProgram.

Давайте же приступим. Для начала создаём файл custom_shader.frag и добавляем его в pubspec.yaml в секцию shaders. Содержимое файла покажем чуть ниже.

1flutter:
2  shaders:
3    - assets/shaders/custom_shader.frag

Загрузка шейдера — асинхронный процесс, поэтому необходимо предварительно загрузить шейдер перед передачей его в CustomPainter.

1Future<FragmentShader> _loadShader(String shaderAssetPath) async {
2  final program = await FragmentProgram.fromAsset(shaderAssetPath);
3
4  return program.fragmentShader();
5}

Теперь можно создать CustomPainter и передать туда шейдер.

1class ShaderPainter extends CustomPainter {
2  final FragmentShader shader;
3
4  ShaderPainter({required this.shader});
5
6  @override
7  void paint(Canvas canvas, Size size) {
8    final paint = Paint()..shader = shader;
9    canvas.drawRect(
10      Rect.fromLTWH(0, 0, size.width, size.height),
11      paint,
12    );
13  }
14
15  @override
16  bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
17}

На стороне Flutter — всё готово чтобы отрисовывать шейдер, но пока наш файл шейдера пуст. Исправим это! Для начала просто просто нарисуем синий фон.

1// Версия API
2#version 460 core
3
4// Вспомогательный инструментарий для Flutter
5#include <flutter/runtime_effect.glsl>
6
7// Точность вычислений GPU
8precision mediump float;
9
10// Итоговый цвет пикселя
11out vec4 fragColor;
12
13// Константа - синий цвет
14const vec3 blue = vec3(0, 0, 255) / 255;
15
16void main() {
17  fragColor = vec4(blue, 1);
18}

Group

Метод main выполняется для каждого пикселя, рассчитывая финальный цвет пикселя (fragColor). Переделаем наш шейдер так чтобы получать цвет в качестве входной переменной. Для этого необходимо объявить переменную с ключевым словом uniform — оно означает, что переменная будет передана снаружи.

1// Версия API
2#version 460 core
3
4// Вспомогательный инстурментарий для Flutter
5#include <flutter/runtime_effect.glsl>
6
7// Точность вычислений GPU
8precision mediump float;
9
10// Итоговый цвет пикселя
11out vec4 fragColor;
12
13// Переменные, передаваемые в шейдер
14uniform vec3 color; // Цвет
15
16void main() {
17  fragColor = vec4(color/255, 1);
18}

Напомним, что каждый канал пикселя — это значение от 0 до 254. Но в синтаксисе GLSL мы оперируем векторами, и значения вектора цвета должно быть в пределах от 0 до 1. Поэтому делим вектор color на 255.

Доработаем CustomPainter и передадим цвет в шейдер.

1...
2  static const color = Colors.red;
3
4  @override
5  void paint(Canvas canvas, Size size) {
6    shader
7      ..setFloat(0, color.red.toDouble())
8      ..setFloat(1, color.green.toDouble())
9      ..setFloat(2, color.blue.toDouble());
10
11    final paint = Paint()..shader = shader;
12    canvas.drawRect(
13      Rect.fromLTWH(0, 0, size.width, size.height),
14      paint,
15    );
16  }
17...

Group

API шейдера очень низкоуровневое. Мы должны передать каждый параметр отдельно — вызвать метод setFloat и передать индекс аргумента и его значение.

В данном примере есть только одна входная переменная с типом vec3. vec3 — это просто последовательность из трёх переменных, поэтому мы и передаём значения, указывая индекс в диапазоне от 0 до 2. Представим что есть ещё переменная с типом vec2 , тогда чтобы передать параметры в неё, необходимо вызвать метод setFloat ещё два раза с индексами 3 и 4.

Сейчас каждый пиксель краситься в один и тот же цвет. Добавим в шейдер логику, которая будет менять яркость цвета в зависимости от Y-координаты пикселя.

1// Версия API
2#version 460 core
3
4// Вспомогательный инстурментарий для Flutter
5#include <flutter/runtime_effect.glsl>
6
7// Точность вычислений GPU
8precision mediump float;
9
10// Итоговый цвет пикселя
11out vec4 fragColor;
12
13// Переменные, передаваемые в шейдер
14uniform vec3 color; // Цвет
15uniform vec2 resolution; // Разрешение экрана
16
17void main() {
18  // Коордианты пикселя
19  vec2 pixelCoord = FlutterFragCoord();
20
21  // Позиция пикселя относительно разрешения экрана
22  vec2 positionOnScreen = pixelCoord / resolution;
23   
24  fragColor = vec4((color/255) * positionOnScreen.y, 1);
25}

Теперь передадим размеры экрана в CustomPainter и посмотрим что будет выведено на экране.

1...
2	@override
3  void paint(Canvas canvas, Size size) {
4    shader
5      ..setFloat(0, color.red.toDouble())
6      ..setFloat(1, color.green.toDouble())
7      ..setFloat(2, color.blue.toDouble())
8      ..setFloat(3, size.width)
9      ..setFloat(4, size.height);
10
11    final paint = Paint()..shader = shader;
12    canvas.drawRect(
13      Rect.fromLTWH(0, 0, size.width, size.height),
14      paint,
15    );
16  }
17...

Group

Таким образом мы получили шейдер, который динамически рассчитывает яркость цвета в зависимости от Y-координаты, т.е. простейший градиент.

Конечно, вряд ли вы будете писать более сложные шейдеры сами. Для поиска подходящего шейдера можно воспользоваться ресурсом GLSL SANDBOX — это приложение WebGL для live-кодирования шейдеров, которая также содержит большую библиотеку шейдеров, созданных пользователями ресурса.

Но есть одна проблема: скорее всего, без некоторой адаптации эти виджеты не будут компилироваться во Flutter-приложении. Это связано с особенностями реализации поддержки шейдеров во Flutter.

Поэтому давайте возьмём какой-нибудь шейдер и попробуем его адаптировать, например вот этот. Скопируем код в файл и добавим его в проект.

1#ifdef GL_ES
2precision mediump float;
3#endif
4
5uniform float time;
6uniform vec2 mouse;
7uniform vec2 resolution;
8
9const int MAXITER = 30;
10
11vec3 field(vec3 p) {
12	p *= .1;
13	float f = .1;
14	for (int i = 0; i < 3; i++) {
15		p = p.yzx; //*mat3(.8,.6,0,-.6,.8,0,0,0,1);
16//		p += vec3(.123,.456,.789)*float(i);
17		p = abs(fract(p)-.5);
18		p *= 2.0;
19		f *= 2.0;
20	}
21	p *= p;
22	return sqrt(p+p.yzx)/f-.05;
23}
24
25void main( void ) {
26	vec3 dir = normalize(vec3((gl_FragCoord.xy-resolution*.5)/resolution.x,1.));
27	float a = time * 0.1;
28	vec3 pos = vec3(0.0,time*0.1,0.0);
29	dir *= mat3(1,0,0,0,cos(a),-sin(a),0,sin(a),cos(a));
30	dir *= mat3(cos(a),0,-sin(a),0,1,0,sin(a),0,cos(a));
31	vec3 color = vec3(0);
32	for (int i = 0; i < MAXITER; i++) {
33		vec3 f2 = field(pos);
34		float f = min(min(f2.x,f2.y),f2.z);
35		
36		pos += dir*f;
37		color += float(MAXITER-i)/(f2+.01);
38	}
39	vec3 color3 = vec3(1.-1./(1.+color*(.09/float(MAXITER*MAXITER))));
40	color3 *= color3;
41	gl_FragColor = vec4(vec3(color3.r+color3.g+color3.b),1.);
42}

Попытавшись запустить проект мы получим ошибку ещё во время компиляции. Давайте попробуем поправить код, ориентируясь на пример, который мы сделали ранее.

Во-первых, меняем конфигурационные строки:

1~~#ifdef GL_ES
2precision mediump float;
3#endif~~
4
5// Версия API
6#version 460 core
7
8// Вспомогательный инстурментарий для Flutter
9#include <flutter/runtime_effect.glsl>
10
11// Точность вычислений GPU
12precision mediump float;
13
14...

Всё равно получаем ошибку компиляции. В коде находим gl_FragColor и gl_FragCoord, нам нужно заменить их на fragColor и FlutterFragCoord() используемые ранее.

1// Версия API
2#version 460 core
3
4// Вспомогательный инстурментарий для Flutter
5#include <flutter/runtime_effect.glsl>
6
7// Точность вычислений GPU
8precision mediump float;
9
10// Итоговый цвет пикселя
11out vec4 fragColor;
12
13uniform float time;
14uniform vec2 mouse;
15uniform vec2 resolution;
16
17const int MAXITER = 30;
18
19vec3 field(vec3 p) {
20	p *= .1;
21	float f = .1;
22	for (int i = 0; i < 3; i++) {
23		p = p.yzx; //*mat3(.8,.6,0,-.6,.8,0,0,0,1);
24//		p += vec3(.123,.456,.789)*float(i);
25		p = abs(fract(p)-.5);
26		p *= 2.0;
27		f *= 2.0;
28	}
29	p *= p;
30	return sqrt(p+p.yzx)/f-.05;
31}
32
33void main() {
34	vec3 dir = normalize(vec3((FlutterFragCoord().xy-resolution*.5)/resolution.x,1.));
35	float a = time * 0.1;
36	vec3 pos = vec3(0.0,time*0.1,0.0);
37	dir *= mat3(1,0,0,0,cos(a),-sin(a),0,sin(a),cos(a));
38	dir *= mat3(cos(a),0,-sin(a),0,1,0,sin(a),0,cos(a));
39	vec3 color = vec3(0);
40	for (int i = 0; i < MAXITER; i++) {
41		vec3 f2 = field(pos);
42		float f = min(min(f2.x,f2.y),f2.z);
43		
44		pos += dir*f;
45		color += float(MAXITER-i)/(f2+.01);
46	}
47	vec3 color3 = vec3(1.-1./(1.+color*(.09/float(MAXITER*MAXITER))));
48	color3 *= color3;
49	fragColor = vec4(vec3(color3.r+color3.g+color3.b),1.);
50}

Запускаем ещё раз. Ошибка компиляции пропала, но на экране ничего не отобразилось, также в логах ошибки о несовпадении количества аргументов шейдера с переданными нами. Если посмотреть в код шейдера, то мы видим что ожидается передача 3 аргументов:

  • float time — время, можно воспринимать этот параметр как прогресс анимации.
  • vec2 mouse — текущая позиция курсора мыши, от этого параметра мы избавимся, нам он не нужен.
  • vec2 resolution — разрешение экрана.

Итоговая версия нашего шейдера будет выглядеть так:

1// Версия API
2#version 460 core
3
4// Вспомогательный инстурментарий для Flutter
5#include <flutter/runtime_effect.glsl>
6
7// Точность вычислений GPU
8precision mediump float;
9
10// Итоговый цвет пикселя
11out vec4 fragColor;
12
13uniform float time;
14uniform vec2 resolution;
15
16const int MAXITER = 30;
17
18vec3 field(vec3 p) {
19	p *= .1;
20	float f = .1;
21	for (int i = 0; i < 3; i++) {
22		p = p.yzx;
23		p = abs(fract(p)-.5);
24		p *= 2.0;
25		f *= 2.0;
26	}
27	p *= p;
28	return sqrt(p+p.yzx)/f-.05;
29}
30
31void main() {
32	vec3 dir = normalize(vec3((FlutterFragCoord().xy-resolution*.5)/resolution.x,1.));
33	float a = time * 0.1;
34	vec3 pos = vec3(0.0,time*0.1,0.0);
35	dir *= mat3(1,0,0,0,cos(a),-sin(a),0,sin(a),cos(a));
36	dir *= mat3(cos(a),0,-sin(a),0,1,0,sin(a),0,cos(a));
37	vec3 color = vec3(0);
38	for (int i = 0; i < MAXITER; i++) {
39		vec3 f2 = field(pos);
40		float f = min(min(f2.x,f2.y),f2.z);
41		
42		pos += dir*f;
43		color += float(MAXITER-i)/(f2+.01);
44	}
45	vec3 color3 = vec3(1.-1./(1.+color*(.09/float(MAXITER*MAXITER))));
46	color3 *= color3;
47	fragColor = vec4(vec3(color3.r+color3.g+color3.b),1.);
48}

Поправим код CustomPainter.

1class ShaderPainter extends CustomPainter {
2  final FragmentShader shader;
3
4  ShaderPainter({ required this.shader });
5
6  @override
7  void paint(Canvas canvas, Size size) {
8    shader
9      ..setFloat(0, 0)
10      ..setFloat(1, size.width)
11      ..setFloat(2, size.height);
12
13    final paint = Paint()..shader = shader;
14    canvas.drawRect(
15      Rect.fromLTWH(0, 0, size.width, size.height),
16      paint,
17    );
18  }
19
20  @override
21  bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
22}

Вуаля, на экране отобразилась следующая картинка.

Group

Как видно в коде выше, первым аргументом мы передаём константное значение. Чтобы оживить наш шейдер, добавим анимацию и передадим её значение в этот параметр.

1class ShaderPainter extends CustomPainter {
2  final Animation<double> animation;
3  final FragmentShader shader;
4
5  ShaderPainter({
6    required this.shader,
7    required this.animation,
8  }) : super(repaint: animation);
9
10  @override
11  void paint(Canvas canvas, Size size) {
12    shader
13      ..setFloat(0, animation.value)
14      ..setFloat(1, size.width)
15      ..setFloat(2, size.height);
16
17    final paint = Paint()..shader = shader;
18    canvas.drawRect(
19      Rect.fromLTWH(0, 0, size.width, size.height),
20      paint,
21    );
22  }
23
24  @override
25  bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
26}

В итоге получаем на экране следующую картину.

Вот и всё! Теперь мы как следует рассмотрели CustomPainter: научились не просто рисовать домики, но и работать со слоями, выполнять трансформации, создавать собственные шейдеры и контролировать смешение цветов с помощью BlendMode.

Если вам интересно углубиться в это тему ещё сильнее, то вот интересные ссылки:

А в следующих параграфах мы детальнее изучим процесс визуализации во Flutter — рендер-дерево, а также его важнейший элемент — RenderObject.

Отмечайте параграфы как прочитанные чтобы видеть свой прогресс обучения

Вступайте в сообщество хендбука

Здесь можно найти единомышленников, экспертов и просто интересных собеседников. А ещё — получить помощь или поделиться знаниями.
Вступить
Сообщить об ошибке
Предыдущий параграф3.6. CustomPainter
Следующий параграф3.8. RenderObject