flutter3d
Showcase Changelog 39 packages API reference

A 3D engine on Flutter GPU

Five games, one engine, no edits in between

flutter3d is a renderer, a game layer, and six games built on them: a shooter, a platformer, a racing game and a strategy game, each with a genre package of its own, and two Flame games drawn in 3D, Meteor Yard and River Sortie. The platformer and the racer were built without changing a line in the shooter's engine packages.

Playable right now, in this browser: the shooter · the platformer · the racing game · the strategy game · Meteor Yard and River Sortie, two Flame games drawn in 3D. Or try every capability, one page each, with live controls: the showcase. What changed in each release, with a link to the page that shows it: the changelog.

One frame, as this engine encodes itone command buffer per pass

shadow3 cascades
linear depth
opaquesorted by
pipeline
transparentback to front
no depth write
bloomhalf-size chain
blur between
compositetonemap
exposure, sRGB
overlaydebug lines
view model

The first four render into r16g16b16a16Float. Metal allows one open encoder per command buffer and flutter_gpu cannot end a render pass, so each pass gets its own command buffer, submitted in order.

Status #

Platforms macOS and the browser are supported and exercised; Android is played on a real handset (Impeller Vulkan, touch controls); iOS runs clean in the simulator on Metal; Windows and Linux are unverified
Published Yes: all 38 packages are on pub.dev. Thirty-six carry 0.8.0, so any ^0.8.0 resolves against every other; pad_input and pointer_lock keep their own line at 0.4.2
Stability Pre-1.0. The graphics HAL carries a written compatibility promise; nothing else does

Where to start #

All six run in a browser on the WebGL2 backend and are embedded on their demo pages. The racing game was the holdout, at well under a frame a second for months, and its demo page keeps the hunt. The cost turned out to be a cube shadow atlas sized from the sun's setting: four hundred megabytes of texture on a platform with less, which no reduction in frame size could touch.

The package split #

Thirty-eight packages in all. The diagram shows the ones an application stands on, and each boundary in it is a rule that a check enforces.

flowchart TB
  subgraph app["applications"]
    dungeon["apps/flutter3d_demo_dungeon<br>the shooter"]
    platformer["apps/flutter3d_demo_platformer<br>the platformer"]
    racing["apps/flutter3d_demo_racing<br>the racing game"]
    editor["apps/flutter3d_editor<br>the level editor"]
    gameSeed["flutter3d_game/example<br>the game scaffold"]
  end

  subgraph genre["genres: vocabulary"]
    shooter["flutter3d_game_shooter<br>weapons, monsters, inventory"]
    plat["flutter3d_game_platformer<br>runner, springs, surfaces"]
    race["flutter3d_game_racing<br>track, car, tire, lap"]
  end

  subgraph layer["the application layer"]
    appLayer["flutter3d_app<br>backend choice, surface, level loading, storage"]
    game["flutter3d_game<br>input, the run, the screens, actor visuals"]
  end

  subgraph draw["drawing"]
    f3d["flutter3d<br>renderer, scene, assets"]
    gfx["flutter3d_hardware<br><b>the HAL</b>"]
    impeller["flutter3d_impeller<br>flutter_gpu"]
    webgl["flutter3d_webgl<br>WebGL2"]
    cpu["flutter3d_cpu<br>software"]
  end

  subgraph sim["simulating, no GPU"]
    simp["flutter3d_sim<br>step, input, levels, actors, ECS<br><i>no Flutter at all</i>"]
    physics["flutter3d_physics<br>shapes, sweeps, controller"]
  end

  dungeon --> shooter & game
  platformer --> plat & game
  racing --> race & game
  editor --> appLayer
  gameSeed --> game
  shooter --> simp
  plat --> simp
  race --> simp
  game --> appLayer
  appLayer --> f3d & simp
  f3d --> gfx
  impeller --> gfx
  webgl --> gfx
  cpu --> gfx
  simp --> physics

Three more packages are left out of the diagram on purpose, because none of them changes what an app may know. pad_input and pointer_lock are gamepad and mouse capture, read once per frame by flutter3d_game, and flutter3d_conformance is test-only: it is what a backend has to pass before it can appear in the table below. flutter3d_app makes one decision of its own, which device to open (web or native at compile time, and which of the two on each side at run time), so the conditional import an app needs is written once and not per project. Assembling an application walks through all of it with the real code that uses them. The rules the diagram states live outside any package, in tool/structure.dart: thirty-five checks that read source text and run before a build.

Three of those rules hold the picture up:

The application layer exists because the first two rules leave nowhere else for one mapping to live. Level geometry has to become mesh nodes and an actor has to get a visual, while the game rules must not learn what a mesh is and the renderer must not learn what a monster is. flutter3d_app loads a level into a scene and flutter3d_game gives actors and fixtures their visuals, so those two packages are the only ones allowed to know both sides.

One HAL, four backends #

