A model on screen
Loading a model in Orblit is one field. OrblitObject.mesh takes a path to a
.gltf, .glb, .fbx or .obj, and leaving it null gets you the built-in
cube. On a desktop the path is a file on disk. In a browser, or from inside an
Android app’s archive, it’s a name you’ve handed bytes over under, which is
covered below.
import 'package:flutter/material.dart';import 'package:orblit_filament/orblit_filament.dart';import 'package:vector_math/vector_math_64.dart' hide Colors;
void main() => runApp(const ModelApp());
class ModelApp extends StatefulWidget { const ModelApp({super.key});
@override State<ModelApp> createState() => _ModelAppState();}
class _ModelAppState extends State<ModelApp> { /// An absolute path. A relative one is relative to wherever the application /// happened to be launched from, which is not somewhere you can rely on. static const _model = '/Users/you/models/crate.glb';
/// What the renderer said about a file it could not read, if anything. String? _note;
@override Widget build(BuildContext context) { return MaterialApp( home: Scaffold( body: Stack( children: [ Positioned.fill( child: OrblitView( scene: _scene(), // Load failures come back here rather than going to a log. // A game can name the asset it is missing; an editor can put // it in front of whoever has to fix it. onSceneNotes: (notes) { final first = notes.entries.isEmpty ? null : '${notes.entries.first.key}: ' '${notes.entries.first.value}'; if (first != _note) setState(() => _note = first); }, ), ), if (_note != null) Positioned( left: 16, bottom: 16, child: Text( _note!, style: const TextStyle(color: Colors.orange, fontSize: 12), ), ), ], ), ), ); }
OrblitScene _scene() { return OrblitScene( camera: OrblitCamera( position: Vector3(0, 2.5, 7), target: Vector3(0, 0.5, 0), ), objects: [ // Three objects naming the same file. It is parsed once; the second // and third get an instance of it rather than another copy. for (var i = 0; i < 3; i++) OrblitObject( key: 100 + i, transform: Matrix4.identity() ..setTranslation(Vector3((i - 1) * 2.4, 0, 0)) ..rotateY(i * 0.4), // Used only if the file will not load and the cube stands in. colour: Vector3(0.85, 0.42, 0.16), mesh: _model, ), OrblitObject( key: 90, transform: Matrix4.identity() ..setTranslation(Vector3(0, -1.2, 0)) ..scaleByDouble(12, 0.1, 12, 1), colour: Vector3(0.18, 0.19, 0.21), castShadows: false, ), ], lights: [ OrblitLight( key: 110, kind: OrblitLightKind.directional, direction: Vector3(-0.5, -1, -0.4)..normalize(), intensity: 76000, ), ], sky: OrblitSky(ambient: 14000), ); }}A file is parsed once, however many objects name it
Section titled “A file is parsed once, however many objects name it”A scene arrives on every frame of a drag, and re-reading a glTF at sixty hertz isn’t a slow path, it’s an unusable one. So the path is the cache key. The first object naming a file causes it to be read, and every object after that gets an instance.
Which means the way to draw a hundred of something is to write the same path a hundred times. There is no separate “load this, keep the handle” step, and therefore no handle to leak, forget or free at the wrong moment.
A missing file draws the cube
Section titled “A missing file draws the cube”Not an exception, not a black screen, and not an empty space. A file that
cannot be read is drawn as the placeholder cube, in whatever colour the
object carried, and the reason comes back through onSceneNotes.
That is deliberate. A throw would take down a frame for one bad asset in a scene of five hundred; silence would have you hunting for a thing that is not there. A cube where the model should be is legible from across the room.
Where the bytes come from
Section titled “Where the bytes come from”A browser has no disk, and an Android app’s assets are inside its archive, so a path means nothing to either. Hand the bytes over under a name instead, and use the name wherever a path would go:
import 'package:flutter/services.dart';import 'package:orblit_filament/orblit_filament.dart';
Future<String> provideCrate() async { final data = await rootBundle.load('assets/crate.glb'); final name = OrblitResources.nameFor('crate.glb'); await OrblitResources.provide(name, data.buffer.asUint8List()); return name; // use as OrblitObject.mesh}Every renderer in the app looks for a provided name before it looks at the
disk, and that covers textures, environments, decal pictures, splat captures
and the files a .gltf names beside itself as well as meshes. A name stands
for bytes that don’t change: providing it again won’t reload anything already
loaded, so give changed bytes a new name, with a content hash in it. A scene
can arrive before its bytes do. It draws without them, and picks them up when
they arrive.
What loads and what doesn’t
Section titled “What loads and what doesn’t”glTF and .glb load as they are. A .fbx or .obj is converted to glTF when
it’s first named, using ufbx, then kept for the
life of the process. What couldn’t be carried across is said in the scene
notes. Models has the details, including an offline
converter and a list of what gets lost.
There’s no asset store, and nothing exports FBX.
Materials come with the file. A model arrives with its own, rather than being
tinted by whatever colour the object carried, which is why colour above is
described as the fallback rather than the appearance. A file’s clips, skins,
material variants and lights are all reachable too, and
Models covers them.
