Skip to content

Your first scene

This page builds a complete, runnable Flutter app that renders a 3D cube with an orbiting camera. It uses only the engine’s built-in geometry and material, so there is no asset to load yet.

This is the scene you are about to build, running live in your browser.

A unit cube with debug face colors, orbited by the camera.

If you have not added the package, start with Installation.

Copy this into lib/main.dart and run it. It is a whole Flutter app in one file.

import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter_scene/scene.dart';
import 'package:vector_math/vector_math.dart' as vm;
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
home: Scaffold(body: FirstScene()),
);
}
}
class FirstScene extends StatefulWidget {
const FirstScene({super.key});
@override
State<FirstScene> createState() => _FirstSceneState();
}
class _FirstSceneState extends State<FirstScene> {
// Constructing a Scene starts loading the engine's shared resources.
final Scene scene = Scene();
@override
void initState() {
super.initState();
// A cube mesh, made from built-in geometry and an unlit material.
final mesh = Mesh(
CuboidGeometry(vm.Vector3(1, 1, 1), debugColors: true),
UnlitMaterial(),
);
scene.add(Node(mesh: mesh));
}
@override
Widget build(BuildContext context) {
return SceneView(
scene,
// The camera orbits the origin once per second.
cameraBuilder: (elapsed) {
final t = elapsed.inMicroseconds / 1e6;
return PerspectiveCamera(
position: vm.Vector3(sin(t) * 5, 2, cos(t) * 5),
target: vm.Vector3(0, 0, 0),
);
},
);
}
}

Run it with the flags from Installation.

Terminal window
flutter run --enable-flutter-gpu --enable-impeller # native
flutter run -d chrome # web

You should see a multicolored cube turning as the camera circles it.

  • A Scene holds the scene graph and does the rendering. Constructing one also kicks off loading the engine’s shared resources in the background (see Warming up below).
  • A Mesh pairs a Geometry (the shape) with a Material (the shading). Here that is a unit CuboidGeometry and an UnlitMaterial. The debugColors flag tints each face so the cube is easy to read without any lighting set up.
  • A Node places the mesh in the scene graph, and scene.add attaches it under the scene root.
  • SceneView is the bridge to Flutter. It owns the per-frame ticker and the render call. Each frame it calls cameraBuilder with the elapsed time, so returning a camera computed from elapsed animates the orbit with no manual timer.

The first time the engine renders, it loads a few shared resources, such as shaders and a lookup texture. Until that finishes, SceneView skips frames and shows nothing. You do not have to manage this. Build your scene whenever you like and the content appears on its own once the engine is ready, usually within a frame or two.

If you would rather show your own placeholder during that brief warm-up, await Scene.initializeStaticResources() and swap in the scene when it completes.

@override
Widget build(BuildContext context) {
return FutureBuilder<void>(
future: Scene.initializeStaticResources(),
builder: (context, snapshot) {
if (snapshot.connectionState != ConnectionState.done) {
// Your placeholder while the engine warms up.
return const Center(child: CircularProgressIndicator());
}
return SceneView(scene, cameraBuilder: _orbit);
},
);
}

This is optional. Without it the scene still renders correctly; you just see whatever is behind the view during the warm-up rather than a placeholder.

SceneView already animates the camera. To animate the cube itself, attach a Component. A component is behavior bound to a node, with an update hook the engine calls every frame.

class SpinComponent extends Component {
SpinComponent(this.radiansPerSecond);
final double radiansPerSecond;
@override
void update(double deltaSeconds) {
node.localTransform =
node.localTransform * vm.Matrix4.rotationY(radiansPerSecond * deltaSeconds);
}
}

Attach it when you build the node.

final node = Node(mesh: mesh)..addComponent(SpinComponent(1.5));
scene.add(node);

The cube now spins on its own, independent of the camera.