A physics backend for flutter_scene
built on ODE, the Open Dynamics Engine. It
implements the PhysicsSimulation contract of
package:scene: rigid bodies, motorised
hinges, fixed joints, contacts with friction, sleeping bodies and raycasts.
ODE has been used for two decades in robotics simulators. This backend was born inside Nairda, and the binaries shipped here are the same ones that run there.
To our knowledge (September 2026) it is the first physics backend for flutter_scene written outside the flutter_scene project, and the first one based on ODE. The other backends are flutter_scene_rapier and flutter_scene_box3d.
- Nairda is an educational robotics app. ODE simulates the robots children build, with servos, DC motors, wheels and bumpers, on Android, iOS, macOS, Windows and the web. Nairda's virtual robot contests also run this physics on a server, through the pure-Dart core and without Flutter.
- Proteus is an artificial-life experiment: an amoeba whose body is simulated by ODE. A small decision model moves its pseudopods, and a language model narrates its inner state as a "Cartesian theatre" that steers the amoeba towards homeostasis. It uses the pure-Dart core for the simulation and flutter_scene for its 3D viewer. Code on GitHub, live demo at https://proteus.nairda-back.com.
Documentación en español: README.es.md.
| Platform | How ODE gets there |
|---|---|
Android (arm64-v8a) |
prebuilt libode.so, 747 KB |
| iOS, macOS | the podspec compiles the bundled C++ sources |
| Windows (x64) | windows/CMakeLists.txt compiles the same sources with MSVC |
| Web | prebuilt ode.wasm, 122 KB |
| Linux | not bundled: isAvailable is false (you can load your own libode) |
Nothing crashes where ODE is missing. OdeSimulation.isAvailable and
OdeSimulation.unavailableReason tell you before you create anything.
dependencies:
flutter_scene_ode: ^0.1.0For a program without Flutter (a server, a CLI, a headless evaluator), use
the pure-Dart core instead:
flutter_scene_ode_core. It is the same code; this plugin
only adds the native binaries and the web module.
import 'package:flutter_scene_ode/flutter_scene_ode.dart';
import 'package:scene/physics.dart';
import 'package:vector_math/vector_math.dart';
Future<void> main() async {
// On the web this downloads ode.wasm from the asset bundle.
await FlutterSceneOde.ensureAvailable();
if (!OdeSimulation.isAvailable) {
print('No physics here: ${OdeSimulation.unavailableReason}');
return;
}
final sim = OdeSimulation();
final ground = sim.createBody(
target: SimplePoseTarget(translation: Vector3(0, -1, 0)),
type: BodyType.fixed,
);
sim.createColliders(ground, BoxShape(halfExtents: Vector3(50, 1, 50)));
final box = sim.createBody(
target: SimplePoseTarget(translation: Vector3(0, 7, 0)),
type: BodyType.dynamic_,
additionalMass: 7.0, // grams
);
sim.createColliders(box, BoxShape(halfExtents: Vector3(1.5, 1.25, 0.66)));
for (var t = 0.0; t < 6.0; t += sim.fixedTimestep) {
sim.step(sim.fixedTimestep);
}
final (position, _) = sim.readBodyPose(box);
print('resting at y = ${position.y}'); // 1.25
sim.dispose(); // required: the native world keeps the process alive
}Two runnable examples:
example/is a Flutter app: boxes, spheres and cylinders rain onto a floor, through flutter_scene'sPhysicsWorldcomponent.core/example/example.dartis the same idea in pure Dart, without Flutter.
The world is tuned for small robots: 1 unit ≈ 1 cm, masses in grams, gravity −9.81 along Y. The fixed step is 1/240 s with up to 16 substeps. The solver runs 40 QuickStep iterations with CFM 1e‑5 and ERP 0.2, and bodies go to sleep when they come to rest.
These values were measured, not guessed. With masses in kilograms, a servo of
a few grams leaves the solver's multipliers near the float epsilon and the
assembly never stops rocking. The reasons for every constant are written in
the comments of core/lib/src/ode/ode_simulation.dart.
| Feature | Status |
|---|---|
| Body types | dynamic, kinematic, fixed |
| Shapes | box, sphere, cylinder, flat compounds of those |
| Joints | hinge with motor and limits, fixed |
| Contacts | friction (average or minimum), mechanism groups that ignore self-contact |
| Queries | raycast, raycastAll, contact map between steps |
| Sleeping | automatic, with wakeBody / sleepBody |
| Web | the same Dart code, through a small dart:ffi shim over WebAssembly |
- Single precision only. The library checks the ABI at startup and refuses a double-precision build instead of reading garbage.
- No capsules. ODE is compiled without libccd, so capsule↔cylinder
collisions would pass through silently.
CapsuleShapethrows on purpose. - No cylinder↔cylinder contact, for the same reason.
- No restitution, triggers, CCD, axis locks, character controller or world
snapshots.
overlapSphere,overlapBoxandshapeCastreturn nothing yet. - Android ships
arm64-v8aonly. - The source comments are in Spanish. Pull requests that translate them are welcome.
NATIVE.md explains every step, in Spanish: the ODE 0.16.6
tarball and its SHA256, the compiler flags, why libccd is off, why the Apple
pod is a dynamic framework, and how the WebAssembly module is built with
Emscripten. The scripts are in tool/.
The tests run on the host in seconds, without a phone:
tool/build_vendored_host.sh # prints the path of libode
cd core
ODE_LIBRARY_PATH=<that path> dart test # 59 tests (and `flutter test` at the root: 64)BSD 3-Clause, see LICENSE. ODE is used and redistributed under its BSD-style license, see LICENSE-ODE-BSD.txt and THIRD_PARTY_NOTICES.md.
