flutter3d
Showcase Changelog 63 packages API reference

Package index

62 packages and 18 applications, resolved as one pub workspace, so a single flutter pub get covers everything against one lock file. Every package but flutter3d_demo_content, the demos' own content, is published, which makes 61 of them on pub.dev. 59 of them carry the release candidate 1.0.0-rc.1, and any ^1.0.0-rc.1 on one of them resolves against every other and admits 1.0.0 when it follows. From 1.0.0 they follow strict semver, and the candidate already does, and SUPPORT.md says which platforms, backends and releases are supported. pad_input and flutter3d_pointer_lock keep a line of their own at 0.5.0, since neither names a sibling. Four names pub.dev still lists at 0.6.0 are no longer packages here: flutter3d_backend, flutter3d_screens, flutter3d_session and flutter3d_bridge were folded into flutter3d_app and flutter3d_game, and pub.dev marks each of them discontinued in favour of the package that took it in.

Engine #

flutter3d #

The renderer and everything that walks a scene. Import package:flutter3d/flutter3d.dart; it re-exports, by name, what an application spells of the core, of the graphics vocabulary (because a Material holds a SamplerDescriptor and a consumer should not need a second dependency to spell the type of something this package handed them) and of the foundation's value types. It does not re-export the simulation, the physics or the audio: a game names those packages, or starts from flutter3d_game, the one facade.

Depends on flutter3d_hardware and nothing below it. Does not re-export a backend, an application picks one by name, and that choice stays visible in its pubspec.

→ The frame · Scene graph · Geometry · Assets · The public API, area by area

flutter3d_core: the renderer with no Flutter behind it, and what it draws from #

The scene graph, the render graph, materials and animation, and two libraries under them that import on their own. package:flutter3d_core/geometry.dart is VertexLayout, MeshData, MeshBuilder, the shape generators a scene is sketched from, MeshGeometry, tangents by Lengyel, morph targets and MorphTexture, and Ray with the intersection arithmetic that reads a triangle. package:flutter3d_core/formats.dart is ModelDocument and everything a decoder fills in, SurfaceMaterial, MaterialDocument, LightingModel, AnimationClip and AnimationTrack, the readers and writers for glTF/GLB, OBJ, STL, USDZ, the engine's own .f3d and .fmat, and the FBX decoder that refuses with a reason. flutter3d exports all of it.

Plain Dart, because a modeller's document layer, a tool an agent starts with dart run and a service checking an upload all read a mesh or a .glb with no window. Geometry and formats were packages of their own until it turned out a library gives such a caller the same thing. What needs Flutter (the asset bundle, dart:ui image decoding and the defaults that reach for it) is the flutter3d shell.

→ Geometry · Assets

flutter3d_hardware: the HAL #

The hardware abstraction layer, and the vocabulary a backend implements: GraphicsDevice, CommandEncoder, PassState, TextureHandle, GeometryBuffer, samplers, formats, vertex layout specs, a render target pool. No implementation at all. Two rules, both checked: no flutter_gpu import ever, and no dart:ui apart from one member on GraphicsDevice that has to name it.

When a second backend arrives, this package does not change. A translation file appears in the new backend, and that is all.

→ The HAL and its four backends · Writing a backend

flutter3d_shaders #

The shared GLSL every backend comes from: Impeller compiles it into a bundle, WebGL translates it, WebGPU translates it down a second toolchain into WGSL, and the CPU backend transcribes it into Dart by hand. Only the first three are machines reading the text, which is what decides who can witness a GLSL edit: a software golden set that matched byte for byte says nothing about one, because those shaders were rewritten by a person, and flutter3d_cpu's own files say so at the head.

Backends, four implementations of the HAL #

An application depends on exactly one of these by name, and that dependency is the only place the choice is visible.

flutter3d_impeller: flutter_gpu #

flutter_gpu over Metal on Apple platforms and Vulkan elsewhere. The production backend, and the only one all three games run on. Ships tool/build_shaders.sh, which calls impellerc directly and prints the compiled binding table. GpuRenderBackend.open(width:, height:) is the entry point, the same shape every backend opens with, and nothing else in the stack names flutter_gpu.

flutter3d_webgl: WebGL2 #

The browser. WebGlDevice implements the HAL and has its own example.

Its real value is not that it runs on the web: it is the first thing that can tell you whether the HAL is a seam or a description of Impeller, because a fake backend can only confirm that an interface is callable, never that it is implementable.

Status: all three games run on it. lib/engine_shaders.dart holds every entry point in GLSL ES 3.00, generated from flutter3d_shaders by tool/generate_shaders.dart, and CI regenerates it and fails on the diff. It used to go stale without anyone noticing, and when cascaded shadows added a member to a uniform block the backend stopped drawing anything until somebody re-ran the generator. See the platformer demo.

flutter3d_webgpu: WebGPU #

The second browser backend. openWebGpu asks for an adapter and then a device, answers null where a browser has neither, and turns that null into the one StateError worth putting on a screen. Two barrels: flutter3d_webgpu.dart is pure Dart (the translation table and the pipeline signature, asserted on the VM in about a second), and flutter3d_webgpu_web.dart is everything that reaches for dart:js_interop.

It is the first backend whose shaders are not the same text the others read, and that is why it exists as a separate question rather than as a second browser option. WGSL is a different language and no browser takes SPIR-V, so tool/generate_shaders.dart runs the same manifest through glslangValidator and then naga --keep-coordinate-space, and CI regenerates the table and fails on the diff. All thirty-nine stages compile.

