Flutter GPU 入门:使用自定义渲染器和 3D 渲染

Flutter 3.24 版本引入了一个名为 Flutter GPU 的新型底层图形 API。同时还有一个由 Flutter GPU 驱动的 3D 渲染库,叫做 Flutter Scene (package: flutter_scene)。Flutter GPU 和 Flutter Scene 目前都处于预览阶段,仅在 Flutter 的 主渠道上可用(因为依赖实验性功能),需要 启用 Impeller,并且可能偶尔引入破坏性变更。

本文包含两个关于这些 package 的“入门”指南:

  1. 🔺 进阶: Flutter GPU 入门
    如果你是一位经验丰富的图形程序员,或者你对底层图形感兴趣并想在 Flutter 中从头构建渲染器,那么这个指南将帮助你开始使用 Flutter GPU。你将从零开始绘制你的第一个三角形……使用 Flutter!
  2. 💚 中级: 使用 Flutter Scene 进行 3D 渲染如果你是一名想要在应用中添加 3D 功能的 Flutter 开发者,或者你想使用 Dart 和 Flutter 创建 3D 游戏,那么这个指南就是为你准备的!你将设置一个项目,导入并在 Flutter 中渲染 3D 资源。

Flutter GPU 入门

#

⚠️警告!⚠️ Flutter GPU 本质上是一个底层 API。绝大多数 Flutter 开发者感受 Flutter GPU 强大的方式是通过使用发布在 Pub.dev 上的高级渲染库,比如 Flutter Scene。如果你对 Flutter GPU API 本身不感兴趣,而只对 3D 渲染感兴趣,可以直接跳到使用 Flutter Scene 进行 3D 渲染部分。

哇,真漂亮。这是一个光线追踪的符号距离场。

你可以使用 Flutter GPU 来渲染这个,不过使用 自定义片段着色器 也完全可以实现。

Flutter GPU 入门

#

Flutter GPU 是 Flutter 内置的底层图形 API。它允许你通过编写 Dart 代码和 GLSL 着色器在 Flutter 中构建和集成自定义渲染器。无需编写任何本机平台代码。

目前,Flutter GPU 处于早期预览阶段,提供了基本的光栅化 API,但随着 API 接近稳定版,将继续添加和完善更多功能。

Flutter GPU 还需要 启用 Impeller。这意味着它只能在 Impeller 支持的平台上使用。在撰写本文时,Impeller 支持:

  • iOS(默认开启)
  • macOS(选择加入预览)
  • Android(选择加入预览)

我们的目标是最终让 Flutter GPU 支持 Flutter 的所有目标平台。我们的终极目标是培养 Flutter 中跨平台渲染解决方案的生态系统,让 package 作者易于维护,用户易于安装。

3D 渲染只是一个可能的用例。Flutter GPU 还可以用于构建专门的 2D 渲染器,或者做一些更非常规的事情,比如渲染 4D 空间的 3D 切片,或者投影非欧几里德空间。

一个由 Flutter GPU 驱动的自定义 2D 渲染器的很好的用例示例是依赖骨骼网格变形的 2D 角色动画格式。Spine 2D 就是一个很好的例子。这种骨骼网格解决方案通常具有操纵骨骼层次结构中的平移、旋转和缩放属性的动画剪辑,每个顶点都有一些相关的"骨骼权重",用于确定哪些骨骼应该影响顶点以及影响程度。

使用 Canvas 解决方案(如 drawVertices),需要在 CPU 上对每个顶点应用骨骼权重变换。使用 Flutter GPU,骨骼变换可以以均匀数组或纹理采样器的形式提供给顶点着色器,允许根据骨骼的状态和每个顶点的骨骼权重在 GPU 上并行计算每个顶点的最终位置。

说了这么多,让我们通过一个温和的介绍开始使用 Flutter GPU 吧:绘制你的第一个三角形!

将 Flutter GPU 添加到你的项目中

#

