diff --git a/src/content/posts/dart-3-5.md b/src/content/posts/dart-3-5.md index aa3b5b722d..14f0e5ca08 100644 --- a/src/content/posts/dart-3-5.md +++ b/src/content/posts/dart-3-5.md @@ -11,13 +11,13 @@ toc: true ## Dart 3.5 中的新功能 -Dart 3.5 包含了一些新功能,下面会详细介绍。同时还对核心库 API 做了一些小的改动,以及约 10 个非常小的破坏性变更,具体可以查看[更新日志](https://github.com/dart-lang/sdk/blob/master/CHANGELOG.md#350)。 +Dart 3.5 包含了一些新功能,下面会详细介绍。同时还对核心库 API 做了一些小的改动,以及约 10 个非常小的破坏性变更,具体可以查看 [更新日志](https://github.com/dart-lang/sdk/blob/master/CHANGELOG.md#350)。 ## Web 平台和 JS 互操作性 -在 Dart 3.4 和 Flutter 3.22 中,我们引入了[将 Flutter Web 应用编译为 WebAssembly](https://docs.flutter.cn/platform-integration/web/wasm) 的支持。编译为 WebAssembly 需要使用我们新的 [Dart 到 JS 互操作模型](https://dart.cn/interop/js-interop),该模型之前还处于预览阶段。从 Dart 3.5 开始,它现在被认为是稳定和完整的,我们还将 [package:web](https://pub.flutter-io.cn/packages/web) 中的浏览器 API 绑定(它取代了旧的 `dart:html` 库)更新到了 1.0 版本。 +在 Dart 3.4 和 Flutter 3.22 中,我们引入了 [将 Flutter Web 应用编译为 WebAssembly](https://docs.flutter.cn/platform-integration/web/wasm) 的支持。编译为 WebAssembly 需要使用我们新的 [Dart 到 JS 互操作模型](https://dart.cn/interop/js-interop),该模型之前还处于预览阶段。从 Dart 3.5 开始,它现在被认为是稳定和完整的,我们还将 [package:web](https://pub.flutter-io.cn/packages/web) 中的浏览器 API 绑定(它取代了旧的 `dart:html` 库)更新到了 1.0 版本。 -我们鼓励所有 Web package 的作者[迁移到 package:web](https://dart.cn/interop/js-interop/package-web)。我们计划在下一个 Dart 版本中弃用旧的互操作 API (dart:html、dart:js、package:js 等),并在明年晚些时候完全停止支持它们。我们邀请你在[跟踪 issue](https://github.com/dart-lang/sdk/issues/56358)中对这个计划提供反馈。我们还计划更新 pub.dev package 管理器的[评分系统](https://pub.flutter-io.cn/help/scoring),为支持新互操作模型的 Web package 加分。 +我们鼓励所有 Web package 的作者 [迁移到 package:web](https://dart.cn/interop/js-interop/package-web)。我们计划在下一个 Dart 版本中弃用旧的互操作 API (dart:html、dart:js、package:js 等),并在明年晚些时候完全停止支持它们。我们邀请你在 [跟踪 issue](https://github.com/dart-lang/sdk/issues/56358)中对这个计划提供反馈。我们还计划更新 pub.dev package 管理器的 [评分系统](https://pub.flutter-io.cn/help/scoring),为支持新互操作模型的 Web package 加分。 我们还添加了[一个新的 lint 规则](https://dart.cn/tools/linter-rules/invalid_runtime_check_with_js_interop_types)(代码风格检查规则),用于验证你的代码是否正确使用了新的 JS 互操作类型。我们建议你在迁移 Web package 时将这个 lint 规则添加到 `analysis_options.yaml` 文件中。 @@ -27,15 +27,15 @@ Dart 3.5 包含了一些新功能,下面会详细介绍。同时还对核心 C 互操作是通过我们的 [FFI](https://dart.cn/interop/c-interop) (外部函数接口)库实现的,我们已经支持了几年。在 Dart 3.5 中,我们做了一些增量改进,支持直接将 Dart `TypedData` 对象的指针传递给 FFI,避免了先将内存从 Dart 复制到原生代码的需要([详情](https://github.com/dart-lang/sdk/issues/44589))。 -Java 和 Kotlin 互操作是通过 [JNIgen](https://pub.flutter-io.cn/packages/jnigen) 生成器(目前处于预览阶段)实现的,它可以自动创建通过 Java 原生接口(JNI,Java Native Interface)从 Dart 调用 Java 和 Kotlin 的绑定代码。我们改进了性能,并添加了对 Java 异常和 Kotlin 顶级函数的支持。我们还停止了对之前的[基于 C 的绑定](https://github.com/dart-lang/native/issues/660)的支持,因为替代的纯 Dart 绑定现在具有可比的性能和功能,而且使用起来更加简单。详情请参见[更新日志](https://pub.flutter-io.cn/packages/jnigen/changelog)。 +Java 和 Kotlin 互操作是通过 [JNIgen](https://pub.flutter-io.cn/packages/jnigen) 生成器(目前处于预览阶段)实现的,它可以自动创建通过 Java 原生接口(JNI,Java Native Interface)从 Dart 调用 Java 和 Kotlin 的绑定代码。我们改进了性能,并添加了对 Java 异常和 Kotlin 顶级函数的支持。我们还停止了对之前的 [基于 C 的绑定](https://github.com/dart-lang/native/issues/660) 的支持,因为替代的纯 Dart 绑定现在具有可比的性能和功能,而且使用起来更加简单。详情请参见 [更新日志](https://pub.flutter-io.cn/packages/jnigen/changelog)。 -Objective-C 互操作建立在 FFI 和我们的 [FFIgen](https://pub.flutter-io.cn/packages/ffigen) 生成器(目前处于预览阶段)之上。我们添加了对 Objective-C 协议和常见类型(如 `NSString`)的支持。有关使用 FFIgen 构建的大型 package 的示例,请参见 [cupertino_http](https://github.com/dart-lang/http/tree/master/pkgs/cupertino_http),它与 Apple 的 URL 加载系统网络库进行互操作。 +Objective-C 互操作建立在 FFI 和我们的 [FFIgen](https://pub.flutter-io.cn/packages/ffigen) 生成器(目前处于预览阶段)之上。我们添加了对 Objective-C 协议和常见类型 (如 `NSString`) 的支持。有关使用 FFIgen 构建的大型 package 的示例,请参见 [cupertino_http](https://github.com/dart-lang/http/tree/master/pkgs/cupertino_http),它与 Apple 的 URL 加载系统网络库进行互操作。 在接下来的版本中,我们将继续投资进一步的互操作性——既包括完善上述提到的库,也包括支持 Swift。详情请参见下面的路线图部分。 ## Pub.dev package 仓库 -Pub.dev 是我们的 package 仓库,社区可以在这里分享和查找具有丰富功能的 package。我们在这方面做了一些改进。首先,我们改进了对[**主题**](https://dart.cn/tools/pub/pubspec#topics)的支持:package 作者可以通过这种机制为他们的 package 添加所属类别的标签(例如 widget)。我们现在[整合](https://github.com/dart-lang/pub-dev/blob/master/doc/topics.yaml)了涵盖同一类别但措辞略有不同的常见主题(例如 widgets 与 widget)。 +Pub.dev 是我们的 package 仓库,社区可以在这里分享和查找具有丰富功能的 package。我们在这方面做了一些改进。首先,我们改进了对 [**主题**](https://dart.cn/tools/pub/pubspec#topics) 的支持——package 作者可以通过这种机制为他们的 package 添加所属类别的标签(例如 widget)。我们现在 [整合](https://github.com/dart-lang/pub-dev/blob/master/doc/topics.yaml) 了涵盖同一类别但措辞略有不同的常见主题(例如 widgets 与 widget)。 其次,我们添加了一个新的 `pub unpack` 命令。这提供了一种快速简便的方法来将 package 下载到你的文件系统中。例如,如果你想在本地机器上运行 package 的示例程序,可以使用这个命令: @@ -60,11 +60,11 @@ Current process path: /Users/mit/tmp/path-1.9.0/example "单一代码库"(monorepo)是一种常见的方式,用于将一组相关 package 和应用的源代码结构化到单个代码库中,例如 Flutter 的 [packages repo](https://github.com/flutter/packages/tree/main)。单一代码库不仅仅是为了方便将所有源代码"放在一起",还可以是确保代码库中的各个 package 和应用相互兼容的关键工具。 -我们一直听到在大型单一代码库中工作的开发者反馈,我们的工具,特别是分析器的性能可能不太理想。我们对这些问题的分析表明,根本原因是我们最终为每个 package 及其所有依赖项加载了多个重叠的分析上下文,导致单一代码库中每个 package 的分析在内存中同时存在多个副本。我们认为根本的解决方案是为这样的代码库中每个依赖项的版本创建一个单一的、共享的解析,我们正在通过一个名为 workspaces 的新 pub 功能来实现这种能力。我们将在下一个 Dart 版本中分享更多相关内容,但现在你可以看看这是如何[最近应用](https://github.com/flutter/engine/pull/54157/files)到 Flutter 引擎代码库的。 +我们一直听到在大型单一代码库中工作的开发者反馈,我们的工具,特别是分析器的性能可能不太理想。我们对这些问题的分析表明,根本原因是我们最终为每个 package 及其所有依赖项加载了多个重叠的分析上下文,导致单一代码库中每个 package 的分析在内存中同时存在多个副本。我们认为根本的解决方案是为这样的代码库中每个依赖项的版本创建一个单一的、共享的解析,我们正在通过一个名为 workspaces 的新 pub 功能来实现这种能力。我们将在下一个 Dart 版本中分享更多相关内容,但现在你可以看看这是如何 [最近应用](https://github.com/flutter/engine/pull/54157/files) 到 Flutter 引擎代码库的。 ## Pub.dev package 仓库 -pub.dev package 仓库的用户长期以来一直要求改进对每个 package 的[使用/下载](https://github.com/dart-lang/pub-dev/issues/2714)情况的度量。这对 package 作者来说可能很有帮助,作为他们的工作为多少用户带来好处的信号,对 package 消费者来说也是其他开发者正在使用哪些 package 的信号。我们很高兴地分享,我们在这个功能上取得了很好的进展,希望在年底前提供预览版。 +pub.dev package 仓库的用户长期以来一直要求改进对每个 package 的 [使用/下载](https://github.com/dart-lang/pub-dev/issues/2714) 情况的衡量。这对 package 作者来说可能很有帮助,作为他们的工作为多少用户带来好处的信号,对 package 消费者来说也是其他开发者正在使用哪些 package 的信号。我们很高兴地分享,我们在这个功能上取得了很好的进展,希望在年底前提供预览版。 ## Dart 原生互操作 @@ -74,15 +74,17 @@ pub.dev package 仓库的用户长期以来一直要求改进对每个 package ## 原生互操作和原生源代码的打包 -在许多情况下,直接互操作用于调用操作系统中存在的 API,这意味着这些 API 在那些宿主平台上始终可用。然而,在某些情况下,Dart 互操作的代码是不直接包含在宿主上的原生源代码,这给使用这种互操作的 package 作者带来了一个实际挑战:如何在不将大量手动步骤推给 package 的消费者的情况下,打包和构建那些原生源代码?为了支持这一点,我们正在探索一个[原生资产系统](https://github.com/dart-lang/sdk/issues/50565),它可以支持发布包含原生源代码的 Dart package,同时提供一个标准化的协议,使 `dart` 和 `flutter` CLI 工具能够自动化构建和打包这些源代码。我们设想这将启用一组新的互操作用例,同时为使用依赖原生源代码的 package 的开发者提供简单的用户体验。 +在许多情况下,直接互操作用于调用操作系统中存在的 API,这意味着这些 API 在那些宿主平台上始终可用。然而,在某些情况下,Dart 互操作的代码是不直接包含在宿主上的原生源代码,这给使用这种互操作的 package 作者带来了一个实际挑战:如何在不将大量手动步骤推给 package 的消费者的情况下,打包和构建那些原生源代码? + +为了支持这一点,我们正在探索一个 [平台原生资源](https://github.com/dart-lang/sdk/issues/50565) 的新特性,它可以支持发布包含原生源代码的 Dart package,同时提供一个标准化的协议,使 `dart` 和 `flutter` CLI 工具能够自动化构建和打包这些源代码。我们设想这将启用一组新的互操作用例,同时为使用依赖原生源代码的 package 的开发者提供简单的用户体验。 ## Dart 语言和宏 -Dart 语言和编译器团队目前的大部分时间都花在推进非常大的语言特性宏上,我们在 [Dart 3.4 博文](https://medium.com/dartlang/dart-3-4-bd8d23b4462a)中介绍过这个特性。正如我们当时所说,这是一项巨大的工作,有可能在热重载等一些核心用例中造成回归,所以我们采取了一种彻底的方法,可能还需要几个季度的进一步工作才能分享下一步的细节。 +Dart 语言和编译器团队目前的大部分时间都花在推进非常大的语言特性宏上,我们在 [Dart 3.4 博文](https://medium.com/dartlang/dart-3-4-bd8d23b4462a) 中介绍过这个特性。正如我们当时所说,这是一项巨大的工作,有可能在热重载等一些核心用例中造成回归,所以我们采取了一种彻底的方法,可能还需要几个季度的进一步工作才能分享下一步的细节。 -除了宏之外,我们还同时在探索一些其他较小的语言特性,如 [Dart 语言漏斗](https://github.com/orgs/dart-lang/projects/90/views/1)中所记录的。 +除了宏之外,我们还同时在探索一些其他较小的语言特性,如 [Dart 语言漏斗](https://github.com/orgs/dart-lang/projects/90/views/1) 中所记录的。 -从去年秋天开始,我们一直在重写 Dart 格式化程序。旧的设计运行良好多年,但随着 Flutter 的成功,我们希望转向[一种新的风格](https://github.com/dart-lang/dart_style/issues/1253),更适合 Flutter 用户经常编写的那种声明式代码。旧的格式化程序无法产生那种输出。重写工作即将完成,很快就会发布。如果你想试用,可以传递实验标志 `tall-style` ([标志说明](https://dart.cn/tools/experiment-flags))。如果你看到奇怪的输出,欢迎[提供反馈](https://github.com/dart-lang/dart_style/issues)。 +从去年秋天开始,我们一直在重写 Dart 格式化程序。旧的设计运行良好多年,但随着 Flutter 的成功,我们希望转向 [一种新的风格](https://github.com/dart-lang/dart_style/issues/1253),更适合 Flutter 用户经常编写的那种声明式代码。旧的格式化程序无法产生那种输出。重写工作即将完成,很快就会发布。如果你想试用,可以传递实验标志 `tall-style` ([标志说明](https://dart.cn/tools/experiment-flags))。如果你看到奇怪的输出,欢迎 [提供反馈](https://github.com/dart-lang/dart_style/issues)。 ## 结语 diff --git a/src/content/posts/flutter-gpu-101.md b/src/content/posts/flutter-gpu-101.md new file mode 100644 index 0000000000..49f974bea4 --- /dev/null +++ b/src/content/posts/flutter-gpu-101.md @@ -0,0 +1,739 @@ +--- +title: Flutter GPU 入门:使用自定义渲染器和 3D 渲染 +toc: true +--- + +Flutter 3.24 版本引入了一个名为 [Flutter GPU](https://github.com/flutter/engine/blob/main/docs/impeller/Flutter-GPU.md) 的新型底层图形 API。同时还有一个由 Flutter GPU 驱动的 3D 渲染库,叫做 [Flutter Scene](https://pub.flutter-io.cn/packages/flutter_scene) (package: `flutter_scene`)。Flutter GPU 和 Flutter Scene 目前都处于预览阶段,仅在 Flutter 的 [主渠道](https://docs.flutter.cn/release/upgrade#other-channels)上可用(因为依赖实验性功能),需要 [启用 Impeller](https://docs.flutter.cn/perf/impeller#availability),并且可能偶尔引入破坏性变更。 + +本文包含两个关于这些 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 渲染部分。 + +![哇,真漂亮。这是一个光线追踪的符号距离场。](https://files.flutter-io.cn/posts/flutter-cn/2024/flutter-gpu/0_hAqIOVkaI1IWnOHE.png) + +你可以使用 Flutter GPU 来渲染这个,不过使用 [自定义片段着色器](https://docs.flutter.cn/ui/design/graphics/fragment-shaders) 也完全可以实现。 + +## Flutter GPU 入门 + +Flutter GPU 是 Flutter 内置的底层图形 API。它允许你通过编写 Dart 代码和 GLSL 着色器在 Flutter 中构建和集成自定义渲染器。无需编写任何本机平台代码。 + +目前,Flutter GPU 处于早期预览阶段,提供了基本的光栅化 API,但随着 API 接近稳定版,将继续添加和完善更多功能。 + +Flutter GPU 还需要 [启用 Impeller](https://docs.flutter.cn/perf/impeller#availability)。这意味着它只能在 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 吧:绘制你的第一个三角形! + +![](https://files.flutter-io.cn/posts/flutter-cn/2024/flutter-gpu/0_JEI3fLDGcRHWKruT.png) + +## 将 Flutter GPU 添加到你的项目中 + +首先,请注意 Flutter GPU 目前处于早期预览状态,可能容易发生 API 变化。目前的 API 已经可以实现很多功能,但有经验的图形工程师可能会注意到一些常见功能的缺失。未来几个月计划为 Flutter GPU 添加很多功能。 + +出于这些原因,我们强烈建议你在开发使用 Flutter GPU 的 package 时,暂时使用 [主渠道](https://docs.flutter.cn/release/upgrade#other-channels)的最新版本。如果你遇到任何意外行为、错误或有功能请求,请使用 GitHub 上的标准 [Flutter 问题模板](https://github.com/flutter/flutter/issues/new/choose)提交问题。所有与 Flutter GPU 相关的跟踪问题都被标记为 [flutter-gpu 标签](https://github.com/flutter/flutter/labels/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 的 [片段着色器](https://docs.flutter.cn/ui/design/graphics/fragment-shaders) 功能所使用的着色器有不同的语义,特别是在统一绑定方面。你还需要定义一个顶点着色器来配合片段着色器。 + +首先定义最简单的着色器。你可以将着色器放在项目的任何位置,但在这个例子中,创建一个 `shaders` 目录并在其中放置两个着色器:`simple.vert` 和 `simple.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](https://pub.flutter-io.cn/packages/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 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](https://docs.flutter.cn/perf/impeller#availability)。所以你必须使用支持 Impeller 的平台。在本指南中,我将以 macOS 为目标。 + +```shell +flutter run -d macos --enable-impeller +``` + +![](https://files.flutter-io.cn/posts/flutter-cn/2024/flutter-gpu/0_lKTtaX2ih6dFpSMQ.png) + +如果 Flutter GPU 正常工作,你应该会看到默认颜色格式打印到控制台。 + +```console +flutter: Default color format: PixelFormat.b8g8r8a8UNormInt +``` + +如果未启用 Impeller,在尝试访问 `gpu.gpuContext` 时会抛出异常。 + +```console +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` 开始和结束时的设置/拆卸行为。 + +本质上,`RenderTarget` 是 `RenderPass` 的可重用描述符。 + +现在,定义一个只包含一个颜色附件的非常简单的 `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()); +``` + +![](https://files.flutter-io.cn/posts/flutter-cn/2024/flutter-gpu/0_ebUDtzQOuIGmdlop.png) + +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 构建了一个渲染器!](https://files.flutter-io.cn/posts/flutter-cn/2024/flutter-gpu/0_LWnGU5WPT_Eom0wJ.png) + +无论这是你第一次渲染三角形还是你是一个经验丰富的图形老手,我希望你会继续探索 Flutter GPU 并查看我们正在开发的 package,比如 Flutter Scene。 + +将来,我们希望发布初学者友好的 codelabs,深入探讨 Flutter GPU 的默认行为和最佳实践。我们还没有讨论顶点属性布局、纹理绑定、统一和对齐要求、管线混合、深度和模板附件、透视校正等等! + +在此之前,我建议探索 [Flutter Scene](https://github.com/bdero/flutter_scene) 作为如何使用 Flutter GPU 的更全面示例。 + +## 使用 Flutter Scene 进行 3D 渲染 + +Flutter Scene(package `flutter_scene`)是一个由 Flutter GPU 驱动的新的 3D 场景图 package,使 Flutter 开发者能够导入动画 glTF 模型并渲染实时 3D 场景。 + +其目的是提供一个 package,使在 Flutter 中构建交互式 3D 应用和游戏变得容易。 + +![](https://files.flutter-io.cn/posts/flutter-cn/2024/flutter-gpu/0_tC68CbPLef2rJp1e.png) + +这个 package 最初是作为 `dart:ui` 扩展开始的,用于一个用 C++ 编写并直接构建到 Flutter 的原生运行时中的 3D 渲染器,但它已经被重写为针对 Flutter GPU 的更灵活接口。 + +与 Flutter GPU API 本身一样,Flutter Scene 目前处于早期预览状态,需要 [启用 Impeller](https://docs.flutter.cn/perf/impeller#availability)。Flutter Scene 通常会跟上 Flutter GPU API 的破坏性变更,因此强烈建议在尝试 Flutter Scene 时使用 [主渠道](https://docs.flutter.cn/release/upgrade#other-channels)。 + +接下来,让我们用 Flutter Scene 制作一个应用! + +## 设置 Flutter Scene 项目 + +由于强烈建议在 [主渠道](https://docs.flutter.cn/release/upgrade#other-channels) 上使用 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](https://en.wikipedia.org/wiki/GlTF) 示例资源:[DamagedHelmet.glb](https://github.com/KhronosGroup/glTF-Sample-Assets/tree/main/Models/DamagedHelmet)。这是它的样子。 + +![](https://files.flutter-io.cn/posts/flutter-cn/2024/flutter-gpu/0_vVWRLxJ348tCxv7T.png) + +原始的 Damaged Helmet 模型由 theblueturtle\_ 在 2016 年创建(许可证:[CC BY-NC 4.0 International](https://creativecommons.org/licenses/by-nc/4.0/legalcode))。转换后的 glTF 版本由 ctxwing 在 2018 年创建(许可证:[CC BY 4.0 International](https://creativecommons.org/licenses/by/4.0/legalcode)) + +你可以从托管在 GitHub 上的 [glTF 示例资源仓库](https://raw.githubusercontent.com/KhronosGroup/glTF-Sample-Assets/main/Models/DamagedHelmet/glTF-Binary/DamagedHelmet.glb) 获取它。将 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 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 with SingleTickerProviderStateMixin { + double elapsedSeconds = 0; + Scene scene = Scene(); + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: '我的 3D 应用', + home: Placeholder(), + ); + } +} +``` + +运行应用作为烟雾测试,确保没有错误。记住要 [启用 Impeller](https://docs.flutter.cn/perf/impeller#availability)! + +```shell +flutter run -d macos --enable-impeller +``` + +![](https://files.flutter-io.cn/posts/flutter-cn/2024/flutter-gpu/0_74qs6ytcTjyVHwML.png) + +在继续之前,为动画设置 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`。 + +`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 +``` + +![](https://files.flutter-io.cn/posts/flutter-cn/2024/flutter-gpu/0__-OFc0vhBHAhrPrO.gif) + +这是我们组合在一起的完整源代码。 + +```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 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 开发者们构建一个低淘汰风险、高回报的空间。 + +![](https://files.flutter-io.cn/posts/flutter-cn/2024/flutter-gpu/1_jfeUgpEP9AgAz94yVxVW1g_gif.gif) + +我迫不及待地想看看你们将用这些新功能创造出什么。敬请期待 Flutter Scene 的未来版本。还有很多内容正在路上。 + +同时,我要回去工作了。 + +不久后再见。:) \ No newline at end of file diff --git a/src/content/posts/index.md b/src/content/posts/index.md index 19a534dd55..bf292b7cf8 100644 --- a/src/content/posts/index.md +++ b/src/content/posts/index.md @@ -8,6 +8,7 @@ title: 官方博客 ## 2024 年 +- [新] [Flutter GPU 入门:使用自定义渲染器和 3D 渲染](/posts/flutter-gpu-101) - [新] [Flutter 3.24 和 Dart 3.5 现已发布](/posts/flutter-3-24-dart-3-5) - [新] [Flutter 3.24 更新详解](/posts/whats-new-in-flutter-3-24) - [新] [Dart 3.5 更新详解](/posts/dart-3-5)