What it declines, by name rather than by silence: the blend constant, because WebGPU has "constant" and "one-minus-constant" and no colour/alpha split for BlendFactor.blendAlpha; and wireframe, because the API has no polygon fill mode. Those two are how the conformance suite comes back 41 of 41 with two honest refusals in it. Block compression left that list when the device started asking the adapter which of texture-compression-bc, -etc2 and -astc it carries and requesting exactly those, because asking for one it lacks rejects the device outright, which is why the intersection is taken and why the capability answers from what was granted. Three formats stay refused whatever the adapter: a8UNormInt and the two HDR ASTC layouts have no WebGPU spelling at all.

Status: it draws, and its conformance run is an ordinary test file, because Chrome has a real WebGPU device inside flutter test, the one thing this backend gets that Impeller cannot. It is on pub.dev with the rest of the set, and since 1.0.0 it is what a browser build opens first, with WebGL2 as the fallback where no adapter is handed out. --dart-define=FLUTTER3D_WEBGPU=false leaves it out: a build that can try both backends ships both, which measured 376,649 bytes of main.dart.js on the strategy demo, 14.9%.

flutter3d_cpu: software #

A rasteriser written in Dart. CpuDevice implements the same HAL, plus PNG output and Dart transcriptions of the shaders.

Not a fallback. Two hardware backends agreeing proves less than it looks like: both are driven by a C API and both rasterise on a GPU, so an assumption shared by graphics hardware would be invisible to the pair of them. This one shares nothing with either, no driver, no shading language, no command buffer.

It is how 96 golden scenes are checkable with no GPU in the room, and it is a dev dependency of every game because three shipped bugs would have been caught by rendering a single frame in a test. It stopped being only a dev dependency once flutter3d_app started reaching for it as the runtime fallback when Impeller will not start, which makes it a real production path now, if a last-resort one.

flutter3d_conformance #

The suite any fourth backend would have to pass before it counted as one, plus the cross-backend comparison with per-scene budgets.

Two tiers, and the split is a correction. The library said it was shader-free as a whole, and that stopped being true the day a check needed a pipeline: 33 of the 57 link stages and draw. coreChecks is what runs on clears, uploads and readback alone, so a backend can ask it before compiling a single shader; shaderChecks is the rest. A backend that believed the old promise would have met every one of those failures with nothing it could do about them yet. The phrasing here is the one tool/structure.dart holds to the lists themselves, so the sentence cannot go stale again without the scan saying so.

A plugin has a suite here too: package:flutter3d_conformance/plugins.dart checks its manifest, its switch at a step boundary, its determinism, every backend it declares and the budget it declares, and says whether it earns the conformance badge.

→ Writing a HAL backend · Plugin catalogue

flutter3d_testing #

Renders a scene through the software backend and compares it against a reference image, so a game built on the engine gets pixel regression tests on a machine with no display.

Two calls: renderFrame builds a frame from a scene and a camera, expectMatchesGolden compares it against a PNG and records one when there is none. The tolerance defaults to zero, and that is the point. A software rasteriser has no driver, no clock and no thread to disagree with itself, so the same scene drawn twice is the same bytes twice; a package telling its users to allow "a few pixels" would be telling them to stop watching.

Simulation #

flutter3d_sim #