首先,请注意 Flutter GPU 目前处于早期预览状态,可能容易发生 API 变化。目前的 API 已经可以实现很多功能,但有经验的图形工程师可能会注意到一些常见功能的缺失。未来几个月计划为 Flutter GPU 添加很多功能。

出于这些原因,我们强烈建议你在开发使用 Flutter GPU 的 package 时,暂时使用 主渠道的最新版本。如果你遇到任何意外行为、错误或有功能请求,请使用 GitHub 上的标准 Flutter 问题模板提交问题。所有与 Flutter GPU 相关的跟踪问题都被标记为 flutter-gpu 标签

因此,在尝试 Flutter GPU 之前,请通过运行以下命令将 Flutter 切换到主渠道。

shell
flutter channel main
flutter upgrade

现在创建一个新的 Flutter 项目。

shell
flutter create my_cool_renderer
cd my_cool_renderer

接下来,将 flutter_gpu SDK package 添加到你的 pubspec 中。

shell
flutter pub add flutter_gpu --sdk=flutter

构建和导入着色器包

#

为了使用 Flutter GPU 渲染任何内容,你需要编写一些 GLSL 着色器。Flutter GPU 的着色器与 Flutter 的 片段着色器 功能所使用的着色器有不同的语义,特别是在统一绑定方面。你还需要定义一个顶点着色器来配合片段着色器。

首先定义最简单的着色器。你可以将着色器放在项目的任何位置,但在这个例子中,创建一个 shaders 目录并在其中放置两个着色器:simple.vertsimple.frag

glsl
#version 320 es

in vec2 position;

void main() {
  gl_Position = vec4(position, 0.0, 1.0);
}

绘制三角形时,你将有一个定义每个顶点的数据列表。在这种情况下,它只列出了 2D 位置。对于这些顶点中的每一个,一个简单的顶点着色器将这些 2D 位置分配给裁剪空间输出内在变量 gl_Position

glsl
#version 320 es

out vec4 frag_color;

void main() {
  frag_color = vec4(0, 1, 0, 1);
}

片段着色器更简单;它输出一个 RGBA 颜色,范围从 (0, 0, 0, 0)(1, 1, 1, 1)。所以一切都会被着色为绿色。

好了,现在你有了着色器,使用 Flutter 的预先时间(AOT)着色器编译器编译它们。要为着色器包设置自动构建,我们建议使用 flutter_gpu_shaders package。

使用 pub 将 flutter_gpu_shaders 作为常规依赖项添加到你的项目中。

shell
flutter pub add flutter_gpu_shaders

Flutter GPU 着色器被打包成 .shaderbundle 文件,可以作为常规资源添加到你项目的资源库中。着色器包包含针对平台目标编译的着色器源代码。

接下来,创建一个着色器包清单文件,描述着色器包的内容。在项目的根目录中添加以下内容到 my_renderer.shaderbundle.json

json
{
    "SimpleVertex": {
        "type": "vertex",
        "file": "shaders/simple.vert"
    },
    "SimpleFragment": {
        "type": "fragment",
        "file": "shaders/simple.frag"
    }
}

着色器包中的每个条目都可以有任意名称。在这种情况下,名称是"SimpleVertex"和"SimpleFragment"。这些名称用于在你的应用中查找着色器。

接下来,使用 flutter_gpu_shaders package 构建 shaderbundle。你可以添加一个钩子,通过启用实验性的“平台原生资源 (Native Assets)”功能来自动触发构建。使用以下命令启用平台原生资源并安装 native_assets_cli package。

shell
flutter config --enable-native-assets
flutter pub add native_assets_cli

启用“平台原生资源”功能后,在 hook 目录下添加一个 build.dart 脚本,它将自动触发构建着色器包。

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');
  });
}

进行此更改后,当 Flutter 工具构建项目时,buildShaderBundleJson 会构建着色器包并将结果输出到 package 根目录下的 build/shaderbundles/my_renderer.shaderbundle

