Flutter 3.24 版本引入了 Flutter GPU 概念的新底层图形 API flutter_gpu
,还有 flutter_scene
的 3D 渲染支持库,它们目前都是预览阶段,只能在 main channel 上体验,并且依赖 Impeller 的实现。
Flutter GPU 是 Flutter 内置的底层图形 API,它可以通过编写 Dart 代码和 GLSL 着色器在 Flutter 中构建和集成自定义渲染器,而无需 Native 平台代码。
目前 Flutter GPU 处于早期预览阶段并只提供基本的光栅化 API,但随着 API 接近稳定,会继续添加和完善更多功能。
详细说,Flutter GPU 是 Impeller 对于 HAL 的一层很轻的包装,并搭配了关于着色器和管道编排的自动化能力,也通过 Flutter GPU 就可以使用 Dart 直接构建自定义渲染器。
Flutter GPU 和 Impeller 一样,它的着色器也是使用 impellerc 提前编译,所以 Flutter GPU 也只支持 Impeller 的平台上可用。
❝ Impeller 的 HAL 和 Flutter GPU 都没打算成为类似 WebGPU 这样的正式标准,相反,Flutter GPU 主要是由 Flutter 社区开发和发展,专职为了 Flutter 服务,所以不需要考虑「公有化」的兼容问题。
在 Flutter GPU 上,可直接从 Dart 与 Impeller 的 HAL 对话,甚至 Impeller Scene API(3D)也将作为重写的一部分出现。
❝ 说人话就是,可以用 Dart 通过 Flutter GPU 直接构建自定义渲染效果,未来直接支持 3D
可能有的人对于 Impeller 的整体结构和 HAL 还很模式无法理解,那么这里我们简单过一下:
在 Framework 上层,我们知道 Widget -> Element -> RenderObject -> Layer 这样的过程,而后其实流程就来到了 Flutter 自定义抽象的 DisplayList
DisplayList 帮助 Flutter 在 Engine 做了接耦,从而让 Flutter 可以在 skia 和 Impeller 之间进行的切换
之后 Impeller 架构的顶层是 Aiks,这一层主要作为绘图操作的高级接口,它接受来自 Flutter 框架的命令,例如绘制路径或图像,并将这些命令转换为一组更精细的 “Entities”,然后转给下一层。
Entities Framework,它是 Impeller 架构的核心组件,当 Aiks 处理完命令时生成 Entities 后,每一个 Entity 其实就是渲染指令的独立单元,其中包含绘制特定元素的所有必要信息(编码位置、旋转、缩放、content object),此时还不能直接作用于 GPU
HAL(Hardware Abstraction Layer) 则为底层图形硬件提供了统一的接口,抽象了不同图形 API 的细节,该层确保了 Impeller 的跨平台能力,它将高级渲染命令转换为低级 GPU 指令,充当 Impeller 渲染逻辑和设备图形硬件之间的桥梁。
所以 HAL 它包装了各种图形 API,以提供通用的设备作业调度接口、一致的资源管理语义和统一的着色器创作体验,而对于 Impeller , Entities (2D renderer) 和 Scene (3D renderer) 都是直接通过 HAL 对接,甚至可以认为,Impeller 的 HAL 抽象并统一了 Metal 和 Vulkan 的常见用法和相似结构。
❝ Unity 现在也有在 C# 直接向用户公开其 HAL 版本,称为 "Scriptable Render Pipeline" ,并提供了两个基于该 API 构建的默认渲染器 "Universal RP" / "High Definition RP" 用于服务不同的场景,所以 Unity 开发可以从使用这些渲染器去进行修改或扩展一些特定渲染需求。
而在 Flutter 的设计上,Flutter GPU 会作为 Flutter SDK 的一部分,并以 flutter_gpu
的 Dart 包的形式提供使用。
当然,Flutter GPU 由 Impeller 支持,但重要的是要记住它不是 Impeller ,Impeller 的 HAL 是私有内部代码与 Flutter GPU 的要求非常不同, Impeller 的私有 HAL 和 Flutter GPU 的公共 API 设计之间是存在一定差异化实现,而前面的流程,如 Scene (3D renderer) ,也可以被调整为基于 Flutter GPU 的全新模式实现。
而通过 Flutter GPU,如曾经的 Scene (3D renderer) 支持,也可以被调整为基于 Flutter GPU 的全新模式实现,因为 Flutter GPU 的 API 允许完全控制渲染通道附件、顶点阶段和数据上传到 GPU。这种灵活性对于创建复杂的渲染解决方案(从 2D 角色动画到复杂的 3D 场景)至关重要。
Flutter GPU 支持的自定义 2D 渲染器的一个很好的用例:依赖于骨骼网格变形的 2D 角色动画格式。
Spine 2D 就是一个很好的例子,骨骼网格解决方案通常具有动画剪辑,可以按层次结构操纵骨骼的平移、旋转和缩放属性,并且每个顶点都有几个相关的“bone weights”,这些权重决定了哪些骨骼应该影响顶点以及影响程度如何。
使用像 drawVertices
这样的 Canvas 解决方案,需要在 CPU 上对每个顶点应用骨骼权重变换,而 使用 Flutter GPU,骨骼变换可以用统一数组或纹理采样器的形式发送到顶点着色器,从而允许根据骨架状态和每个顶点的 “bone weights” 在 GPU 上并行计算每个顶点的最终位置。
使用 Flutter GPU
首先你需要在最新的 main channel 分支,然后通过 flutter pub add flutter_gpu --sdk=flutter
将 flutter_gpu SDK 包添加到你的 pubspec。
为了使用 Flutter GPU 渲染内容,你会需要编写一些 GLSL 着色器,Flutter GPU 的着色器与 Flutter 的 fragment shader 功能所使用的着色器具有不同的语义,特别是在统一绑定方面,还需要定义一个顶点(vertex)着色器来与 fragment shader 一起使用,然后配合 gpu.ShaderLibrary
等 API 就可以直接实现 Flutter GPU 渲染。
当然,本篇不会介绍详细的 API 使用 ,这里只是单纯做一个简单的介绍,目前 Flutter GPU 进行光栅化的简单流程如下:
获取 GPUContext。
GpuContext.createCommandBuffer
创建一个CommandBuffer
CommandBuffer.createRenderPass
创建一个RenderPass
使用各种方法设置状态/管道并绑定资源
RenderPass
附加绘图命令
RenderPass.draw
CommandBuffer
使用CommandBuffer.submit
(异步)提交绘制,所有RenderPass
会按照其创建顺序进行编码
·····
///导入 flutter_gpu
import 'package:flutter_gpu/gpu.dart' as gpu;
ByteData float32(List
values) { return Float32List.fromList(values).buffer.asByteData(); } ByteData float32Mat(Matrix4 matrix) { return Float32List.fromList(matrix.storage).buffer.asByteData(); } class TrianglePainter extends CustomPainter { TrianglePainter(this.time, this.seedX, this.seedY); double time; double seedX; double seedY; @override void paint(Canvas canvas, Size size) { /// Allocate a new renderable texture. final gpu.Texture? renderTexture = gpu.gpuContext.createTexture( gpu.StorageMode.devicePrivate, 300, 300, enableRenderTargetUsage: true, enableShaderReadUsage: true, coordinateSystem: gpu.TextureCoordinateSystem.renderToTexture); if (renderTexture == null) { return; } final gpu.Texture? depthTexture = gpu.gpuContext.createTexture( gpu.StorageMode.deviceTransient, 300, 300, format: gpu.gpuContext.defaultDepthStencilFormat, enableRenderTargetUsage: true, coordinateSystem: gpu.TextureCoordinateSystem.renderToTexture); if (depthTexture == null) { return; } /// Create the command buffer. This will be used to submit all encoded /// commands at the end. final commandBuffer = gpu.gpuContext.createCommandBuffer(); /// Define a render target. This is just a collection of attachments that a /// RenderPass will write to. final renderTarget = gpu.RenderTarget.singleColor( gpu.ColorAttachment(texture: renderTexture), depthStencilAttachment: gpu.DepthStencilAttachment(texture: depthTexture), ); /// Add a render pass encoder to the command buffer so that we can start /// encoding commands. final encoder = commandBuffer.createRenderPass(renderTarget); /// Load a shader bundle asset. final library = gpu.ShaderLibrary.fromAsset('assets/TestLibrary.shaderbundle')!; /// Create a RenderPipeline using shaders from the asset. final vertex = library['UnlitVertex']!; final fragment = library['UnlitFragment']!; final pipeline = gpu.gpuContext.createRenderPipeline(vertex, fragment); encoder.bindPipeline(pipeline); /// (Optional) Configure blending for the first color attachment. encoder.setColorBlendEnable(true); encoder.setColorBlendEquation(gpu.ColorBlendEquation( colorBlendOperation: gpu.BlendOperation.add, sourceColorBlendFactor: gpu.BlendFactor.one, destinationColorBlendFactor: gpu.BlendFactor.oneMinusSourceAlpha, alphaBlendOperation: gpu.BlendOperation.add, sourceAlphaBlendFactor: gpu.BlendFactor.one, destinationAlphaBlendFactor: gpu.BlendFactor.oneMinusSourceAlpha)); /// Append quick geometry and uniforms to a host buffer that will be /// automatically uploaded to the GPU later on. final transients = gpu.HostBuffer(); final vertices = transients.emplace(float32(
[ -0.5, -0.5, // 0, 0.5, // 0.5, -0.5, // ])); final color = transients.emplace(float32(
[0, 1, 0, 1])); // rgba final mvp = transients.emplace(float32Mat(Matrix4( 1, 0, 0, 0, // 0, 1, 0, 0, // 0, 0, 1, 0, // 0, 0, 0.5, 1, // ) * Matrix4.rotationX(time) * Matrix4.rotationY(time * seedX) * Matrix4.rotationZ(time * seedY))); /// Bind the vertex data. In this case, we won't bother binding an index /// buffer. encoder.bindVertexBuffer(vertices, 3); /// Bind the host buffer data we just created to the vertex shader's uniform /// slots. Although the locations are specified in the shader and are /// predictable, we can optionally fetch the uniform slots by name for /// convenience. final mvpSlot = pipeline.vertexShader.getUniformSlot('mvp')!; final colorSlot = pipeline.vertexShader.getUniformSlot('color')!; encoder.bindUniform(mvpSlot, mvp); encoder.bindUniform(colorSlot, color); /// And finally, we append a draw call. encoder.draw(); /// Submit all of the previously encoded passes. Passes are encoded in the /// same order they were created in. commandBuffer.submit(); /// Wrap the Flutter GPU texture as a ui.Image and draw it like normal! final image = renderTexture.asImage(); canvas.drawImage(image, Offset(-renderTexture.width / 2, 0), Paint()); } @override bool shouldRepaint(covariant CustomPainter oldDelegate) { return true; } } class TrianglePage extends StatefulWidget { const TrianglePage({super.key}); @override State createState() => _TrianglePageState(); } class _TrianglePageState extends State
{ Ticker? tick; double time = 0; double deltaSeconds = 0; double seedX = -0.512511498387847167; double seedY = 0.521295573094847167; @override void initState() { tick = Ticker( (elapsed) { setState(() { double previousTime = time; time = elapsed.inMilliseconds / 1000.0; deltaSeconds = previousTime > 0 ? time - previousTime : 0; }); }, ); tick!.start(); super.initState(); } @override Widget build(BuildContext context) { return Column( children: [ Slider( value: seedX, max: 1, min: -1, onChanged: (value) => {setState(() => seedX = value)}), Slider( value: seedY, max: 1, min: -1, onChanged: (value) => {setState(() => seedY = value)}), CustomPaint( painter: TrianglePainter(time, seedX, seedY), ), ], ); } }
GpuContext 是分配所有 GPU 资源并调度 GPU 的存在,而 GpuContext 仅有启用 Impeller 时才能访问。
DeviceBuffer 和 Texture 就是 GPU 拥有的资源,可以通过 GPUContext 创建获取,如 createDeviceBuffer
和 createTexture
:
DeviceBuffer 简单理解就是在 GPU 上分配的简单字节串,主要用于存储几何数据(索引和顶点属性)以及统一数据
Texture 是一个特殊的设备缓冲区
CommandBuffer 用于对 GPU 上的异步执行进行排队和调度工作。
RenderPass 是 GPU 上渲染工作的顶层单元。
RenderPipeline 提供增量更改绘制所有状态以及附加绘制调用的方法如 RenderPass.draw()
可以想象,通过 Flutter GPU,Flutter 开发者可以更简单地对 GPU 进行更精细的控制,通过与 HAL 直接通信,创建 GPU 资源并记录 GPU 命令,从而最大限度的发挥 Flutter 的渲染能力。
另外,对于 3D 支持的 Flutter Scene , 可以通过使用 native-assets 来设置 Flutter Scene 的 3D 模型自动导入,通过导入编译模型 .model 之后,就可以通过 Dart 实现一些 3D 的渲染。
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: '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 GPU 和 Flutter Scene 的支持还十分有限,但是借助 Impeller ,Flutter 开启了新的可能,可以说是,Flutter 团队完全掌控了渲染堆栈,在除了自定义更丰富的 2D 场景之外,也为 Flutter 开启了 3D 游戏的可能,2023 年 Flutter Forward 大会的承诺,目前正在被落地实现。
❝ 详细 API 使用例子可以参看 :https://medium.com/flutter/getting-started-with-flutter-gpu-f33d497b7c11
如果你对 Flutter Impeller 和其着色器感兴趣,也可以看:
热门跟贴