The simulation: FixedStep, GameLoop, InputState and InputTape, the Level format and its validator, Breaches for the holes a blast leaves in it, LevelVisibility and the bake_visibility tool that writes one, Lightmap and its layout (the baker, flutter3d lightmap, is flutter3d_build's), EntityKind and the registry, mechanisms and movers, Actor / Brain / ActorSystem, the navigation grid, its flow fields and jump links and the Automap drawn from them, EcsWorld, Snapshot, Demo, RewindBuffer, GameRandom, StateDigest.

Plain Dart. No Flutter anywhere, and a scan says so: the simulation names no Flutter reads its lib/, test/ and bin/. The whole suite runs under dart test, and dart run flutter3d_sim:headless_run walks a body through the shipped crypt level with no Flutter SDK installed.

The reason is practical. A server that verifies a submitted run has to replay it through the same simulation the player ran, since a second copy of the game logic on the server proves nothing about the first, and that server is a Dart process in a container.

→ Simulation layer

flutter3d_physics #

Collision shapes, a uniform-grid broadphase, sweeps and rays that do not tunnel, CharacterController, Dynamics and RigidBody. Plain Dart, no Flutter, no renderer, runs under dart test.

flutter3d_matter #

What a world is made of, with nothing that moves it: WorldProperties (gravity, the air, the wind, the medium), the standard world's defaults, the constants of nature, and MaterialCatalog, the PhysicalMaterials an engine knows by id, each group citing its source. The physics, the audio core and the particles read it; the native core's C header is generated from it. How often a world is stepped is not here: that is the loop's WorldTiming.

→ Collision & physics

Genres #

flutter3d_game_shooter #

Weapons, an arsenal, hitscan, projectiles and blasts, monsters with a six-state brain, an inventory, gifts and pickups, and GameSimulation, a shooter's step order.

Two barrels: flutter3d_game_shooter.dart (nothing imports the renderer or names Flutter) and bridge.dart (WeaponView and the readouts, which do). The package ships no content: this repository's own roster, crypt and staging are in flutter3d_demo_content, which is not published.

→ What a shooter adds · Tutorial

flutter3d_game_platformer #

Runner and RunnerSettings, Surfaces, Purse, collectibles, checkpoints, hazards, springs, one-way platforms, conveyors, crumbling and breakable blocks, climbables, crates, Patrol, Leaper and Hunter enemies, FollowCamera, and PlatformerSimulation.

The simulation's barrel imports neither the renderer nor Flutter, so all of it runs in a test with no device; the readouts are in bridge.dart.

→ What a platformer adds · Tutorial

flutter3d_game_racing #

A car, a road that is a curve instead of a floor, and a lap that only counts when it was driven all the way round. TrackSpline and TrackField place a car by where it is on the curve instead of sweeping it against geometry, so a circuit can run to a kilometre. SphereVehicle and TireModel are the car; RaceState and RacingSimulation own laps, checkpoints and positions; AiDriver races against it and GhostRecorder/GhostTape/GhostPlayer record a lap as places rather than inputs, so it survives the car being retuned.

Draws nothing, for the same reason the other two genres don't: a car that understeers differently at a lower frame rate, or a lap counted twice in one step, appears nowhere in a screenshot and is reachable from a plain test.

→ What a racing game adds · Tutorial

flutter3d_game_strategy #

A camera over a map, orders given to a selection, and a crowd instead of a hero: the first genre here that is not one body under a camera bolted to it. A Heightfield of samples is the ground everything stands on, and MapCamera looks down at it; StrategyOrder is an abstract final family of MoveOrder, AssignOrder, AttackOrder and TrainOrder, which only this package grows, and a UnitOrder carries a goal, a formation and, when there is one, a target; Formation, Squad and a flow field shared by destination are how a crowd descends on the same place without a path apiece; UnitType is a row of numbers, so a worker, a soldier and a tank differ in what they measure rather than in what runs; ResourceNode, HarvestJob, Stockpile, Producer, Building and Bot give a side something to spend and a policy that plays it without a mouse; FogOfWar keeps what a side has explored apart from what it can see right now; Match and MatchGoal own the two ways to win; and OrderTape writes a whole match down as the orders that produced it.

Two probes ran before any of it was written, and both moved the design. Ten thousand agents step in under a millisecond and fifty thousand instanced units hold the display's refresh rate. So neither the crowd nor the draw call is the constraint, and it follows that separation applies to everybody, not only to what a camera can see (limiting it saves 717 microseconds and costs a run that replays the same way twice) and that the navigation grid is two metres rather than the quarter a shooter bakes.

It is what finally exercised three things the engine had built and no game had used: InstancedMeshNode, the picking pass, and the flow fields in flutter3d_sim's navigation.

Keeping the rules #

There is no package for this. The rules about how the repository is arranged (which package may name a genre, which file may reach a renderer, where a static final Vector3 may not be) live in tool/structure.dart and run before a build:

dart run tool/structure.dart

101 rules, no device, and all but two read source text in under a second with no pub get. The two that do not are the snapshots of the API and of the tools for agents, which parse every published package with the analyzer and so wait for a resolved workspace. They were a boundaries_test.dart in each package until thirteen packages of twenty-one turned out to have none, all thirteen clean and not one of them checked. A runner that walks packages/ covers a package the day it exists.

The detectors prove they fire before a single file is scanned, and a broken detector stops the run rather than letting 101 green scans be reported behind it. See Testing.

Assembling an application #

Two packages hold what an application is assembled from: flutter3d_app what every application on the engine repeats, and flutter3d_game what a game adds on top. The line between them is who uses them: the modeller, the level editor and the lessons stand on the first and never touch the second.

flutter3d_app #

One import for what every application shares:

import 'package:flutter3d_app/flutter3d_app.dart';

final device = await openDevice(width: 1280, height: 720);

Which graphics backend a build draws through: openDevice({required width, required height}) returns a GraphicsDevice. SceneSurface is the widget that hands a frame to Flutter, with its RenderSettings read from a function called once per frame rather than stored, so anything derived from the camera is derived after it moved. FrameClock is how long since the last frame, measured on the wall clock rather than read off the ticker: five applications had written that out, three said a sixtieth of a second on the first frame and one said nought, and a ticker's timestamp is the vsync a frame was aimed at, which steps in pairs when the GPU falls behind. WidgetSurface draws a live widget onto a quad in the scene.

Flutter3dView is the widget most applications start from: it opens the device, makes the renderer and the loop, and owns the frame clock, focus, pausing and teardown. Its options come in four final, const groups, DeviceSettings, ViewSettings, LoopSettings and PluginSettings, so an option a minor release adds lands in one group with a default. LoopSettings.simulation runs the simulation inline or in an isolate of its own (SimulationPlacement.isolate, on the web the same factory in this isolate). Once the device is open the view warms the scene's pipelines a slice at a time, shows a closable banner when the GPU backend would not start and the software rasteriser draws, and keeps an EngineLog. Scene3D is sugar on it and takes the same four groups, handed on whole, with SceneSurface beneath.

LevelLoader turns a document into a scene and a collision world, and its rebuildBrushes draws the walls again after a breach; VisibilityCuller applies the visibility table to the batches once a frame, and SharedMeshes keeps one mesh per shape. Storage and BinaryStorage keep a document where each platform keeps such things. A second entry point, package:flutter3d_app/native.dart, holds the one piece that needs a filesystem (an atomic document write), kept off the main barrel so a dart:io import never stops a web build compiling.

Three decisions, made three different ways. Web or native is a conditional export, picked at compile time, because flutter_gpu does not compile for the web and dart:js_interop does not compile for macOS. On the native half, Impeller or software is a runtime try/catch instead: GpuRenderBackend.open(width:, height:) is tried first, and a throw (Flutter GPU refusing to start on Skia, or a platform where Impeller was never enabled) falls back to flutter3d_cpu's CpuDevice, since flutter_gpu ships with the SDK and no compile-time check can see whether it will actually start. On the browser half, WebGPU or WebGL2 is the same shape of try/catch (navigator.gpu may be absent, or present and hand out no adapter on a blocklisted driver, and no dart.library.* check sees either), and it can be left out with --dart-define=FLUTTER3D_WEBGPU=false, because a probe that can call both openers keeps both backends reachable and dart2js ships what it can reach: 376,649 bytes of main.dart.js on the strategy demo, 14.9%, measured on two builds of the same checkout. It does not decide resolution or shadow budget, on purpose: fixedResolution reports whether the primary backend renders to a fixed internal target, and the size stays the application's own choice.

Deliberately not behind this barrel: flutter3d, flutter3d_sim, flutter3d_game and a genre package. Import them by name, so the choice is visible in the pubspec.

The four games, the modeller, the level editor and the lessons import it, and so do both seeds a new project starts from.

flutter3d_game #

What a game adds that is not a widget: keyboard, mouse, gamepad and touch input through one set of bindings, the settings a player chooses and the controller and file that keep them, the saves, the accessibility settings that read a MediaQuery, and the run. RunSession<L> is loading a level, restarting it, moving to the next, saving, resuming, and reporting how a run ended. It is an ordinary class that two of the three games wrap in a cubit, which the package neither knows nor requires.

The widgets are flutter3d_game_ui's. The settings panel, the touch controls, the credits, the automap and the loss screen were here until 1.0.0-rc.1, beside widgets of the same kind in flutter3d_game_ui that did the same jobs; now every widget is there, and this package keeps what they show and write. A game without a screen, a test or a replaying server, reads the settings and the run the same way.

ActorVisuals and FixtureVisuals draw what a level's simulation moves; what a torch looks like arrives through FixtureAppearance and ActorAppearance. SoundOcclusion is the walls between a sound and the ear, as a gain and a muffle.

It is the engine's one facade: it re-exports flutter3d through a list of every name flutter3d exports (so a name added there reaches the facade only once somebody lists it), the view from flutter3d_app, and by name the level, EngineLoop and InputState from flutter3d_sim, CollisionWorld, Collider and RigidDynamics from flutter3d_physics, and the listener, emitters and sound bank from flutter3d_audio_core, so a first game takes two imports. No other package re-exports another's API to save an import; a file that needs more of the simulation imports flutter3d_sim by name, and its package depends on it. tool/structure.dart holds the rule the run exists to keep: exactly one function per game turns a level document into a run. The platformer had six copies of that assembly before extraction, and they had drifted.

→ Assembling an application

Extensions #

flutter3d_particles #

One pool for the whole application, one draw call, plugged in through PassContributor. Emitters, affectors, curves and gradients, flipbooks, mesh particles, and ParticleGlow, the light a fire casts, measured from the fire's own particles.

The simulation and the two contributors that draw it are one plain Dart package. A contributor is handed a pass encoder by flutter3d_core, never a widget, so nothing here names Flutter. That is what lets flutter3d_model_core's BakeParticleSystemJobRequest bake a system into a cache with no window in front of it.

flutter3d_elements #

Water, fire and bodies in the native physics core's world, stepped with nothing that draws them: ElementsSimulation, the flames an Igniter holds to a body (Fires), the waters poured over a ground (WaterBody), the game's objects followed in (Follower), and ElementHook, a plugin element's step over the world's heat, water, wind and forces. Its events go on the bus with codecs, and ElementsSimulationPlugin steps it in a loop. No core, no hardware layer, no shaders, no particles: a server replays the fire and the water with no renderer.

flutter3d_effects #

The elements drawn and heard: LiquidView, FireView, PhysicsHearing, and Elements, which draws an ElementsSimulation and keeps the looks of its bodies and the views of its waters (lookOf, viewOf). liquidMeshes draws a liquid in a vessel of flutter3d_physics.

flutter3d_audio #

Positional audio: attenuation curves, panning, occlusion through a callback, voice limiting, buses. SilentBackend and SoLoudBackend, so a machine with no audio device still plays the game.

→ Particles & audio

flutter3d_pointer_lock #

Published as pointer_lock up to 0.4.2+2 and under this name from 0.4.3 on; migrate moves the dependency and the imports.

Relative mouse deltas, which Flutter offers on no desktop platform and does not surface in a browser either. On macOS it turns off the association between the physical mouse and the on-screen cursor, so mouseMoved events keep arriving with their deltas while the cursor stays put. In a browser it is document.requestPointerLock through static interop: pure Dart, chosen by conditional export, so nothing is registered and flutter test --platform chrome can exercise it. Windows and Linux are not implemented, and now say so: a game reads isSupported and turns the camera by dragging instead, which it could not do while the answer was a platform list in the game.

Three things a browser does that a desktop does not, all handled: a capture must come out of a user gesture, a refusal arrives as an event rather than as an exception, and the player can leave the lock with Escape at any moment.

flutter3d_samples #

The Khronos glTF sample assets, the Utah teapot and the .f3d conversions of them. Test data with two path constants over it (samplesAsset for a bundle, samplesPath for disk) and no other Dart.

Its own package because it was flutter3d's: declared in the engine's flutter.assets, so every application built on the engine bundled 4.1 MB of models it never loads, a third of the shooter's web asset payload. The decoder tests and the demo still read them; a game no longer carries them.

pad_input #

Sticks, triggers and buttons, read as a snapshot the caller asks for once per frame. It is not a stream, because a fixed-step simulation asks what is the pad doing now and a stream would push edge detection into every caller. The dead zone is radial and rescaled, applied on the way out so nobody can forget it. The entry point is Gamepad.instance and a PadSnapshot.

Button names are physical positions (face.south, not a), because the string lands in a player's config file and is read back years later, possibly on a different pad: Xbox's lower face button is A, PlayStation's is Cross, and Nintendo swaps A and B.

Knows nothing about games. The translation into actions is PadInput in flutter3d_game, beside the keyboard's. The web backend is pure Dart over navigator.getGamepads(); macOS, iOS and Android wait for a controller in hand, for the reason ARCHITECTURE.md §11.1 records. Windows and Linux are not implemented yet.

flutter3d_stereo #

A rig of two eyes under a head, and StereoSurface, the widget that draws the pair side by side into one frame. Each eye gets the off-axis frustum its half of the screen calls for, from the viewer profile and the screen's size in metres. The head is turned by a HeadTracker; the one that reads a sensor is Android's rotation vector, three degrees of freedom. It is a pair for a phone in a holder: there is no lens distortion correction and no XR runtime behind it.

flutter3d_education #

Education on the engine, in plain Dart: the lab simulations, the experiment document and a lesson's steps. flutter3d_lab until 1.0.0-rc.1; the chemistry bench is its example.

lab.dart. Virtual laboratory simulations, of which the pendulum is the first. Plain Dart, so a server can replay a student's run with no Flutter SDK, and divergenceFrom names the first checkpoint at which a run departs from the reference.

flutter3d_education_lti #

LTI 1.3 launch and xAPI reporting, so a lesson can be launched from a learning management system and report back to it. LtiPlatformSettings is one LMS registration, read from environment variables. OidcLoginInitiation builds the redirect for LTI's third-party login with a fresh state and nonce. LtiLaunchValidator checks an incoming id_token against the platform's JWKS (cached by kid), its issuer, audience and expiry, and the state and nonce of the login that started it. AgsClient posts a score to a line item through Assignment and Grade Services, signing its own client-credentials grant, and XapiClient PUTs a statement to a Learning Record Store under the statement's own UUID, so a retried submission cannot count twice.

flutter3d_lti until 1.0.0-rc.1, and the lti.dart library of flutter3d_education for a while; a package of its own so that what reads an experiment carries none of its cryptography. Plain Dart, without shelf, jaspr or Flutter, so a service that only verifies a launch or files a grade carries none of them. cloud/lti/server in the repository is that service: /login and /launch run the launch over this package, and /launch/<token>/check-result sends a lesson's answer back through both clients.

flutter3d_game_kit, flutter3d_game_physics, flutter3d_game_ui, flutter3d_camera #

The parts of the demos another game would use, a library each. flutter3d_game_kit is gameplay: reactions.dart, soundtrack.dart, ghost.dart, seeded_levels.dart and world.dart. flutter3d_game_physics is the gameplay that needs the native physics core, kept apart so a game without it compiles no C: ragdoll.dart, wrecks.dart, party.dart and elements.dart (the elements heard, and the water lit by the day). flutter3d_game_ui is every widget drawn around a game: hud.dart (with the minimap and the automap), touch.dart (the stick, the buttons and pedals, and the layouts of them), settings.dart (the panel, its sections and the rebinding list), access.dart, screens.dart (title, credits, ending, loss), photo_mode.dart, capture.dart, and theme.dart with the colours and the words all of them read. flutter3d_camera is the virtual cameras the four genres' own are presets of. Each was a package of its own, flutter3d_addon_*, until 1.0.0-rc.1.

flutter3d_post #

Post-processing as plugins, one library per family (light.dart, shading.dart, reflections.dart, atmosphere.dart, motion.dart, style.dart), each effect the switch of its own render step, and standard.dart, the six in one list. Its pubspec marks the plugins of each library, so depending on it installs every effect switched to what the settings say, and the frame does not change until a setting does.

flutter3d_net #

Rollback netcode over flutter3d_sim for two peers: input frames exchanged per step, prediction from the last frame that arrived, a rollback and replay when a confirmation disagrees. NetTransport is the one door to a network. LoopbackTransport stands in for it with a fixed delay and a seeded loss rate, and WebSocketTransport is a real one.

flutter3d_net_webrtc #

NetTransport over a WebRTC data channel, peer to peer, with a relay that carries only the SDP and ICE handshake. Its own package because it is the one that names a plugin.

flame_flutter3d #

A bridge to the Flame 2D game engine. Flame draws its own layer and flutter3d draws its own: Flutter3dFlameWidget puts a flutter3d SceneSurface under Flame's GameWidget in one Stack, with Flame on top because it needs raw input, the same arrangement the platformer uses for its HUD. Neither engine's renderer is reimplemented.

There is one clock, Flame's. A BridgeClock component, added to the hosted FlameGame once, calls back every frame after Flame's own components have updated, and that callback is where a physics step, an actor-system step or a camera sync advances. BridgePlane is the one place a Flame Vector2 and a flutter3d Vector3 are the same point: BridgePlane.ground(height:) for a top-down game, where Flame's y becomes flutter3d's z, and BridgePlane.backdrop(depth:) for a side-scroller, where it stays y. Every bridged component takes one, so a game settles its axis convention once.

A game that mixes in HasFlutter3d owns its 3D world: its scene, camera, renderer and projector are fields of the game, it builds the world in onOpen3d, and Flutter3dFlameWidget(game: game) needs nothing else. Object3dComponent keeps a Flame component and a scene node in one place, with Flame's effects, nesting, visibility, scale and an elevation off the plane all crossing; InstancedObject3dComponent does the same for many small things drawn in one call.

Around that: a FlameChaseCamera and a ViewCamera as framings on flutter3d_camera's VirtualCamera, a BridgeProjector between the 3D camera and Flame's screen, taps that land on what a perspective camera shows (Tap3dCallbacks), hitboxes drawn in the scene for debugging, ChunkStreamer for a world built piece by piece, Particles3dComponent for blasts, and physics, actors, input and camera sync through flutter3d_physics, flutter3d_sim and flutter3d_game, the first two in fixed steps. apps/flutter3d_demo_river uses most of it, apps/flutter3d_demo_arcade the physics side.

flame_flutter3d_audio #

Sound for a bridged game. AudioSceneComponent is the game's audio scene: silent until the player's first input opens the speakers, which is when a browser allows it, heard from the game's 3D camera, and mixed after everything has moved. SoundEmitterComponent is a loop held open by state rather than by events: it plays while its component is in the game and playing is true, sounds from its bridged parent, and moves onto the real speakers when they open.

A package of its own because sound brings SoLoud's native library, and on the web its script, into every application that depends on it; a bridged game with no sound carries neither.

flame_multiplayer #

Two players on two machines, for any game that steps in fixed steps, and with no dependencies at all. PeerRoom is the room: slot nought for whoever made it, a hello repeated until the other machine answers with what it plays, and channels on one wire that do not hear each other. RollbackPlay runs one simulation on both machines in step, each driving its own player, over RollbackSession (input delay, the guess of the far side's hands, the rollback when it was wrong), and keeps the first settled ending so both go on from the same state. BatonStream is turns: the machine playing tells frames and events, the other replays them at the pace they were played, and the turn goes across with its state. PeerFeed is two games side by side, each telling the other where it is.

PeerWire is the one door to a network, JSON-shaped messages sent reliably or not, and LoopbackWire two ends of it in one process for tests. flutter3d_net's NetTransportWire carries it over that package's relay and WebRTC, and NetSession there is RollbackSession on a Snapshot.

flame_multiplayer_dashwire #

DashwireWire: a dashwire WireConnection as a PeerWire, reliable and unreliable messages on dashwire's two channels. dashwire's binary WebSocket passes through flutter3d_net's relay unchanged, so games on either adapter meet in the same rooms. A package of its own so the core keeps no dependencies.

Plugins #

flutter3d_plugin_api #

What a plugin is written against: Flutter3dPlugin, its PluginManifest, the PluginHost and its registries, the loop's phases, the bus and what a step publishes. PluginGroup holds plugins in install order, one per id, adjusted without mutation by set, add, without<T>() and withoutId; the project's generated installedPlugins is one, and PluginManager.installGroup installs one. SimulationProfile (deterministic, accurate) is an open class with capabilities, handed to a plugin as PluginHost.profile; code asks isBitExact rather than comparing names. It depends on flutter3d_foundation alone, re-exports the foundation types its signatures name, and has an API version of its own, so a plugin written for 1.0 installs on every engine that provides 1.x. A registry a package fills is declared by that package: DecoderRegistry in flutter3d_core, EntityKindRegistry and SimulationQueryRegistry in flutter3d_sim, EditorRegistry in flutter3d_editor_core, McpToolRegistry in flutter3d_mcp.

flutter3d_foundation #

The values every package speaks, under all of them: Flutter3dException and its four families, WorldPosition and LinearColor with their crossings into vector_math, Issue, the file envelope (FormatSpec, FormatDocument), Registration, Portable, the maths a step may call, and EngineLog, what an engine says about itself at a LogLevel, with crash reporters registered by onCrash. It depends on vector_math and nothing else, so the hardware layer and the backends throw and read files without the plugin contract.

flutter3d_plugin_runtime #

Plugins loaded while the game runs: .f3dplugin data plugins (format version 4, which files a document's phases under its plugin as it files its events), Wasm step systems under ABI 1 or 2 (wasmPluginAbiMin to wasmPluginAbiMax; 2 adds f3d.field_frac and fields with fewer fraction bits; 32-bit integers only, stepped by a metered Dart interpreter on every platform), and expression systems written in the expression language, which take the place of the scripts and the ScriptRuntime seam of before 1.0 (a version-2 scripts entry is read as one). This build reads expression systems and refuses, by name, to install a plugin that brings one until the evaluator is wired in. It depends on flutter3d_plugin_api alone: entity kinds, materials, render steps, effects and physical materials are sections the packages that own them read through DataSectionRegistry (flutter3d_sim's entityKindsSection, flutter3d_core's renderDataSections, flutter3d_particles' effectsSection, flutter3d_matter's physicalMaterialsSection), so a server replaying a run with a data plugin resolves no render core. A permission a run-time plugin uses must be declared in its manifest and granted by the application.

flutter3d_lints #

An analysis server plugin with the rules a step is held to, for code outside this repository: no clock, no unseeded Random, no dart:math transcendental where Portable gives one answer everywhere.

→ Plugin catalogue · The plugin and extension API

Tools #

flutter3d_editor_core #

A level editor with the editor taken out. Editing (select, nudge, grow, add, duplicate, turn, brighten, delete, set a field, undo, redo, and write the document back exactly as it was written), Handle and handlesOf, Picking (a ray against the brushes, and a tie broken towards the thing standing against the wall rather than the wall), Placeable and the palette a level builds out of itself, Looks (what a game says its own words look like), OpenKind, and Template / scaffold / projectAt, a new project as a map of bytes.

Plain Dart, held by the same scan the simulation is: the simulation names no Flutter reads a list of packages now, and this is the second one on it. dart test runs the suite with no binding.

It was eight files of apps/flutter3d_editor/lib/src, and moving them was not tidiness: no package depends on an application forbids anything depending on an application, and pub cannot express such a dependency anyway, because an application is not published. So a level linter for CI, a service that validates an uploaded level before a player loads it, or a tool an agent speaks to had nowhere to start. A level is a document, and the programs that most want to say a document is wrong are the ones with nothing to draw.

It does not read a disk, and knows no genre. Building a scene from a level, which its light optimizer and the application's loader both need, is flutter3d_level_scene's.

flutter3d_level_scene #

A level document turned into a scene: LevelScene builds the brushes into uploaded meshes, per material or per brush (LevelBatching), and the lights, probes, decals, mirrors and camera screens into nodes; meshDataOf interleaves a BrushSurface into the engine's vertex layout. Plain Dart. The application's LevelLoader hands it the textures it decoded; the editor's agent server hands it nothing and draws the level flat on the software renderer. It was part of flutter3d_editor_core until 1.0.0-rc.1, which made the application depend on the editor to load a level. Two files stayed with the application and say where the line is: the fly camera needs a renderer to have a camera, and reading a file off a disk is where a crash loses somebody's work.

→ The level editor

flutter3d_mcp #

The editors offered to an agent, and the kit their servers share: flutter3d_editor_mcp, flutter3d_model_mcp and flutter3d_mcp_kit until 1.0.0-rc.1. Each server keeps its command: dart run flutter3d_mcp:editor_mcp, :model_mcp and :project_mcp.

editor.dart

The same editor, offered to an agent. A Model Context Protocol server over stdio (dart run flutter3d_mcp:editor_mcp <level.json>), holding one document for the life of one process.

The tools are EditorCommand, not a second implementation of one. Ten of the eighteen are the EditorCommand family flutter3d_editor_core already publishes, offered under the names that package already gives them, and a tool call is its arguments handed to EditorCommand.fromJson. So an edit made by an agent and an edit made by a hand reach the document by one route, get one name in the undo stack, and come back under the same key. The table is built from editorCommandNames and the suite holds it to that list both ways round, because a server keeping its own copy is a server that silently cannot call the eleventh command.

Two of the tools are not commands, and both were missing from every sketch of this. list prints everything in the level with the kind and index select takes. Every other verb works on "the selection", which a program with no screen cannot guess, so without it driving the editor means moving the third brush without ever finding out there is a third brush. It lives in the core package as piecesOf, beside handlesOf, because it is the same question asked by a caller with no pixels. validate runs the document through LevelValidator against its own vocabulary; without it the first news of a broken level is a diff somebody reads later.

It draws in software, flat. screenshot renders the level through flutter3d_cpu at 320×200 with no GPU and no Flutter, every brush in its material's colour and a small box at each light and entity, since the textures are decoded by the application that ships them. report reads the same frame's object ids with the level drawn one brush per draw, and says for every brush, light and entity how many pixels it owns, where, how far away, and what covers the part of the screen it would fill.

And it will not write over a generated document. A level carrying generatedBy may be opened, changed and saved somewhere else, and the copy claims itself. Saving over the original is refused, because that save would look like it worked right up until the next run of the generator threw the work away.

Plain Dart, held by the same scan the simulation and the editor's core are: no Flutter anywhere in the graph, which for this package is not tidiness but whether the server starts at all.

model.dart

The modeller offered to an agent, over MCP on stdio, one project per process: the shape editor.dart has for levels.

A hundred and forty-seven editing tools, each one of the editor's own commands under its own name, plus render and renderSheet, which draw the project through the software backend and hand the agent a picture of what it just did. A path that does not exist yet starts a fresh project there, so the first call can be addPrimitive. It also proves the property everything under it is arranged for: a machine with the Dart SDK and no Flutter can resolve this package and start it, which CI asks in a container of exactly that kind.

kit.dart

What the four MCP servers share, written once: OfferedTool, a tool and the code that runs it as one value; ToolTableServer, a server that is a list of those over one session; Answer, what a call did and the sentence about it, with a drawing tool's picture beside it, and the functions that turn a refusal into an error result an agent reads rather than a server that failed; and LoopbackMcpServer, any of those servers over 127.0.0.1 HTTP with a token per server, for an application handing an agent the session a person already has open. The token travels in the Authorization: Bearer header and in no URL, and every tool that reads or writes a file resolves it through one ProjectRoot, refusing a path that leaves the project. Plain Dart.

flutter3d_mesh #

The mesh a modeller edits, with the topology still in it: faces of any valency, half-edges that know their twin, attribute layers, and the operations that change them. Plain Dart.

It exists because MeshData is the wrong shape for editing. That one describes a finished mesh (vertices in the order a GPU wants them, a corner duplicated once per face normal meeting there), and every question a modeller asks is about what that arrangement threw away: which faces share this edge, what ring does it belong to, what is the loop around this face.

A spike today, and its own header says so. EditMesh builds from faces or as a cuboid, walks loops without allocating, extrudes a face, validates its invariants and converts to MeshData; operations rebuild the arrays rather than editing them in place. What comes next (persistent chunked arrays, selections, loop cuts, dissolves) is doc/model-editor-plan.md §2.1.

flutter3d_model_core #

The headless half of the model editor: the project, the sealed command every edit is one of, the history that takes them back, and the rules about whether the result can leave. The same split flutter3d_editor_core made for levels, for the same reason.

ModelProject is a value: an edit hands back a new one sharing every object it did not touch, which is what makes a step of history cost two hundred pointers rather than two hundred objects. Ids are numbers and are never handed out twice, because a selection, a command's arguments and a step of history all name objects and all three outlive the object being deleted and put back by an undo.

Twenty-eight commands so far, each carrying its own name, a sentence for the menu and its arguments as a map, which is what lets the history say "undo move" rather than "undo", a file replay the steps that made it, and an agent call one by name. Mesh commands are the exception to the value rule and the reason is measured: p0-05 timed copy-on-write chunks against a flat array with a journal of previous values, and chunks cost 92 to 100 per cent of a full copy the moment an edit is scattered. So an EditMesh takes a journal step and the history rolls it in lockstep with the documents it keeps for everything else.

flutter3d_sim_mcp #

Two servers. SimMcpServer is a level an agent can play without seeing it: open a level, step it with input, read positions and health back in words, capture a headless frame, and write the run out as a .f3drun. DiagnosticMcpServer answers why a frame is wrong: a debug view of normals and depth, the HDR value of one pixel, the passes the frame graph ran, a scan for NaN.

flutter3d_build #

The build hook dart run flutter3d_build:init wires into a project. It converts model and texture sources into what the engine loads on every build, from the rules in flutter3d_assets.yaml, with a content-hash cache so an unchanged source costs nothing. Textures are cooked once into a block format the device turns into BC, ASTC, ETC2 or RGBA8 when it loads them. Plain Dart, because the Flutter tool starts a hook as a separate process with no SDK to resolve inside it. See The asset pipeline for init, the manifest, the families, and what a failed hook actually says.

flutter3d_editor_widgets #

The controls the modeller and the level editor share, so neither keeps its own copy: number, colour, range, enum and texture fields and the row they assemble into, over one theme.

Applications #

apps/flutter3d_demo_dungeon #

The shooter, and a headless test that plays it to the exit.

apps/flutter3d_demo_platformer #

The platformer: third person, two jumps and a dash, and no line of the engine changed to allow it. Short enough to read in one sitting, which the dungeon's 898 lines are not.

apps/flutter3d_demo_racing #

An arcade racer: one circuit, a car that can be made to slide, and a lap time to beat. Runs on Impeller and, since the cube shadow atlas got its own resolution, in a browser too. Its demo page keeps the hunt that took it there.

apps/flutter3d_demo_strategy #

A map, two sides and a match played to a finish: a camera over the ground, a box drawn round a selection, orders given to it, and an opposing side played by Bot. playthrough_test.dart plays assets/levels/map_a.json to somebody winning it with nothing drawn at all. The balance of a map is what no other test in this repository would notice, and a match is thousands of steps, so a picture per step would be an hour of frames nobody looks at. Builds for macOS, the web, Android and iOS.

apps/flutter3d_demo_arcade #

Meteor Yard: a ship and a yard, with Flame and flutter3d each drawing their own layer and every bridge in flame_flutter3d doing real work at once. It is the fifth demo game and the one that exists to prove the bridge end to end.

apps/flutter3d_demo_river #

River Sortie: a jet up a river that never ends, after River Raid. A Flame game from end to end, with Flame's own hitboxes and collision callbacks deciding every hit and Flame painting the instrument panel, and flutter3d drawing the valley, the water and the craft underneath through Object3dComponent. Five levels with a task each, whose last bridge stays shielded until the task is done, and targets that go down the way their kind would: a tanker sinks, a helicopter spins into the river, a depot takes its neighbours with it.

apps/flutter3d_showcase #

Every capability of the engine on a page of its own, with the version it appeared in, a step-by-step guide that builds it, and the source that runs. The site's guides by capability are generated from the same files, and its web build is what the live showcase runs.

apps/flutter3d_editor #

The first application here that is not a game: opens a level document with the same LevelLoader the games use, and lets somebody fly around it, drag what's in it, and write it back. What it keeps is the half that reaches a device (the window, the disk, the camera, the frame); everything else is flutter3d_editor_core. → The level editor

apps/flutter3d_modeler #

The model editor: object, mesh, material, UV, sculpt, retopology, paint, simulation, animation, render and scene modes over one project document, with an undo history that records who made each change, a person or an agent. It runs on macOS and in a browser, and its web build is what models.pleion.dev serves. The tutorial walks six cases through it.

apps/flutter3d_lesson_viewer, apps/flutter3d_stereo_lesson_viewer #

A lesson is a level document with steps in it. The viewer plays the steps: where the camera stands, what is shown and hidden, and a question with its answers. The stereo one is the same player behind a StereoRig.

apps/flutter3d_lab_pendulum #

The pendulum laboratory from flutter3d_education's lab.dart, as something a student runs.

apps/flutter3d_lab_incident #

A recorded incident on an operator's own panel: a DataSourceTrace an engineer scrubs between named moments, replayed from a recording instead of read from a live feed.

packages/flutter3d_app/example #

The smallest application on the engine: a device, a renderer, a lit cube and a camera that orbits it. It has no level, body or genre, because it is what a project that is not a game starts from.

packages/flutter3d_game/example #

The application a new game starts as: a level you can walk around, with no genre package. It opens its device through flutter3d_app's openDevice in one exported line, which is what gives a scaffolded project the web build and the software fallback without its author deciding anything; see Assembling an application. The editor's four game templates are copies of it, and apps/flutter3d_editor/test/scaffold_test.dart holds them to that.

packages/flutter3d/example #

The engine's own demo: a model browser with every feature switchable, and the frame-capture hook.

Dependency rules, in one picture #

flowchart TB
  apps["apps/*"] --> genres["flutter3d_game_shooter<br>flutter3d_game_platformer<br>flutter3d_game_racing<br>flutter3d_game_strategy"]
  apps --> game["flutter3d_game"]
  apps --> app["flutter3d_app<br>(backend choice, surface, level loading)"]
  game --> app
  gameui["flutter3d_game_ui<br>(every widget)"] --> game
  apps --> gameui
  app --> levelscene["flutter3d_level_scene"]
  genres --> sim["flutter3d_sim"]
  app --> gfx["flutter3d_hardware<br><b>the HAL</b>"]
  app -.-> onebackend["one backend<br><i>impeller · webgl · webgpu · cpu</i>"]
  app --> engine["flutter3d"]
  app --> sim
  game --> engine
  sim --> physics["flutter3d_physics"]
  physics --> matter["flutter3d_matter"]
  matter --> foundation["flutter3d_foundation"]
  gfx --> foundation
  engine --> gfx
  onebackend --> gfx
  onebackend --> shaders["flutter3d_shaders"]
  extras["flutter3d_particles<br>flutter3d_audio"] -.-> engine
  game --> devices["flutter3d_pointer_lock<br>pad_input<br><i>devices only</i>"]

  classDef forbidden stroke-dasharray: 4 3
  sim -.-x engine

The dashed crossed edge is the rule that pays: flutter3d_sim must never reach the renderer, and the genre and backend rules in tool/structure.dart are what make it a fact instead of an intention.