着色器包格式本身与你使用的特定 Flutter 版本相关联,并且可能随时间而变化。如果你正在创作一个构建着色器包的 package,不要将生成的 .shaderbundle 文件放进你的源代码库。相反,使用构建钩子来自动化构建过程(如前面所解释的)。

这样,使用你的库的开发者将始终使用正确格式构建新的着色器包!

现在你已经自动构建了着色器包,像常规资源一样导入它。在项目的 pubspec.yaml 中添加一个 assets 条目:

yaml
flutter:
  assets:
    - build/shaderbundles/

将来,“平台原生资源”功能将允许构建钩子将数据资源附加到 package 中。一旦发生这种情况,就不再需要在构建钩子旁边添加资源导入规则了。

接下来,添加一些代码在运行时加载着色器。创建 lib/shaders.dart 并添加以下代码。

dart
import 'package:flutter_gpu/gpu.dart' as gpu;

const String _kShaderBundlePath =
    'build/shaderbundles/my_renderer.shaderbundle';

// 这是 Flutter GPU 着色器运行时库的单例 getter
gpu.ShaderLibrary? _shaderLibrary;
gpu.ShaderLibrary get shaderLibrary {
  if (_shaderLibrary != null) {
    return _shaderLibrary!;
  }
  _shaderLibrary = gpu.ShaderLibrary.fromAsset(_kShaderBundlePath);
  if (_shaderLibrary != null) {
    return _shaderLibrary!;
  }

  throw Exception("加载着色器包失败!($_kShaderBundlePath)");
}

这段代码为 Flutter GPU 着色器运行时库创建了一个单例 getter。第一次访问 shaderLibrary 时,使用内置资源包初始化运行时着色器库,使用 gpu.ShaderLibrary.fromAsset(shader_bundle_path)

现在项目已经设置好使用 Flutter GPU 着色器了。是时候渲染那个三角形了!

绘制你的第一个三角形

#

在本指南中,你将创建一个 RGBA Flutter GPU Texture 和一个 RenderPass,将 Texture 作为颜色输出附加。然后,你将使用 Canvas.drawImage 在 widget 中渲染 Texture

为了简洁起见,你将不遵循最佳实践,而是为每一帧重新构建所有资源。

只要在分配 Texture 时将其标记为"着色器可读",你就可以将其转换为 dart:ui.Image。要在 widget 树中显示渲染结果,将其绘制到 dart:ui.Canvas 上!

你可以通过使用自定义画家来搭建 widget 树来访问 Canvas。将 lib/main.dart 的内容替换为以下内容:

dart
import 'dart:typed_data';

import 'package:flutter/material.dart';
import 'package:flutter_gpu/gpu.dart' as gpu;

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 三角形示例',
      home: CustomPaint(
        painter: TrianglePainter(),
      ),
    );
  }
}

class TrianglePainter extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    // 这里是魔法发生的地方!
    
    print('默认颜色格式:' +
        gpu.gpuContext.defaultColorFormat.toString());
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
}

现在,运行应用。请记住,Flutter GPU 目前需要 启用 Impeller。所以你必须使用支持 Impeller 的平台。在本指南中,我将以 macOS 为目标。

shell
flutter run -d macos --enable-impeller

如果 Flutter GPU 正常工作,你应该会看到默认颜色格式打印到控制台。

flutter: Default color format: PixelFormat.b8g8r8a8UNormInt

如果未启用 Impeller,在尝试访问 gpu.gpuContext 时会抛出异常。

Exception: Flutter GPU requires the Impeller rendering backend to be enabled.
异常:Flutter GPU 需要启用 Impeller 渲染后端。

The relevant error-causing widget was:
  CustomPaint
相关错误来源的 widget 是:
  CustomPaint

为了简单起见,从现在开始你只会修改 paint 方法。