The renderer talks to a hardware abstraction layer and never to a graphics API. Four packages implement that layer.

Backend Runs on Status
flutter3d_impeller flutter_gpu: Metal on Apple platforms, Vulkan elsewhere Complete. Every game ships on it
flutter3d_webgl WebGL2, in the browser Runs all five games, slower, and the four genre games at a fixed resolution. The racing game was the holdout for months and drives now; the cost was a cube shadow atlas sized from the sun's setting, not the frame
flutter3d_cpu Nothing. It rasterises in Dart Complete for the golden set. A dev dependency of every game, and now flutter3d_app's last resort too
flutter3d_webgpu WebGPU, in a browser that has an adapter Draws, and answers the whole conformance suite against a live device. Declines three capabilities by name. You get it by asking for it, not by default; see below

flutter3d_conformance is the suite a backend has to pass before it belongs in this table: clears that cover the whole attachment, upload and readback row order, HDR renderability, shader stage linking. It runs against all four, including Impeller through packages/flutter3d_impeller/tool/conformance.sh, which is what the harness for Impeller has to be, since Flutter GPU requires Impeller and a headless flutter test cannot give it one. WebGPU is the opposite case and the easiest of the four: Chrome has a real WebGPU device inside flutter test, so the suite is an ordinary test file there.

flutter3d_app, the assembly-layer package, picks which of these an application gets. It tries Impeller first on every native build and only reaches for the CPU backend at runtime, if Impeller throws. In a browser it opens WebGL2, and tries WebGPU first only when the build says --dart-define=FLUTTER3D_WEBGPU=true. Assembling an application documents those fallbacks.

WebGPU is not the browser default, and the reason is a number. Whether navigator.gpu hands out an adapter depends on the browser, the driver and the machine's blocklist, so finding out means trying, and code that can try is code dart2js ships. Measured on the strategy demo, flutter build web --release writes 2,529,865 bytes of main.dart.js without the flag and 2,906,514 with it: 376,649 bytes, 14.9%, for a second backend a build may never open. WebGL2 is also the browser backend all five games have been checked on, and the one whose reference set is recorded. Switching the probe on for everybody would change what those games draw and charge each of them the bytes, so it stays a game's own call, and making it takes one flag.

An application names a backend in its pubspec and hands the device to Renderer.create. Moving between backends is that line and one constructor call.

Each backend exists for a different reason. Impeller is the production one. WebGL2 answers whether the HAL is a real seam or only a description of Impeller, because a fake backend can only show that an interface is callable, never that it is implementable. The CPU rasteriser shares no driver, shading language or command buffer with either of the others, so agreement with it means more than agreement between two GPU backends would. WebGPU is the first one whose shaders are not the same text (WGSL instead of GLSL, translated by a second toolchain), which asks a different question again: whether the shader half of the contract is a seam too.

How the HAL is put together · Writing a backend

What is in the box #

Rendering Six lighting models as pre-built shaders. HDR pipeline with tone mapping and exposure, bloom from a half-size chain, cascaded directional shadows with PCF, point-light shadows with a static half baked once, instanced batches, precomputed visibility and baked lightmaps with bounces for brush levels, screen-space reflections, fog, 4× MSAA, wireframe (Impeller only: the WebGL and software backends decline it, and the frame says so through FrameResult.wireframeDeclined)
Geometry Surfaces of revolution as the base generator, MeshData and MeshBuilder, custom vertex layouts, tangent generation with Lengyel's method
Assets glTF 2.0 / GLB, Wavefront OBJ, and .f3d, the engine's own container. Decoding on a background isolate, a reference-counted cache
Animation All three glTF interpolations, skinning with 64 joint matrices, an AnimationPlayer with crossfades, transport and once/loop/ping-pong
Scene Version-stamped nodes, a BVH shared by culling and picking, LOD groups by screen coverage, orbit and follow cameras, CPU raycasting
Simulation A fixed step with interpolation, device-agnostic input with latched edges, a level format with a validator, holes blown in its walls, mechanisms, actors and brains, a navigation grid with flow fields and an automap drawn from it, an ECS, snapshots, demos that replay a run exactly, a rewind buffer for a kill camera
Input Gamepad through pad_input on macOS, iOS, Web and Android, read as a per-frame snapshot; mouse capture for FPS-style cameras through pointer_lock, on macOS through a method channel and in a browser through the Pointer Lock API, which is pure Dart and needs no registration; on-screen touch controls in flutter3d_game
Physics Overlap, sweeps and rays over a uniform-grid broadphase, a character controller that walks, jumps, climbs a step and rides a lift, and rigid bodies without rotation
Driving A track as a spline with width, camber and surface bands, a sphere-and-frame vehicle, a tire curve with a friction circle, lap counting through checkpoints, AI drivers and ghost tapes
Extras A pooled particle system that draws in one call, positional audio with attenuation, panning, voice limiting, and occlusion through the level's walls that muffles a sound as well as quietening it

What it does not do #