Привет, Хабр! Меня зовут Станислав Чернышев, я автор книги «Основы Dart», телеграм-канала MADTeacher и доцент кафедры прикладной информатики в Санкт-Петербургском государственном университете аэрокосмического приборостроения.
Вчера на меня напала жуткая прокрастинация к одной задаче по работе. А именно - написать кучу тестов для рабочей программы дисциплины, которая тупо значится как альтернативная и, соответственно, никогда не преподается, но, т.к. пришли новые требования от мониторинговых организаций – все равно придется их составлять >_<...
В результате возложения детородного органа на написание тестов, сделал перевод статьи посвященной знакомству с Flutter GPU с Medium. Его лучше всего отнести к разряду вольных, т.е. он не дословный и отбрасывает некоторый авторский текст, сокращая его в тех местах, где это не критично для смысла. А последующее редактирование добавило ей щепотку забавных реплик ;)
Getting started with Flutter GPU
В анонсе релиза Flutter 3.24 был представлен новый низкоуровневый графический API – Flutter GPU (прим. переводчика – до стабилизации API будет доступен в качестве пакета flutter_gpu), а также высокоуровневая библиотека для 3D-рендеринга – Flutter Scene (пакет: flutter_scene). И Flutter GPU, и Flutter Scene в настоящее время находятся на ранней стадии разработки, поэтому доступны только в основном канале (main channel) Flutter (из-за зависимости от экспериментальных функций), требуют включения Impeller и иногда могут вносить изменения, ломающие предыдущие версии API Flutter.
Статья состоит из двух руководств по «началу работы» с этими пакетами:
? Для любителей хардкора: Знакомимся с Flutter GPU
Предназначено для программистов, имеющих опыт работы с графикой или интересующихся низкоуровневой графикой, с заделом на создание собственных рендеров с нуля в Flutter. Звезд с неба не ловим и рисуем треугольник!
? Для любителей смузи: 3D-рендеринг с помощью Flutter Scene
Предназначено для Flutter-разработчиков, которые хотят добавить 3D-фичи в свои приложения или создавать 3D-игры с помощью Dart и Flutter. Реализуем проект, который импортирует и рендерит 3D-ассеты.
Знакомимся с Flutter GPU
⚠️ Внимание! ⚠️ Flutter GPU – низкоуровневое API. Подавляющее большинство Flutter-разработчиков, которые получат выгоду от его существования, будут пользоваться более высокоуровневой библиотекой для рендеринга, опубликованной на pub.dev (например – Flutter Scene). Если вас не интересует сам Flutter GPU API, а интересует только 3D-рендеринг, переходите к второй части статьи.
![Великолепно! Это поле расстояний с указанием ray-marched. Вы можете визуализировать его с помощью Flutter GPU, но это вполне возможно сделать и с помощью пользовательских фрагментных шейдеров Великолепно! Это поле расстояний с указанием ray-marched. Вы можете визуализировать его с помощью Flutter GPU, но это вполне возможно сделать и с помощью пользовательских фрагментных шейдеров](https://habrastorage.org/getpro/habr/upload_files/586/e47/2c5/586e472c55bb5b9e1db386885cde1d07.png)
Flutter GPU – встроенный низкоуровневый графический API Flutter. Он позволяет создавать и интегрировать пользовательские рендеры в Flutter путем их написания на Dart и GLSL. При этом нет необходимости погружаться в омут платформозависимого нативного кода!
В настоящее время этот механизм находится на ранней стадии разработки и предлагает базовый API растеризации. По мере приближения к его стабильной версии будут добавляться и дорабатываться дополнительные функции. В идеале, Flutter GPU должен будет поддерживать все платформы, на которые нацелен фреймворк, что скажется на развитии экосистемы кросс-платформенных решений для рендеринга в Flutter.
3D-рендеринг – лишь один из возможных вариантов использования Flutter GPU. Другие сценарии включают в себя: создание специализированных 2D-рендеров, рендеринг 3D-фрагментов 4D-пространства или проектирование неэвклидовых пространств. Пример отличного варианта использования пользовательского 2D-рендерера на базе Flutter GPU – 2D-анимация персонажей, основанная на скелетной деформации сетки (Spine 2D). Такие решения обычно имеют анимационные клипы, которые управляют свойствами перемещения, вращения и масштабирования костей в иерархии, где каждая вершина имеет несколько связанных «весов костей», которые определяют, какие кости должны влиять на вершину и насколько сильно.
При использовании Canvas-решения, такого как drawVertices, веса преобразования костей рассчитываются для каждой вершины на CPU. С помощью Flutter GPU преобразования костей можно передавать в вершинный шейдер в виде однородного массива или сэмплера текстур. Это позволит параллельно вычислять конечное положение каждой вершины на GPU на основе состояния скелета и весов костей для каждой вершины.
Итак, давайте приступим к работе с Flutter GPU и нарисуем свой первый треугольник!
![](https://habrastorage.org/getpro/habr/upload_files/1b5/49b/8ce/1b549b8ceddff321206dde0c7d595407.png)
Перед тем, как перейдем к следующим шагам, следует отметить, что Flutter GPU можно использовать только на тех платформах, которые поддерживаются Impeller. На момент написания статьи в iOS он включен по умолчанию, а macOS и Android требуют небольших танцев с бубном.
Добавляем Flutter GPU в свой проект
Так как Flutter GPU находится на ранней стадии разработки, не исключены сбои в работе его API и отсутствие некоторых функций работы с графикой. По этим причинам настоятельно рекомендуется ориентироваться на основной канал (main channel) при разработке собственных пакетов для Flutter GPU. Если вы столкнулись с неожиданным поведением, ошибками или пожеланиями по функциям, пожалуйста, создавайте issue, используя стандартные шаблоны в GitHub-репозитории Flutter. В нем все отслеживаемые проблемы, связанные с Flutter GPU, получают метку flutter-gpu.
Итак, прежде чем экспериментировать с Flutter GPU, с помощью следующих команд переключите Flutter на основной канал:
flutter channel main
flutter upgrade
Теперь создайте новый проект.
flutter create my_cool_renderer
cd my_cool_renderer
Добавьте пакет flutter_gpu в pubspec.
flutter pub add flutter_gpu --sdk=flutter
Создание и импортирование шейдерных пакетов
Чтобы что-либо визуализировать с помощью Flutter GPU, вам потребуется создать несколько GLSL-шейдеров. В отличие от уже имеющихся в Flutter фрагментных шейдеров, шейдеры Flutter GPU имеют несколько иную семантику. Особенно, когда дело касается единообразных привязок. В качестве примера давайте реализуем вершинный шейдер, который будет работать вместе с фрагментным.
Начнем с определения самых простых шейдеров. В корневой директории проекта создайте каталог shaders и заполните его двумя шейдерами: simple.vert
и simple.frag
// Copy into: shaders/simple.vert
in vec2 position;
void main() {
gl_Position = vec4(position, 0.0, 1.0);
}
В коде выше мы перечислили 2D-позиции. Для каждой из вершин, простой вершинный шейдер назначает эти 2D-позиции выходу пространства отсечения gl_Position
.
// Copy into: shaders/simple.frag
out vec4 frag_color;
void main() {
frag_color = vec4(0, 1, 0, 1);
}
Фрагментный шейдер вершинного. Он выводит RGBA-цвет с диапазоном от (0, 0, 0, 0, 0) до (1, 1, 1, 1, 1). Таким образом, все будет затенено зеленым цветом.
Теперь, когда у нас есть шейдеры, скомпилируйте их с помощью aot-компилятора шейдеров Flutter, воспользовавшись пакетом flutter_gpu_shaders, который можно добавить в качестве зависимости к проекту следующей командой:
flutter pub add flutter_gpu_shaders
Скомпилированные исходные тексты GPU-шейдеров Flutter для целевой(ых) платформы(ых) упаковываются в файлы с расширением «.shaderbundle». Их можно спокойно добавлять в проект как обычные ресурсы (assets).
Далее, в корневой директории проекта, создайте файл манифеста «my_renderer.shaderbundle.json» и добавьте в него следующее описание содержимого пакета шейдеров:
{
"SimpleVertex": {
"type": "vertex",
"file": "shaders/simple.vert"
},
"SimpleFragment": {
"type": "fragment",
"file": "shaders/simple.frag"
}
}
Каждая запись в манефесте может иметь произвольное имя. В нашем случае – «SimpleVertex» и «SimpleFragment». Эти имена используются для поиска шейдеров в приложении.
Далее, для сборки пакета шейдеров, используйте пакет flutter_gpu_shaders. Вы можете добавить хук, который автоматически запускает сборку, включив экспериментальную функцию «native assets». Для этого введите в терминале команду для ее включения и установки пакета native_assets_cli:
flutter config --enable-native-assets
flutter pub add native_assets_cli
Если функция native-assets включена, добавьте в корневую директорию проекта каталог hook, в который поместите скрипт build.dart. Он будет автоматически запускать сборку пакета шейдеров:
// Copy into: hook/build.dart
import 'package:native_assets_cli/native_assets_cli.dart';
import 'package:flutter_gpu_shaders/build.dart';
void main(List<String> args) async {
await build(args, (config, output) async {
await buildShaderBundleJson(
buildConfig: config,
buildOutput: output,
manifestFileName: 'my_renderer.shaderbundle.json');
});
}
Функция buildShaderBundleJson в ходе сборки проекта собирает пакет шейдеров и выводит результат в build/shaderbundles/my_renderer.shaderbundle в его корневом каталоге.
Формат шейдерного пакета привязан к используемой при сборке версии Flutter и со временем может измениться. Если вы являетесь автором пакета, который собирает пакеты шейдеров, не проверяйте сгенерированные файлы с расширением «.shaderbundle» в вашем исходном дереве. Вместо этого воспользуйтесь предложенным ранее подходом для автоматизации процесса сборки (скрипт build.dart). Это позволит разработчикам, которые будут использовать вашу библиотеку, всегда создавать свежие пакеты шейдеров в правильном формате!
Теперь, когда вы автоматически собираете свой пакет шейдеров, импортируйте его как обычный ресурс (asset). Для этого добавьте следующую запись в pubspec.yaml:
flutter:
assets:
- build/shaderbundles/
Для загрузки шейдеров во время выполнения необходимо в директории lib создать файл shaders.dart и добавить в него приведенный ниже код:
// Copy into: lib/shaders.dart
import 'package:flutter_gpu/gpu.dart' as gpu;
const String _kShaderBundlePath =
'build/shaderbundles/my_renderer.shaderbundle';
// NOTE: If you're building a library, the path must be prefixed
// with a package name. For example:
// 'packages/my_cool_renderer/build/shaderbundles/my_renderer.shaderbundle'
gpu.ShaderLibrary? _shaderLibrary;
gpu.ShaderLibrary get shaderLibrary {
if (_shaderLibrary != null) {
return _shaderLibrary!;
}
_shaderLibrary = gpu.ShaderLibrary.fromAsset(_kShaderBundlePath);
if (_shaderLibrary != null) {
return _shaderLibrary!;
}
throw Exception("Failed to load shader bundle! ($_kShaderBundlePath)");
}
Этот код представляет собой паттерн Singleton и задает единственную точку доступа к инстансу библиотеки времени выполнения шейдера Flutter GPU. При первом доступе к shaderLibrary библиотека времени выполнения шейдера инициализируется с использованием собранного пакета ресурсов с помощью gpu.ShaderLibrary.fromAsset(shader_bundle_path)
.
Теперь проект настроен и готов к использованию шейдеров Flutter GPU. А значит пришло время отрисовать треугольник!
Рисуем треугольник
В этой части руководства мы создадим текстуру RGBA Flutter GPU и RenderPass, который прикрепит текстуру в качестве цветового вывода. Затем отрисуем текстуру в виджете с помощью Canvas.drawImage. Для краткости излагаемого материала пошлем в далекое пешее эротическое путешествие лучшие практики и знатно накидаем на вентилятор, перестраивая все ресурсы для каждого кадра и используя минимум функционала Flutter GPU.
Чтобы преобразовать нашу текстуру в dart:ui.Image, пометим ее как «shader readable». Это позволит отобразить отрендеренные результаты в дереве виджетов с помощью dart:ui.Canvas, достучаться до которого можно через CustomPaint, передав на вход его аргумента painter пользовательский виджет, наследуемый от CustomPainter. Для этого замените исходное содержимое lib/main.dart на:
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter_gpu/gpu.dart' as gpu;
// NOTE: We made this earlier while setting up shader bundle imports!
import 'shaders.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter GPU Triangle Example',
home: CustomPaint(
painter: TrianglePainter(),
),
);
}
}
class TrianglePainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
// Attempt to access `gpu.gpuContext`.
// If Flutter GPU isn't supported, an exception will be thrown.
print('Default color format: ' +
gpu.gpuContext.defaultColorFormat.toString());
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
}
Теперь запустите приложение:
flutter run -d macos --enable-impeller
![](https://habrastorage.org/getpro/habr/upload_files/889/baf/f36/889baff3657dc169ba5f698de16fa18a.png)
Т.к. Flutter GPU в настоящее время требует включения Impeller, для написания этого руководства использовалась macOS. Если Flutter GPU работает должным образом, то в терминал выведется формат цвета по умолчанию:
flutter: Default color format: PixelFormat.b8g8r8a8UNormInt
Если Impeller не включен, при попытке получить доступ к gpu.gpuContext
будет выброшено исключение:
Exception: Flutter GPU requires the Impeller rendering backend to be enabled.
The relevant error-causing widget was:
CustomPaint
Для начала создадим и очистим текстуру Flutter GPU. После чего отобразим на пользовательском интерфейсе, нарисовав ее на Canvas.
Первым шагом создадим текстуру размером с Canvas. Так как в последующем коде мы будем использовать только инструкции, которые обращаются к памяти текстуры с устройства (GPU), выберем режим gpu.StorageMode.devicePrivate
:
final texture = gpu.gpuContext.createTexture(gpu.StorageMode.devicePrivate,
size.width.toInt(), size.height.toInt())!;
Если данные текстуры перезаписываются путем загрузки с хоста (процессора), используйте режим StorageMode.hostVisible. А для вложений, которым не нужно превышать время жизни одного RenderPass, т.е. которые могут храниться в памяти тайла и не нуждаются в поддержке выделения VRAM, выбирайте режим StorageMode.deviceTransient. Часто этим критериям соответствуют текстуры глубины/трафарета.
Следующим шагом определим RenderTarget. Цели рендеринга содержат набор «вложений», описывающих структуру памяти для каждого фрагмента и ее поведение настройки/демонтажа в начале и конце RenderPass. Таким образом, вы можете воспринимать RenderTarget, как повторно используемый дескриптор для RenderPass.
Для наших целей достаточно простого RenderTarget, состоящего только из одного цветового вложения:
final renderTarget = gpu.RenderTarget.singleColor(
gpu.ColorAttachment(texture: texture, clearValue: Colors.lightBlue)
);
Обратите внимание на то, что этот код устанавливает clearValue
в светло-голубой цвет. У каждого такого вложения имеется LoadAction
и StoreAction
. Они определяют, что должно произойти с эфемерной памятью тайлового вложения в начале и конце прохода.
Цветовые вложения по умолчанию установлены в LoadAction.clear
(инициализирует тайловую память заданным цветом) и StoreAction.store
(сохраняет результаты в выделенной VRAM прикрепленной текстуры).
Третьим шагом создадим CommandBuffer
и посредством него инстанцируем RenderPass
. Для этого передадим на вход его метода createRenderPass
полученный на предыдущем шаге экземпляр RenderTarget
. И в конечном счете, закончим сей мерлезонский балет очисткой текстуры:
final commandBuffer = gpu.gpuContext.createCommandBuffer();
final renderPass = commandBuffer.createRenderPass(renderTarget);
// ... draw calls will go here!
commandBuffer.submit();
Последним шагом нарисуем инициализированную текстуру на Canvas!
final image = texture.asImage();
canvas.drawImage(image, Offset.zero, Paint());
![](https://habrastorage.org/getpro/habr/upload_files/237/1e6/b7a/2371e6b7a93fc38f3cb9d8da7b89b5fa.png)
Теперь, когда у нас есть подключенный RenderPass
и результаты отображаются на экране, приступим к рисованию треугольника. Для этого настроим:
RenderPipeline
, созданный из наших шейдеровДоступный для GPU буфер, содержащий нашу геометрию (три позиции вершин).
Создать RenderPipeline
легко! Достаточно объединить вершинный и фрагментный шейдер:
final vert = shaderLibrary['SimpleVertex']!;
final frag = shaderLibrary['SimpleFragment']!;
final pipeline = gpu.gpuContext.createRenderPipeline(vert, frag);
Шейдер SimpleVertex
имеет только один вход: in vec2 position
. Поэтому, чтобы нарисовать три вершины, потребуется три набора из двух чисел с плавающей точкой:
final vertices = Float32List.fromList([
-0.5, -0.5, // First vertex
0.5, -0.5, // Second vertex
0.0, 0.5, // Third vertex
]);
final verticesDeviceBuffer = gpu.gpuContext
.createDeviceBufferWithCopy(ByteData.sublistView(vertices))!;
Осталось только привязать новые ресурсы и для завершения отрисовки вызвать renderPass.draw()
:
renderPass.bindPipeline(pipeline);
final verticesView = gpu.BufferView(
verticesDeviceBuffer,
offsetInBytes: 0,
lengthInBytes: verticesDeviceBuffer.sizeInBytes,
);
renderPass.bindVertexBuffer(verticesView, 3);
renderPass.draw();
Теперь запустите приложение и полюбуйтесь зеленым треугольником!
![](https://habrastorage.org/getpro/habr/upload_files/b94/870/536/b9487053689e77919bbcb9c0b2594695.png)
Ура, вы создали рендер с нуля с помощью Flutter, Dart и небольшого количества магии GLSL!
Неважно, впервые ли вы рендерите треугольник или являетесь опытным ветераном, я надеюсь, что вы продолжите «играть» с Flutter GPU и посмотрите на пакеты, над которыми мы работаем, например Flutter Scene.
В будущем мы надеемся опубликовать удобные обучающие материалы, которые глубже погрузят вас в подноготную Flutter GPU (сейчас посмотрели только на вершину айсберга) и познакомят с лучшими практиками. А до тех пор, в качестве более полного примера использования возможностей Flutter GPU, рекомендую изучить Flutter Scene.
3D-рендеринг с помощью Flutter Scene
Flutter Scene (пакет flutter_scene) – новый графический пакет 3D-сцен на базе Flutter GPU. Он позволяет импортировать анимированные glTF-модели и визуализировать 3D-сцены в режиме реального времени. Основная цель его создания – предоставить разработчикам пакет, который упрощает создание интерактивных 3D-приложений и игр в Flutter.
![](https://habrastorage.org/getpro/habr/upload_files/ad2/780/349/ad2780349b1786ac19c3b1423346bb2f.png)
Этот пакет начинал свою жизнь как расширение dart:ui для 3D-рендерера, написанного на C++ и встроенного непосредственно в нативную среду выполнения Flutter. Теперь же он переписан для Flutter GPU и имеет более гибкий интерфейс.
Как и Flutter GPU, Flutter Scene в настоящее время находится на ранней стадии разработки и требует включения Impeller. Разработчики этого пакета, как правило, следят за последними изменениями в API Flutter GPU, поэтому настоятельно рекомендуется использовать основной канал (main channel) при экспериментах с Flutter Scene.
Создание проекта с использованием Flutter Scene
Для начала перейдите в основной канал (main channel) фреймворка:
flutter channel main
flutter upgrade
и создайте новый проект:
flutter create my_3d_app
cd my_3d_app
Для автоматического создания шейдеров и импорта 3D-моделей Flutter Scene использует экспериментальную фичу –native-assets
. Для ее включения введите в терминале следующую команду:
flutter config --enable-native-assets
После чего добавьте Flutter Scene в качестве зависимости проекта:
flutter pub add flutter_scene
Так как для реализуемого примера, при взаимодействии с API Flutter Scene, понадобится использовать несколько конструкций vector_math, добавьте в зависимости пакет vector_math:
flutter pub add flutter_scene vector_math
Теперь займемся импортированием 3D-модели!
Импорт 3D-модели
Во-первых, нужна 3D-модель для рендеринга. Для этого руководства используется обычный glTF-ассет: DamagedHelmet.glb. Вот как он выглядит:
![The original Damaged Helmet model was created by theblueturtle_ in 2016 (license: CC BY-NC 4.0 International). The converted glTF version was created by ctxwing in 2018 (license: CC BY 4.0 International) The original Damaged Helmet model was created by theblueturtle_ in 2016 (license: CC BY-NC 4.0 International). The converted glTF version was created by ctxwing in 2018 (license: CC BY 4.0 International)](https://habrastorage.org/getpro/habr/upload_files/3c1/20e/c15/3c120ec1588238fbc8a3252a125c4913.png)
Возьмите его из репозитория glTF-ассетов, размещенного на GitHub и поместите файл DamagedHelmet.glb в корневую директорию проекта.
Как и большинство 3D-рендереров, работающих в режиме реального времени, Flutter Scene использует специализированный формат 3D-моделей. Для того, чтобы преобразовать стандартные двоичные файлы glTF (файлы .glb) в этот формат, добавьте пакет flutter_scene_importer в качестве зависимости:
flutter pub add flutter_scene_importer
После чего, для запуска импортирования, введите в терминале следующую команду:
dart --enable-experiment=native-assets \
run flutter_scene_importer:import \
--input "path/to/my/source_model.glb" \
--output "path/to/my/imported_model.model"
Чтобы автоматически запускать импортер, воспользуемся хуком сборки нативных ресурсов. Для этого сначала добавьте native_assets_cli в качестве обычной зависимости проекта:
flutter pub add native_assets_cli
А следующим шагом добавьте в корневую директорию проекта каталог hook, в который поместите скрипт build.dart:
import 'package:native_assets_cli/native_assets_cli.dart';
import 'package:flutter_scene_importer/build_hooks.dart';
void main(List<String> args) {
build(args, (config, output) async {
buildModels(buildConfig: config, inputFilePaths: [
'DamagedHelmet.glb',
]);
});
}
Утилита buildModels из пакета flutter_scene_importer принимает на свой вход список моделей для сборки, пути к которым указывается относительно корневой директории собираемого проекта. В нашем случае buildModels собирает пакет шейдеров и выводит результат в build/models/DamagedHelmet.model.
Формат импортируемой модели привязан к используемой при сборке версии Flutter и со временем может измениться. При создании приложения или библиотеки, использующей Flutter Scene, не проверяйте сгенерированные файлы с расширением «.model» в исходном дереве. Вместо этого воспользуйтесь предложенным ранее подходом для автоматизации процесса сборки (скрипт build.dart). Это позволит всегда (вне зависимости от версии Flutter Scene) создавать свежие файлы «.model» в правильном формате!
Теперь, когда импортирование модели осуществляется в автоматическом режиме, добавим путь к директории, в которой она располагается, в раздел assets файла pubspec.yaml:
flutter:
assets:
- build/models/
В будущем native assets позволит хукам сборки автоматизировать и эту процедуру.
Рендеринг 3D-сцены
Перейдем к коду приложения. Во-первых, создайте StatefulWidget
для сохранения сцены во всех кадрах. Так как анимация будет основана на времени, нам понадобится миксина SingleTickerProviderStateMixin
и переменная elapsedSeconds
:
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter_scene/camera.dart';
import 'package:flutter_scene/node.dart';
import 'package:flutter_scene/scene.dart';
import 'package:vector_math/vector_math.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatefulWidget{
const MyApp({super.key});
@override
MyAppState createState() => MyAppState();
}
class MyAppState extends State<MyApp> with SingleTickerProviderStateMixin {
double elapsedSeconds = 0;
Scene scene = Scene();
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'My 3D app',
home: Placeholder(),
);
}
}
Запустите приложение, чтобы убедиться в отсутствии ошибок. Не забудьте включить Impeller!
flutter run -d macos --enable-impeller
![](https://habrastorage.org/getpro/habr/upload_files/534/f40/350/534f40350e4acd6b47d47f657eb75ce3.png)
Прежде чем продолжить, настроем тикер для анимации, переопределив initState
в MyAppState
:
@override
void initState() {
createTicker((elapsed) {
setState(() {
elapsedSeconds = elapsed.inMilliseconds.toDouble() / 1000;
});
}).start();
super.initState();
}
До тех пор, пока виджет виден, callback-функция тикера будет вызывается для каждого кадра, запуская его перестроение посредством setState.
Теперь загрузим помещенную ранее в проект 3D-модель и добавим ее на сцену. Для этого воспользуемся Node.fromAsset, которая асинхронно десериализует модель и возвращает Future<Node>, когда она будет готова к добавлению.
@override
void initState() {
createTicker((elapsed) {
setState(() {
elapsedSeconds = elapsed.inMilliseconds.toDouble() / 1000;
});
}).start();
Node.fromAsset('build/models/DamagedHelmet.model').then((model) {
model.name = 'Helmet';
scene.add(model);
});
super.initState();
}
Однако, 3D-сцена до сих пор не рендерится! Чтобы это исправить, воспользуемся функцией Scene.render, которая принимает на свой вход UI Canvas, Flutter Scene Camera и размер.
Для доступа к Canvas нам потребуется объявить пользовательский класс, наследующийся CustomPainter
и переопределить в нем пару методов.
class ScenePainter extends CustomPainter {
ScenePainter({required this.scene, required this.camera});
Scene scene;
Camera camera;
@override
void paint(Canvas canvas, Size size) {
scene.render(camera, canvas, viewport: Offset.zero & size);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
}
Не забудьте установить переопределение shouldRepaint
на возврат значения true
. Это необходимо для того, чтобы CustomPainter
перерисовывался каждый раз, когда происходит перестроение.
Следующим шагом добавим в методе build
создание экземпляра класса ScenePainter
и передадим его на вход аргумента painter виджета CustomPaint
:
@override
Widget build(BuildContext context) {
final painter = ScenePainter(
scene: scene,
camera: PerspectiveCamera(
position: Vector3(sin(elapsedSeconds) * 3, 2,cos(elapsedSeconds)* 3),
target: Vector3(0, 0, 0),
),
);
return MaterialApp(
title: 'My 3D app',
home: CustomPaint(painter: painter),
);
}
Этот код предписывает камере двигаться по непрерывной окружности, но всегда быть направленной в начало координат.
Запустите приложение и полюбуйтесь на результат своих трудов!
flutter run -d macos --enable-impeller
![](https://habrastorage.org/getpro/habr/upload_files/6d4/a85/2cb/6d4a852cbd1503f1322b723ee93ec0e3.gif)
Ниже приведен весь код приложения:
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter_scene/camera.dart';
import 'package:flutter_scene/node.dart';
import 'package:flutter_scene/scene.dart';
import 'package:vector_math/vector_math.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
MyAppState createState() => MyAppState();
}
class MyAppState extends State<MyApp> with SingleTickerProviderStateMixin {
double elapsedSeconds = 0;
Scene scene = Scene();
@override
void initState() {
createTicker((elapsed) {
setState(() {
elapsedSeconds = elapsed.inMilliseconds.toDouble() / 1000;
});
}).start();
Node.fromAsset('build/models/DamagedHelmet.model').then((model) {
model.name = 'Helmet';
scene.add(model);
});
super.initState();
}
@override
Widget build(BuildContext context) {
final painter = ScenePainter(
scene: scene,
camera: PerspectiveCamera(
position: Vector3(sin(elapsedSeconds) * 3, 2, cos(elapsedSeconds) * 3),
target: Vector3(0, 0, 0),
),
);
return MaterialApp(
title: 'My 3D app',
home: CustomPaint(painter: painter),
);
}
}
class ScenePainter extends CustomPainter {
ScenePainter({required this.scene, required this.camera});
Scene scene;
Camera camera;
@override
void paint(Canvas canvas, Size size) {
scene.render(camera, canvas, viewport: Offset.zero & size);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
}
Flutter ждет большое будущее
Если вы смогли успешно следовать одному из этих руководств и получить что-то работающее: Уииии, поздравляю! Если нет – не расстраивайтесь. И Flutter GPU, и Flutter Scene – очень молодые проекты с ограниченной платформенной поддержкой. Но, я думаю, что когда-нибудь мы будем с нежностью вспоминать эти скромные начинания.
С помощью Impeller команда Flutter полностью взяла на себя ответственность за стек рендеринга. Это было необходимо для адаптации средств рендеринга к их вариантам использования в Flutter. И теперь мы начинаем новую главу в истории Flutter. В которой ВЫ берете на себя управление рендерингом!
Flutter Scene начинался как компонент C++ в Impeller вместе с 2D Canvas renderer с небольшим расширением dart:ui. Уже в самом начале создания этой библиотеки я знал, что Flutter Engine не станет его конечным пунктом назначения.
Количество архитектурных решений для 3D-рендереров огромно, и ни один универсальный 3D-рендерер не может эффективно использоваться для решения всевозможных задач. «Универсальный» и «высокая производительность» – это, как правило, противоположные цели. В лучшем случае, достаточность во всем, гарантирует превосходство ни в чем.
В мире высокопроизводительного рендеринга ситуация еще хуже. Специализация для одного варианта использования часто означает ухудшение производительности в другом.
Короче говоря, просто невозможно предоставить универсальный 3D-рендер, который можно эффективно использовать для различных задач. Но, продумав низкоуровневые API, необходимые для создания собственных решений (Flutter GPU), и реализовав универсальный 3D-рендер поверх него, который Flutter-сообщество может легко проверить и модифицировать (Flutter Scene), мы создаем для Flutter-разработчиков технологию с низким риском устаревания и большим профитом от использования.
Мне не терпится увидеть, что вы сможете реализовать, используя эти новые возможности Flutter. Следите за будущими выпусками Flutter Scene. Многое еще впереди.
А пока я возвращаюсь к работе.
До скорой встречи. :)
Перевод подготовил затраханный за лето препод, бомбящий в телеграм-канал MADTeacher
P.S. А как проходит ваша прокрастинация?
gev
Еще бы геометрическое ядро...