首先,创建一个 Flutter GPU Texture,清除它,然后通过将其绘制到 Canvas 上来显示它。

创建一个与 Canvas 大小相同的 Texture。必须选择一个 StorageMode。在这种情况下,你将 Texture 标记为 devicePrivate,因为你只会使用从设备(GPU)访问纹理内存的指令。

dart
final texture = gpu.gpuContext.createTexture(gpu.StorageMode.devicePrivate,
    size.width.toInt(), size.height.toInt())!;

如果要通过从主机(CPU)上传来覆盖纹理的数据,则改用 StorageMode.hostVisible

第三个可用选项是 StorageMode.deviceTransient,这对于不需要超过单个 RenderPass 生命周期的附件很有用(因此它们可以只存在于平铺内存中,不需要由 VRAM 分配支持)。通常,深度/模板纹理符合这个标准。

接下来,定义一个 RenderTarget。渲染目标包含一组"附件",描述每个片段的内存布局及其在 RenderPass 开始和结束时的设置/拆卸行为。

本质上,RenderTargetRenderPass 的可重用描述符。

现在,定义一个只包含一个颜色附件的非常简单的 RenderTarget

dart
final renderTarget = gpu.RenderTarget.singleColor(
  gpu.ColorAttachment(texture: texture, clearValue: Colors.lightBlue));

注意这段代码将 clearValue 设置为浅蓝色。每个附件都有一个 LoadAction 和一个 StoreAction,它们决定在通道开始和结束时应该对附件的短暂平铺内存发生什么。

默认情况下,颜色附件设置为 LoadAction.clear(将平铺内存初始化为给定颜色)和 StoreAction.store(将结果保存到附加纹理的 VRAM 分配中)。

现在,创建一个 CommandBuffer,使用早前的 RenderTarget 从中生成一个 RenderPass,然后立即提交 CommandBuffer 以清除纹理。

dart
final commandBuffer = gpu.gpuContext.createCommandBuffer();
final renderPass = commandBuffer.createRenderPass(renderTarget);

commandBuffer.submit();

剩下的就是将初始化的纹理绘制到 Canvas 上!

dart
final image = texture.asImage();
canvas.drawImage(image, Offset.zero, Paint());

OK,屏幕上现在有了一个连接到 RenderPass 的结果,你已经准备好开始绘制三角形了。为此,我们接下来设置以下内容:

  1. 从我们的着色器创建的 RenderPipeline,以及
  2. 包含我们几何体的 GPU 可访问缓冲区(三个顶点位置)。

创建 RenderPipeline 很容易。你只需要将库中的顶点和片段着色器组合起来。

dart
final vert = shaderLibrary['SimpleVertex']!;
final frag = shaderLibrary['SimpleFragment']!;
final pipeline = gpu.gpuContext.createRenderPipeline(vert, frag);

现在是几何体。回想一下,"SimpleVertex" 着色器只有一个输入:in vec2 position。所以,要绘制三个顶点,你需要三组两个浮点数。

dart
final vertices = Float32List.fromList([
  -0.5, -0.5, // 左下
   0.5, -0.5, // 右下
   0.0,  0.5, // 顶部中心
]);
final verticesDeviceBuffer = gpu.gpuContext
    .createDeviceBufferWithCopy(ByteData.sublistView(vertices))!;

剩下的就是绑定新资源并调用 renderPass.draw() 来完成绘制调用的记录。

dart
renderPass.bindPipeline(pipeline);

final verticesView = gpu.BufferView(
  verticesDeviceBuffer,
  offsetInBytes: 0,
  lengthInBytes: verticesDeviceBuffer.sizeInBytes,
);
renderPass.bindVertexBuffer(verticesView, 3);

renderPass.draw();

如果你启动应用,你现在应该会看到一个绿色三角形!

耶,你使用 Flutter、Dart 和一点 GLSL 构建了一个渲染器!

