Обрезка холста
У объекта Canvas
есть методы для обрезки контента: clipRect
, clipPath
, clipRRect
.
Например, это необходимо, чтобы ограничить зону отрисовки в пределах переданного размера. Допустим, нам нужно покрасить фон холста в определённый цвет. Для этого воспользуемся методом drawColor
— и вот какой результат получится:
@override
void paint(Canvas canvas, Size size) {
canvas.drawColor(Colors.red, BlendMode.src);
}
CustomPaint
растянут на весь экран и обёрнут в SafeArea
, но можно заметить что system bar (панель в верху экрана) тоже окрасился в красный. Обрежем зону отрисовки по размеру холста с помощью метода clipRect
.
@override
void paint(Canvas canvas, Size size) {
// Координата левого верхнего угла холста
const leftTopCorner = Offset(0, 0);
// Координата правого нижнего угла холста
final rightBottomCorner = Offset(size.width, size.height);
// Прямоугольник за пределами которого контент холста будет обрезан
final rect = Rect.fromPoints(leftTopCorner, rightBottomCorner);
canvas.clipRect(rect);
canvas.drawColor(Colors.red, BlendMode.src);
}
Как видите, зона отрисовки обрезалась по размеру холста.
Давайте сейчас чуть остановимся и посмотрим, что происходит «под капотом»:
- Сначала вызывается команда
canvas.clipRect
— что означает, что каждая следующая после него операция отрисовки будет обрезаться по переданному ограничению. - Затем вызывается команда
canvas.drawColor
, которая окрашивает весь холст в один цвет.
Такой алгоритм нельзя назвать хорошо оптимизированным — он обрезает каждую операцию отдельно. В нашем примере это допустимо, но представьте что нам нужно обрезать не одну операцию, а пять-шесть, и всё это в цикле.
Вот ещё пример. Мы используем операцию clipRRect
, для обрезки накладываемых цветов в форме прямоугольника с закруглёнными углами.
@override
void paint(Canvas canvas, Size size) {
// Координата левого верхнего угла холста
const leftTopCorner = Offset(0, 0);
// Координата правого нижнего угла холста
final rightBottomCorner = Offset(size.width, size.height);
// Прямоугольник за пределами которого контент холста будет обрезан
final rect = Rect.fromPoints(leftTopCorner, rightBottomCorner);
final rrect = RRect.fromRectAndRadius(rect, const Radius.circular(150));
// Обрезает хост в форме прямоугольника с закруглёнными углами
canvas.clipRRect(rrect);
// На холсте рисуется синий, затем обрезается операцией [clipRRect]
canvas.drawColor(Colors.blue, BlendMode.srcATop);
// На холсте рисуется зелёный, затем обрезается операцией [clipRRect]
canvas.drawColor(Colors.green, BlendMode.srcATop);
// На холсте рисуется красный, затем обрезается операцией [clipRRect]
canvas.drawColor(Colors.red, BlendMode.srcATop);
}
Чтобы оптимизировать процесс отрисовки, воспользуемся слоями.
Слои
Если вы работали в фоторедакторе наподобие Photoshop, то имеете представление о слоях. Если нет, то вот простое объяснение:
Слой — это группа операций поверх холста. Соответственно, слои можно накладывать друг на друга или смешивать друг с другом.
Вот как можно оптимизировать обрезку холста с помощью слоёв — основная идея заключается в том, что мы откладываем применение обрезания и сначала мержим все операции рисования между собой в отдельном слое:
@override
void paint(Canvas canvas, Size size) {
// Координата левого верхнего угла холста
const leftTopCorner = Offset(0, 0);
// Координата правого нижнего угла холста
final rightBottomCorner = Offset(size.width, size.height);
// Прямоугольник за пределами которого контент холста будет обрезан
final rect = Rect.fromPoints(leftTopCorner, rightBottomCorner);
final rrect = RRect.fromRectAndRadius(rect, const Radius.circular(150));
// Сохраняем изначальное состояния холста до применения обрезания
canvas.save();
canvas.clipRRect(rrect);
// Сохраняем операцию обрезки
canvas.saveLayer(null, Paint());
// * Здесь операции будут обрабатываться только между собой
canvas.drawColor(Colors.blue, BlendMode.srcATop);
canvas.drawColor(Colors.green, BlendMode.srcATop);
canvas.drawColor(Colors.red, BlendMode.srcATop);
// Применяет обрезание
canvas.restore();
// * Здесь каждая операция будет обрезаться отдельно
// Востанавливает состояние холста накладывая слой с обрезанием сверху
canvas.restore();
// * Здесь не будет работать обрезание
}
В приведённом коде видно, что помимо вызова 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
— применяет матрицу трансформации к холсту.
Подробнее останавливаться на них не будем — в конце этой главки есть пример. Вместо этого мы сфокусируемся на нюансах, связанных с трансформациями.
Относительное смещение
Сделав какую-либо трансформацию, вы вероятно столкнётесь с ситуацией, что результат будет отличаться от того, что вы ожидали: фигура, которую вы хотели изменить, может переместиться совсем в другое место или вовсе исчезнуть с экрана.
Почему так происходит отлично иллюстрирует изображение ниже: все трансформации происходят левого верхнего края холста.
Чтобы трансформация объекта выполнилась корректно, необходимо выполнять трансформации относительно его центра. Для этого необходимо с помощью метода translate
сместить холст на координаты центра объекта и после использования методов трансформации вернуть холст в исходное положение, вызвав метод translate
с отрицательными координатами.
// Сдвигаем координатную плоскость к центру
canvas.translate(center.dx, center.dy);
// Поворачиваем холст
canvas.rotate(math.pi / 2);
// Сдвигаем координатную плоскость в исходное состояние
canvas.translate(-center.dx, -center.dy);
Использование слоёв
Трансформации холста рекомендуется совершать в отдельном слое. Так трансформация не повлияет на последующие операции
// Сохраняем слой
canvas.save();
// Трансформация холста
canvas.translate(center.dx, center.dy);
canvas.rotate(math.pi / 2);
canvas.translate(-center.dx, -center.dy);
final trianglePath = Path()
..moveTo(center.dx, center.dy - radius)
..lineTo(center.dx + radius, center.dy)
..lineTo(center.dx - radius, center.dy)
..close();
// Рисуем синий треугольник
canvas.drawPath(
trianglePath,
painter..color = Colors.blue.withOpacity(0.5),
);
// Востанавливаем слой
canvas.restore();
// Рисуем красный треугольник
canvas.drawPath(
trianglePath,
painter..color = Colors.red.withOpacity(0.5),
);
Как можно заметить, мы рисуем оба треугольника в одних и тех же координатах, но в случае синего мы изменяем саму систему координат.
Трансформация на практике
Для наглядной иллюстрации использования трансформации приведём следующий пример — компас, полностью нарисованный с помощью CustomPainter
. У нас есть два основных компонента: статичная подложка и изменяющая своё направление стрелка.
Ещё один аспект напрямую связанный со слоями — BlendMode. Это инструмент, который поможет контролировать, как разные слои накладываются друг на друга, и какие цвета получаются в итоге.
BlendMode
Строго говоря, BlendMode — это алгоритмы, для смешивания пикселей. Выбор конкретного алгоритма зависит от выбранного типа класса BlendMode
. Это мы рассмотрим чуть позже, пока поговорим о том, как именно алгоритмы преобразуют цвета.
Каждый алгоритм имеет два входных параметра:
- source — рисуемое изображение;
- destination — исходное изображение, на которое компонуется source.
destination
часто рассматривается как фон. Как источник, так и пункт назначения имеют четыре цветовых канала: красный, зеленый, синий и альфа-каналы.
Обычно они представлены в виде чисел в диапазоне от 0,0 до 1,0. Выходные данные алгоритма также имеют те же четыре канала со значениями, вычисленными из источника и пункта назначения.
Цель различных алгоритмов BlendMode
— получить на вход 4 канала destination
и 4 канала source
и преобразовать их в 4 канала пикселя.
Для простоты представим задачу смешения пикселей абстрактным методом:
Color? resolveColor(Color? source, Color? destination);
Тогда стратегию BlendMode.srcOver
, — дефолтное значение объекта Paint
, обычное наложение картинки на фон, — можно представить так:
Color? resolveColor(Color? source, Color? destination) {
return destination ?? source;
}
То есть если пиксель накладываемого изображения не пустой — возвращает его, иначе — возвращает пиксель фона.
Все стратегии BlendMode
можно посмотреть на странице официальной документации, давайте перейдём к примеру.
BlendMode на практике
Рассмотрим пример использования BlendMode
. Тут нам снова помогут слои:
class BlendModePainter extends CustomPainter {
const BlendModePainter();
@override
void paint(Canvas canvas, Size size) {
// Координата Y середины холста
final verticalCenter = size.height / 2;
// Координата X середины холста
final horizontalCenter = size.width / 2;
// Середина холста
final center = Offset(horizontalCenter, verticalCenter);
// Радиус окружности
final radius = size.shortestSide / 6;
// Стратегия наложения слоёв
const blendMode = BlendMode.plus;
// Сохраняем исходный фон / Выделяем отрисовку кругов в отдельный слой
canvas.saveLayer(null, Paint());
// Рисуем красный круг
canvas.drawCircle(
center.translate(0, -radius),
radius,
// Т.к. слой новый, первый круг необязательно рисовать
// c BlendMode.exclusion
Paint()..color = const Color(0xFFFF0000),
);
// Рисуем зелёный круг
canvas.drawCircle(
center.translate(-radius / 2, 0),
radius,
Paint()
..color = const Color(0xFF00FF00)
..blendMode = blendMode,
);
// Выделяем слой для отрисовки синего круга
canvas.saveLayer(null, Paint()..blendMode = blendMode);
// Рисуем синий круг
canvas.drawCircle(
center.translate(radius / 2, 0),
radius,
Paint()..color = const Color(0xFF0000FF),
);
// Объединяем слой синего круга с остальными
canvas.restore();
// Объединяем круги с исходным фоном
canvas.restore();
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}
В этом примере используется стратегия смешивания 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
.
Вот небольшой пример — текст, раскрашенный с использованием шейдера линейного градиента.
GradientShaderPainter extends CustomPainter {
const GradientShaderPainter();
@override
void paint(Canvas canvas, Size size) {
// Координата Y середины холста
final verticalCenter = size.height / 2;
// Смещение
final offset = Offset(0, verticalCenter);
// Шейдер линейного градиента
final shader = const LinearGradient(
colors: [Colors.red, Colors.blue],
).createShader(Rect.fromLTWH(0, 0, size.width, size.height));
// Выделяем слой для отрисовки текста
canvas.saveLayer(null, Paint());
TextPainter(
text: const TextSpan(
text: 'Текст написан с использованием шейдеров',
style: TextStyle(color: Colors.black),
),
textDirection: TextDirection.ltr,
textAlign: TextAlign.center,
)
// Расчёт размеров, который занимает текст
..layout(minWidth: size.width, maxWidth: size.width)
// Отрисовка текста на холсте
..paint(canvas, offset);
// Выделяем слой для градиента
canvas.saveLayer(null, Paint()..blendMode = BlendMode.srcATop);
// Рисуем на холсте Paint с градиентом
canvas.drawPaint(Paint()..shader = shader);
// Объединяем градиент с текстом
canvas.restore();
// Объединяем текст с исходным фоном
canvas.restore();
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}
BlendMode.srcATop
— накладывает изображение (source) только на пересечение с фоном (destination). В данном случае source — это градиент, а destination — текст.
Собственные шейдеры
Во Flutter есть возможность загрузить свои собственные шейдеры, для этого необходимо выполнить следующие шаги:
- Поместить в проект файл шейдера в формате glsl (напомним, GLSL — язык для программирования шейдеров). Файл имеет расширение .frag.
- Добавить по аналогии с другими ассетами в pubspec.yaml секцию shaders и указать путь до файла шейдера.
- Создать объект
FragmentProgram
через именованный конструкторfromAsset
, передав путь к файлу шейдера. - Вызывать метод
fragmentShader
, у ранее созданного объектаFragmentProgram
.
Давайте же приступим. Для начала создаём файл custom_shader.frag и добавляем его в pubspec.yaml в секцию shaders. Содержимое файла покажем чуть ниже.
flutter:
shaders:
- assets/shaders/custom_shader.frag
Загрузка шейдера — асинхронный процесс, поэтому необходимо предварительно загрузить шейдер перед передачей его в CustomPainter
.
Future<FragmentShader> _loadShader(String shaderAssetPath) async {
final program = await FragmentProgram.fromAsset(shaderAssetPath);
return program.fragmentShader();
}
Теперь можно создать CustomPainter
и передать туда шейдер.
class ShaderPainter extends CustomPainter {
final FragmentShader shader;
ShaderPainter({required this.shader});
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()..shader = shader;
canvas.drawRect(
Rect.fromLTWH(0, 0, size.width, size.height),
paint,
);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}
На стороне Flutter — всё готово чтобы отрисовывать шейдер, но пока наш файл шейдера пуст. Исправим это! Для начала просто просто нарисуем синий фон.
// Версия API
#version 460 core
// Вспомогательный инструментарий для Flutter
#include <flutter/runtime_effect.glsl>
// Точность вычислений GPU
precision mediump float;
// Итоговый цвет пикселя
out vec4 fragColor;
// Константа - синий цвет
const vec3 blue = vec3(0, 0, 255) / 255;
void main() {
fragColor = vec4(blue, 1);
}
Метод main
выполняется для каждого пикселя, рассчитывая финальный цвет пикселя (fragColor
). Переделаем наш шейдер так чтобы получать цвет в качестве входной переменной. Для этого необходимо объявить переменную с ключевым словом uniform
— оно означает, что переменная будет передана снаружи.
// Версия API
#version 460 core
// Вспомогательный инстурментарий для Flutter
#include <flutter/runtime_effect.glsl>
// Точность вычислений GPU
precision mediump float;
// Итоговый цвет пикселя
out vec4 fragColor;
// Переменные, передаваемые в шейдер
uniform vec3 color; // Цвет
void main() {
fragColor = vec4(color/255, 1);
}
Напомним, что каждый канал пикселя — это значение от 0 до 254. Но в синтаксисе GLSL мы оперируем векторами, и значения вектора цвета должно быть в пределах от 0 до 1. Поэтому делим вектор color
на 255.
Доработаем CustomPainter
и передадим цвет в шейдер.
...
static const color = Colors.red;
@override
void paint(Canvas canvas, Size size) {
shader
..setFloat(0, color.red.toDouble())
..setFloat(1, color.green.toDouble())
..setFloat(2, color.blue.toDouble());
final paint = Paint()..shader = shader;
canvas.drawRect(
Rect.fromLTWH(0, 0, size.width, size.height),
paint,
);
}
...
API шейдера очень низкоуровневое. Мы должны передать каждый параметр отдельно — вызвать метод setFloat
и передать индекс аргумента и его значение.
В данном примере есть только одна входная переменная с типом vec3
. vec3
— это просто последовательность из трёх переменных, поэтому мы и передаём значения, указывая индекс в диапазоне от 0 до 2. Представим что есть ещё переменная с типом vec2
, тогда чтобы передать параметры в неё, необходимо вызвать метод setFloat
ещё два раза с индексами 3 и 4.
Сейчас каждый пиксель краситься в один и тот же цвет. Добавим в шейдер логику, которая будет менять яркость цвета в зависимости от Y-координаты пикселя.
// Версия API
#version 460 core
// Вспомогательный инстурментарий для Flutter
#include <flutter/runtime_effect.glsl>
// Точность вычислений GPU
precision mediump float;
// Итоговый цвет пикселя
out vec4 fragColor;
// Переменные, передаваемые в шейдер
uniform vec3 color; // Цвет
uniform vec2 resolution; // Разрешение экрана
void main() {
// Коордианты пикселя
vec2 pixelCoord = FlutterFragCoord();
// Позиция пикселя относительно разрешения экрана
vec2 positionOnScreen = pixelCoord / resolution;
fragColor = vec4((color/255) * positionOnScreen.y, 1);
}
Теперь передадим размеры экрана в CustomPainter
и посмотрим что будет выведено на экране.
...
@override
void paint(Canvas canvas, Size size) {
shader
..setFloat(0, color.red.toDouble())
..setFloat(1, color.green.toDouble())
..setFloat(2, color.blue.toDouble())
..setFloat(3, size.width)
..setFloat(4, size.height);
final paint = Paint()..shader = shader;
canvas.drawRect(
Rect.fromLTWH(0, 0, size.width, size.height),
paint,
);
}
...
Таким образом мы получили шейдер, который динамически рассчитывает яркость цвета в зависимости от Y-координаты, т.е. простейший градиент.
Конечно, вряд ли вы будете писать более сложные шейдеры сами. Для поиска подходящего шейдера можно воспользоваться ресурсом GLSL SANDBOX — это приложение WebGL для live-кодирования шейдеров, которая также содержит большую библиотеку шейдеров, созданных пользователями ресурса.
Но есть одна проблема: скорее всего, без некоторой адаптации эти виджеты не будут компилироваться во Flutter-приложении. Это связано с особенностями реализации поддержки шейдеров во Flutter.
Поэтому давайте возьмём какой-нибудь шейдер и попробуем его адаптировать, например вот этот. Скопируем код в файл и добавим его в проект.
#ifdef GL_ES
precision mediump float;
#endif
uniform float time;
uniform vec2 mouse;
uniform vec2 resolution;
const int MAXITER = 30;
vec3 field(vec3 p) {
p *= .1;
float f = .1;
for (int i = 0; i < 3; i++) {
p = p.yzx; //*mat3(.8,.6,0,-.6,.8,0,0,0,1);
// p += vec3(.123,.456,.789)*float(i);
p = abs(fract(p)-.5);
p *= 2.0;
f *= 2.0;
}
p *= p;
return sqrt(p+p.yzx)/f-.05;
}
void main( void ) {
vec3 dir = normalize(vec3((gl_FragCoord.xy-resolution*.5)/resolution.x,1.));
float a = time * 0.1;
vec3 pos = vec3(0.0,time*0.1,0.0);
dir *= mat3(1,0,0,0,cos(a),-sin(a),0,sin(a),cos(a));
dir *= mat3(cos(a),0,-sin(a),0,1,0,sin(a),0,cos(a));
vec3 color = vec3(0);
for (int i = 0; i < MAXITER; i++) {
vec3 f2 = field(pos);
float f = min(min(f2.x,f2.y),f2.z);
pos += dir*f;
color += float(MAXITER-i)/(f2+.01);
}
vec3 color3 = vec3(1.-1./(1.+color*(.09/float(MAXITER*MAXITER))));
color3 *= color3;
gl_FragColor = vec4(vec3(color3.r+color3.g+color3.b),1.);
}
Попытавшись запустить проект мы получим ошибку ещё во время компиляции. Давайте попробуем поправить код, ориентируясь на пример, который мы сделали ранее.
Во-первых, меняем конфигурационные строки:
~~#ifdef GL_ES
precision mediump float;
#endif~~
// Версия API
#version 460 core
// Вспомогательный инстурментарий для Flutter
#include <flutter/runtime_effect.glsl>
// Точность вычислений GPU
precision mediump float;
...
Всё равно получаем ошибку компиляции. В коде находим gl_FragColor
и gl_FragCoord
, нам нужно заменить их на fragColor
и FlutterFragCoord()
используемые ранее.
// Версия API
#version 460 core
// Вспомогательный инстурментарий для Flutter
#include <flutter/runtime_effect.glsl>
// Точность вычислений GPU
precision mediump float;
// Итоговый цвет пикселя
out vec4 fragColor;
uniform float time;
uniform vec2 mouse;
uniform vec2 resolution;
const int MAXITER = 30;
vec3 field(vec3 p) {
p *= .1;
float f = .1;
for (int i = 0; i < 3; i++) {
p = p.yzx; //*mat3(.8,.6,0,-.6,.8,0,0,0,1);
// p += vec3(.123,.456,.789)*float(i);
p = abs(fract(p)-.5);
p *= 2.0;
f *= 2.0;
}
p *= p;
return sqrt(p+p.yzx)/f-.05;
}
void main() {
vec3 dir = normalize(vec3((FlutterFragCoord().xy-resolution*.5)/resolution.x,1.));
float a = time * 0.1;
vec3 pos = vec3(0.0,time*0.1,0.0);
dir *= mat3(1,0,0,0,cos(a),-sin(a),0,sin(a),cos(a));
dir *= mat3(cos(a),0,-sin(a),0,1,0,sin(a),0,cos(a));
vec3 color = vec3(0);
for (int i = 0; i < MAXITER; i++) {
vec3 f2 = field(pos);
float f = min(min(f2.x,f2.y),f2.z);
pos += dir*f;
color += float(MAXITER-i)/(f2+.01);
}
vec3 color3 = vec3(1.-1./(1.+color*(.09/float(MAXITER*MAXITER))));
color3 *= color3;
fragColor = vec4(vec3(color3.r+color3.g+color3.b),1.);
}
Запускаем ещё раз. Ошибка компиляции пропала, но на экране ничего не отобразилось, также в логах ошибки о несовпадении количества аргументов шейдера с переданными нами. Если посмотреть в код шейдера, то мы видим что ожидается передача 3 аргументов:
float time
— время, можно воспринимать этот параметр как прогресс анимации.vec2 mouse
— текущая позиция курсора мыши, от этого параметра мы избавимся, нам он не нужен.vec2 resolution
— разрешение экрана.
Итоговая версия нашего шейдера будет выглядеть так:
// Версия API
#version 460 core
// Вспомогательный инстурментарий для Flutter
#include <flutter/runtime_effect.glsl>
// Точность вычислений GPU
precision mediump float;
// Итоговый цвет пикселя
out vec4 fragColor;
uniform float time;
uniform vec2 resolution;
const int MAXITER = 30;
vec3 field(vec3 p) {
p *= .1;
float f = .1;
for (int i = 0; i < 3; i++) {
p = p.yzx;
p = abs(fract(p)-.5);
p *= 2.0;
f *= 2.0;
}
p *= p;
return sqrt(p+p.yzx)/f-.05;
}
void main() {
vec3 dir = normalize(vec3((FlutterFragCoord().xy-resolution*.5)/resolution.x,1.));
float a = time * 0.1;
vec3 pos = vec3(0.0,time*0.1,0.0);
dir *= mat3(1,0,0,0,cos(a),-sin(a),0,sin(a),cos(a));
dir *= mat3(cos(a),0,-sin(a),0,1,0,sin(a),0,cos(a));
vec3 color = vec3(0);
for (int i = 0; i < MAXITER; i++) {
vec3 f2 = field(pos);
float f = min(min(f2.x,f2.y),f2.z);
pos += dir*f;
color += float(MAXITER-i)/(f2+.01);
}
vec3 color3 = vec3(1.-1./(1.+color*(.09/float(MAXITER*MAXITER))));
color3 *= color3;
fragColor = vec4(vec3(color3.r+color3.g+color3.b),1.);
}
Поправим код CustomPainter
.
class ShaderPainter extends CustomPainter {
final FragmentShader shader;
ShaderPainter({ required this.shader });
@override
void paint(Canvas canvas, Size size) {
shader
..setFloat(0, 0)
..setFloat(1, size.width)
..setFloat(2, size.height);
final paint = Paint()..shader = shader;
canvas.drawRect(
Rect.fromLTWH(0, 0, size.width, size.height),
paint,
);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}
Вуаля, на экране отобразилась следующая картинка.
Как видно в коде выше, первым аргументом мы передаём константное значение. Чтобы оживить наш шейдер, добавим анимацию и передадим её значение в этот параметр.
class ShaderPainter extends CustomPainter {
final Animation<double> animation;
final FragmentShader shader;
ShaderPainter({
required this.shader,
required this.animation,
}) : super(repaint: animation);
@override
void paint(Canvas canvas, Size size) {
shader
..setFloat(0, animation.value)
..setFloat(1, size.width)
..setFloat(2, size.height);
final paint = Paint()..shader = shader;
canvas.drawRect(
Rect.fromLTWH(0, 0, size.width, size.height),
paint,
);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}
В итоге получаем на экране следующую картину.
Вот и всё! Теперь мы как следует рассмотрели CustomPainter
: научились не просто рисовать домики, но и работать со слоями, выполнять трансформации, создавать собственные шейдеры и контролировать смешение цветов с помощью BlendMode
.
Если вам интересно углубиться в это тему ещё сильнее, то вот интересные ссылки:
- Официальная документация по CustomPainter.
- Статья: использование шейдеров во Flutter.
- Видео: использование шейдеров во Flutter.
- Видео: рисование с использованием вершин.
А в следующих параграфах мы детальнее изучим процесс визуализации во Flutter — рендер-дерево, а также его важнейший элемент — RenderObject
.