Your first scene
Here is a complete Orblit application. It draws a cube on a ground plane, lights it, and spins it.
import 'package:flutter/material.dart';import 'package:flutter/scheduler.dart';import 'package:orblit_filament/orblit_filament.dart';import 'package:vector_math/vector_math_64.dart' hide Colors;
void main() => runApp(const SpinApp());
class SpinApp extends StatefulWidget { const SpinApp({super.key});
@override State<SpinApp> createState() => _SpinAppState();}
class _SpinAppState extends State<SpinApp> with SingleTickerProviderStateMixin { late final Ticker _clock = createTicker((elapsed) { setState(() => _seconds = elapsed.inMicroseconds / 1e6); })..start();
double _seconds = 0;
@override void dispose() { _clock.dispose(); super.dispose(); }
@override Widget build(BuildContext context) { return MaterialApp( home: Scaffold( body: OrblitView(scene: _scene()), ), ); }
OrblitScene _scene() { return OrblitScene( camera: OrblitCamera( position: Vector3(4, 3, 6), target: Vector3(0, 0.5, 0), ), objects: [ OrblitObject( key: 1, transform: Matrix4.rotationY(_seconds) ..setTranslation(Vector3(0, 0.5, 0)), colour: Vector3(0.85, 0.42, 0.16), ), OrblitObject( key: 2, transform: Matrix4.identity() ..setTranslation(Vector3(0, -0.5, 0)) ..scaleByDouble(12, 1, 12, 1), colour: Vector3(0.18, 0.19, 0.21), ), ], lights: [ OrblitLight( key: 1, kind: OrblitLightKind.directional, direction: Vector3(-0.4, -1, -0.6)..normalize(), intensity: 100000, ), ], ); }}Four things in that are worth stopping on.
The scene is a value, not a graph
Section titled “The scene is a value, not a graph”_scene() builds a whole OrblitScene from scratch, sixty times a second.
That isn’t wasteful, and it isn’t a simplification for the sake of an example.
It’s how the engine is meant to be used.
There is no scene.add(cube), and no handle to a cube that you hold on to and
mutate. Every frame, you say what the scene contains in full, and the renderer
works out the difference. Because the objects carry keys, it knows that object
1 this frame is the same object 1 as last frame, and it only sends what
actually changed.
Which means the scene can’t drift out of sync with your game state, because it is your game state, read out. This is the same bargain Flutter makes about widgets, and it holds for the same reasons.
Stating a scene goes into what this costs and why it’s cheap.
Keys are yours to assign
Section titled “Keys are yours to assign”key: 1 and key: 2 are arbitrary integers you choose. They only have to be
stable across frames and unique within the scene. An object whose key changes
is a different object as far as the renderer is concerned: the old one gets
destroyed and a new one created. That’s occasionally what you want, and
usually a bug.
No mesh means the built-in cube
Section titled “No mesh means the built-in cube”OrblitObject.mesh takes a path to a .gltf, .glb, .fbx or .obj file,
or the name of bytes you’ve handed over.
Leaving it null gets you the built-in cube, which is why the example above needs no assets
at all. Scaled flat, the same cube is a perfectly good ground plane.
A file that can’t be read is drawn as the cube too, and the reason comes back
through OrblitView.onSceneNotes rather than being logged somewhere you’ll
never look:
return OrblitView( scene: _scene(), onSceneNotes: (notes) { // {'/path/to/thing.glb': 'no such file'} for (final note in notes.entries) { debugPrint('${note.key}: ${note.value}'); } },);Lights are in real units
Section titled “Lights are in real units”intensity: 100000 looks alarming until you know that a sun is stated in
lux, and that 100,000 lux is roughly what real daylight comes to. Point
and spot lights are in lumens, where a bright domestic bulb is about 1,600.
The camera has real units too: aperture, shutterSpeed and sensitivity,
defaulting to f/16, 1/125s and ISO 100, which is a sensible daylight exposure.
Between them, those three decide how bright the image is, exactly as they do
on a camera. If your scene comes out black, the light is too dim for the
exposure, and there are two ends you can fix that from.
This is the whole reason to use photometric units: the numbers transfer. A value you read off the box a light fitting came in is the value you type.
Lighting a scene covers this properly, including
orblit_light, which converts from the watts and metres an artist thinks in.
Making it interactive
Section titled “Making it interactive”Because the view is a widget, input is ordinary Flutter input. Nothing about this is engine-specific:
import 'dart:math' as math;
import 'package:flutter/gestures.dart';import 'package:flutter/material.dart';import 'package:orblit_filament/orblit_filament.dart';import 'package:vector_math/vector_math_64.dart' hide Colors;
class Orbit extends StatefulWidget { const Orbit({super.key});
@override State<Orbit> createState() => _OrbitState();}
class _OrbitState extends State<Orbit> { double _distance = 8; double _yaw = 0;
@override Widget build(BuildContext context) { return Listener( onPointerSignal: (event) { if (event is PointerScrollEvent) { setState(() => _distance += event.scrollDelta.dy * 0.01); } }, child: GestureDetector( onPanUpdate: (details) { setState(() => _yaw += details.delta.dx * 0.01); }, child: OrblitView( scene: OrblitScene( camera: OrblitCamera( position: Vector3( _distance * math.sin(_yaw), 3, _distance * math.cos(_yaw), ), target: Vector3.zero(), ), objects: [ OrblitObject( key: 1, transform: Matrix4.identity(), colour: Vector3(0.85, 0.42, 0.16), ), ], ), ), ), ); }}And because it’s a widget, it can be laid out. Put a panel beside it, overlap it, clip it to a rounded rectangle, animate its size: the 3D content takes part, rather than floating above the application in a window of its own.