无论这是你第一次渲染三角形还是你是一个经验丰富的图形老手,我希望你会继续探索 Flutter GPU 并查看我们正在开发的 package,比如 Flutter Scene。

将来,我们希望发布初学者友好的 codelabs,深入探讨 Flutter GPU 的默认行为和最佳实践。我们还没有讨论顶点属性布局、纹理绑定、统一和对齐要求、管线混合、深度和模板附件、透视校正等等!

在此之前,我建议探索 Flutter Scene 作为如何使用 Flutter GPU 的更全面示例。

使用 Flutter Scene 进行 3D 渲染

#

Flutter Scene(package flutter_scene)是一个由 Flutter GPU 驱动的新的 3D 场景图 package,使 Flutter 开发者能够导入动画 glTF 模型并渲染实时 3D 场景。

其目的是提供一个 package,使在 Flutter 中构建交互式 3D 应用和游戏变得容易。

这个 package 最初是作为 dart:ui 扩展开始的,用于一个用 C++ 编写并直接构建到 Flutter 的原生运行时中的 3D 渲染器,但它已经被重写为针对 Flutter GPU 的更灵活接口。

与 Flutter GPU API 本身一样,Flutter Scene 目前处于早期预览状态,需要 启用 Impeller。Flutter Scene 通常会跟上 Flutter GPU API 的破坏性变更,因此强烈建议在尝试 Flutter Scene 时使用 主渠道

接下来,让我们用 Flutter Scene 制作一个应用!

设置 Flutter Scene 项目

#

由于强烈建议在 主渠道 上使用 Flutter Scene,首先切换到它。

shell
flutter channel main
flutter upgrade

接下来,创建一个新的 Flutter 项目。

shell
flutter create my_3d_app
cd my_3d_app

Flutter Scene 依赖于实验性的“平台原生资源”功能来自动构建着色器。你稍后将使用“平台原生资源”来设置自动导入 3D 模型以供 Flutter Scene 使用。

使用以下命令启用“平台原生资源”功能:

shell
flutter config --enable-native-assets

最后,将 Flutter Scene 添加为项目依赖项。

你还需要在与 Flutter Scene 的 API 交互时使用几个 vector_math 构造,所以也添加 vector_math package。

shell
flutter pub add flutter_scene vector_math

接下来,导入一个 3D 模型!

导入 3D 模型

#

首先,你需要一个 3D 模型来渲染。对于本指南,你将使用一个常见的 glTF 示例资源:DamagedHelmet.glb。这是它的样子。

原始的 Damaged Helmet 模型由 theblueturtle_ 在 2016 年创建(许可证:CC BY-NC 4.0 International)。转换后的 glTF 版本由 ctxwing 在 2018 年创建(许可证:CC BY 4.0 International

你可以从托管在 GitHub 上的 glTF 示例资源仓库 获取它。将 DamagedHelmet.glb 放在你的项目根目录中。

shell
curl -O https://raw.githubusercontent.com/KhronosGroup/glTF-Sample-Models/main/2.0/DamagedHelmet/glTF-Binary/DamagedHelmet.glb

像大多数实时 3D 渲染器一样,Flutter Scene 在内部使用专门的 3D 模型格式。你可以使用 Flutter Scene 的离线导入工具将标准 glTF 二进制文件(.glb 文件)转换为这种格式。

flutter_scene_importer package 作为常规依赖项添加到项目中。

shell
flutter pub add flutter_scene_importer

添加这个 package 使得可以使用 dart run 手动调用导入器。

shell
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 安装为常规项目依赖项。

shell
flutter pub add native_assets_cli

现在你可以编写构建钩子了。创建 hook/build.dart,内容如下:

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',
    ]);
  });
}

使用 flutter_scene_importer 中的 buildModels 实用程序,提供要构建的模型列表。路径相对于项目的构建根目录。

当 Flutter 工具构建项目时,buildModels 现在会构建着色器包并将结果输出到 package 根目录下的 build/models/DamagedModel.model

