Sitemap

Flutter is Google's UI framework for crafting high-quality native interfaces on iOS, Android, web, and desktop. Flutter works with existing code, is used by developers and organizations around the world, and is free and open source. Learn more at https://flutter.dev

Getting started with Flutter GPU

Image for: Build custom renderers and render 3D scenes in Flutter.
17 min readAug 6, 2024

--

Getting started with Flutter GPU

Ooh shiny. This is a ray-marched signed distance field. You could render this using Flutter GPU, but it’s perfectly possible to do so with a custom fragment shader as well.

Getting started with Flutter GPU

Add Flutter GPU to your project

flutter channel main
flutter upgrade
flutter create my_cool_renderer
cd my_cool_renderer
flutter pub add flutter_gpu --sdk=flutter

Build and import shader bundles.

// Copy into: shaders/simple.vert

in vec2 position;

void main() {
gl_Position = vec4(position, 0.0, 1.0);
}
// Copy into: shaders/simple.frag

out vec4 frag_color;

void main() {
frag_color = vec4(0, 1, 0, 1);
}
flutter pub add flutter_gpu_shaders
{
"SimpleVertex": {
"type": "vertex",
"file": "shaders/simple.vert"
},
"SimpleFragment": {
"type": "fragment",
"file": "shaders/simple.frag"
}
}
flutter config --enable-native-assets
flutter pub add native_assets_cli
// 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');
});
}
flutter:
assets:
- build/shaderbundles/
// 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)");
}

Drawing your first triangle

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: Default color format: PixelFormat.b8g8r8a8UNormInt
Exception: Flutter GPU requires the Impeller rendering backend to be enabled.

The relevant error-causing widget was:
CustomPaint
final texture = gpu.gpuContext.createTexture(gpu.StorageMode.devicePrivate,
size.width.toInt(), size.height.toInt())!;
final renderTarget = gpu.RenderTarget.singleColor(
gpu.ColorAttachment(texture: texture, clearValue: Colors.lightBlue));
final commandBuffer = gpu.gpuContext.createCommandBuffer();
final renderPass = commandBuffer.createRenderPass(renderTarget);
// ... draw calls will go here!
commandBuffer.submit();
final image = texture.asImage();
canvas.drawImage(image, Offset.zero, Paint());
final vert = shaderLibrary['SimpleVertex']!;
final frag = shaderLibrary['SimpleFragment']!;
final pipeline = gpu.gpuContext.createRenderPipeline(vert, frag);
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.bindPipeline(pipeline);

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

renderPass.draw();

3D rendering with Flutter Scene

Set up a Flutter Scene project

flutter channel main
flutter upgrade
flutter create my_3d_app
cd my_3d_app
flutter config --enable-native-assets
flutter pub add flutter_scene vector_math

Import a 3D model

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)
curl -O https://raw.githubusercontent.com/KhronosGroup/glTF-Sample-Models/main/2.0/DamagedHelmet/glTF-Binary/DamagedHelmet.glb
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"
flutter pub add native_assets_cli
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:
assets:
- build/models/

Rendering a 3D scene

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(),
);
}
}
flutter run -d macos --enable-impeller
  @override
void initState() {
createTicker((elapsed) {
setState(() {
elapsedSeconds = elapsed.inMilliseconds.toDouble() / 1000;
});
}).start();

super.initState();
}
    Node.fromAsset('build/models/DamagedHelmet.model').then((model) {
model.name = 'Helmet';
scene.add(model);
});
  @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();
}
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;
}
  @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’s great future ahead

--

--

Published in Flutter

Image for: Published in Flutter

Flutter is Google's UI framework for crafting high-quality native interfaces on iOS, Android, web, and desktop. Flutter works with existing code, is used by developers and organizations around the world, and is free and open source. Learn more at https://flutter.dev

Responses (14)

Image for: Responses (14)