Привет, Хабр! Меня зовут Станислав Чернышев, я автор книги «Основы 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, но это вполне возможно сделать и с помощью пользовательских фрагментных шейдеров

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 и нарисуем свой первый треугольник!

Перед тем, как перейдем к следующим шагам, следует отметить, что 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

Т.к. 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());

Теперь, когда у нас есть подключенный RenderPass и результаты отображаются на экране, приступим к рисованию треугольника. Для этого настроим:

  1. RenderPipeline, созданный из наших шейдеров

  2. Доступный для 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();

Теперь запустите приложение и полюбуйтесь зеленым треугольником!

Ура, вы создали рендер с нуля с помощью 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.

Этот пакет начинал свою жизнь как расширение 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)

Возьмите его из репозитория 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

Прежде чем продолжить, настроем тикер для анимации, переопределив 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

Ниже приведен весь код приложения:

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. А как проходит ваша прокрастинация?

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


  1. gev
    14.08.2024 18:55

    Еще бы геометрическое ядро...