导入的模型格式本身与你使用的特定 Flutter Scene 版本相关联,并且会随时间变化。在创作使用 Flutter Scene 的应用或库时,不要将生成的 .model 文件检入你的源代码树。相反,使用构建钩子从源模型生成它们(如前面所解释的)。

这样,随着你随时间升级 Flutter Scene,你将始终构建具有正确格式的新 .model 文件!

接下来,像常规资源一样导入模型。在项目的 pubspec.yaml 中添加一个 assets 条目。

yaml
flutter:
  assets:
    - build/models/

将来,“平台原生资源”功能将允许构建钩子将数据资源附加到 package 中。一旦发生这种情况,就不再需要在构建钩子旁边添加资源导入规则了。

渲染 3D 场景

#

现在是应用代码的时候了。

首先,创建一个有状态 widget 以在帧之间保持 Scene

你将根据时间进行动画,所以将 SingleTickerProviderStateMixin 添加到状态中,并添加一个 elapsedSeconds 成员。

dart
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: '我的 3D 应用',
      home: Placeholder(),
    );
  }
}

运行应用作为烟雾测试,确保没有错误。记住要 启用 Impeller

shell
flutter run -d macos --enable-impeller

在继续之前,为动画设置 ticker。在 MyAppState 中重写 initState 以调用 createTicker

dart
  @override
  void initState() {
    createTicker((elapsed) {
      setState(() {
        elapsedSeconds = elapsed.inMilliseconds.toDouble() / 1000;
      });
    }).start();

    super.initState();
  }

只要 widget 可见,ticker 回调就会在每一帧被调用。调用 setState 触发此 widget 在每一帧重建。

接下来,加载你之前放在项目中的 3D 模型并将其添加到 Scene 中。

使用 Node.fromAsset 从资源包中加载模型。将以下代码放在 initState 中。

dart
    Node.fromAsset('build/models/DamagedHelmet.model').then((model) {
      model.name = 'Helmet';
      scene.add(model);
    });

Node.fromAsset 从资源包中异步反序列化模型,并在准备好添加到场景时解析返回的 Future<Node>

MyAppState.initState 现在应该如下所示:

dart
  @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!要做到这一点,使用 Scene.render,它接受一个 UI Canvas、一个 Flutter Scene Camera 和一个大小。

访问 Canvas 的一种方法是创建一个 CustomPainter

dart
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 添加到源代码树中。

dart
  @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: '我的 3D 应用',
      home: CustomPaint(painter: painter),
    );
  }

这段代码指示相机在一个连续的圆中移动,但始终面向原点。

最后,启动应用!

shell
flutter run -d macos --enable-impeller

这是我们组合在一起的完整源代码。

dart
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: '我的 3D 应用',
      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 最初作为 Impeller 中的 C++ 组件开始,与 2D Canvas 渲染器一起,只有一个瘦薄的 dart:ui 扩展。当我构建它时,我已经知道 Flutter Engine 不会是它的最终目标。

33D 渲染器的架构决策选择非常广泛,没有单一的通用 3D 渲染器能够很好地解决每一个用例。“通用”和“高性能”通常是对立的目标。

充其量只能保证对所有东西都够用,但却无法在任何方面做到卓越。

在渲染性能领域,情况甚至更糟。针对于某一个用例进行优化通常意味着降低另一个用例的性能。

简而言之,我们不可能发布一个能够解决所有人的所有需求(用例)的通用级 3D 渲染器,但是,通过提供构建自己解决方案所需的底层 API(Flutter GPU),并在其上构建一个易于 Flutter 社区检查和修改的有用的通用 3D 渲染器(Flutter Scene),我们正在为 Flutter 开发者们构建一个低淘汰风险、高回报的空间。

我迫不及待地想看看你们将用这些新功能创造出什么。敬请期待 Flutter Scene 的未来版本。还有很多内容正在路上。

同时,我要回去工作了。

不久后再见。:)