Skip to content

feat: Add flame_flutter3d, a bridge to the flutter3d 3D engine - #4085

Closed
dzolotov wants to merge 4 commits into
flame-engine:mainfrom
pleiondev:feat/flame-flutter3d
Closed

dzolotov wants to merge 4 commits into
flame-engine:mainfrom
pleiondev:feat/flame-flutter3d

Conversation

@dzolotov

@dzolotov dzolotov commented Oct 1, 2026 •

Copy link
Copy Markdown

Description

This adds flame_flutter3d, a bridge package that puts a flutter3d scene under a Flame game. I've been developing it in the flutter3d repository, where it reached 0.8.4 on pub.dev. This moves it here as 0.9.0-dev.0, ported to Flame 2.0.

Flame keeps running the game and drawing its own layer. flutter3d draws a 3D layer under it, and the bridge keeps the two in agreement on transforms, lifecycle, physics contacts, input, the camera and an actor system. A Flame game gets a full 3D renderer without being rewritten: its components, effects, hitboxes, collision callbacks, camera, overlays, flame_tiled maps and flame_forge2d bodies keep working, and the bridge draws them in 3D.

One loop and one event model

The two engines share Flame's loop. A BridgeClock component draws the 3D frame after Flame's components have updated, so a MoveEffect that moved a component this frame is drawn in 3D this frame, and there is no second ticker to drift from the first. HasFixedStep runs the game's logic, the physics and the actors in fixed steps, so a second of play comes out the same at 30 and at 120 frames per second, and bodies are drawn interpolated between two steps. BridgePriority names the order everything runs in. Some of it runs after Flame's own CameraComponent, so a 3D camera following camera.follow() doesn't trail it by a frame.

Events stay Flame's too. Contacts from flutter3d's physics arrive as Flame's onCollisionStart, onCollision and onCollisionEnd, a tap reaches the component the player sees under a perspective camera through Tap3dCallbacks, and keys, drags, the touch joystick and gamepads all write into one input state. A game handles its events the way it already does.

Rendering

The 3D layer renders in HDR through flutter3d's post-processing chain: tone mapping (Neutral, ACES, AgX, Reinhard), exposure and auto exposure, bloom, SSAO and GTAO, screen-space reflections, contact shadows, light shafts, volumetric fog, depth of field, motion blur, temporal AA, a LUT and spatial upscaling. HasFlutter3d.renderSettings() is read before every frame, so a game can switch any of it on Flame's clock: depth of field for a cutscene, fog as the day turns.

Under that there are directional shadows, six lighting models (PBR among them, plus toon and unlit), up to eight lights per draw out of however many a scene holds, glTF and OBJ models with skinning, morph targets and animation, frustum culling and LOD.

WebGL2, WebGPU, Flutter GPU, and no GPU at all

flutter3d draws through Flutter GPU on macOS, Android and iOS, and through WebGL2 or WebGPU in a browser. A web build draws through WebGL2 by default, so it runs in any current browser with no setup step and no extra shader build. With --dart-define=FLUTTER3D_WEBGPU=true it tries WebGPU first and falls back to WebGL2 where the browser has no adapter. I made WebGPU opt-in because it adds about 368 KiB of JavaScript.

There is also a software rasterizer that needs no GPU. All 213 of this package's tests draw through it, so they run on any CI machine, and the stack's golden images are checked against two independently written renderers.

How this compares to flame_3d

The two packages take different routes. flame_3d is a 3D component tree inside Flame, built on Flutter GPU and, by its own README, experimental. This keeps Flame's world as it is and puts a separate, released engine under it. Where they differ today:

flame_flutter3d flame_3d
Web WebGL2 by default, WebGPU opt-in with a fallback WebGPU only, experimental, shaders built separately with naga
Without a GPU Software rasterizer, used by the tests Not available
Post-processing HDR, bloom, SSAO/GTAO, SSR, DOF, motion blur, fog, TAA, LUT None yet
Shadows Directional, plus contact shadows None yet
Existing Flame games Keep their components, effects, collisions and camera Rewritten as Component3Ds
Physics, actors, particles Bridged, on Flame's clock Not included
Tests 213 here, about 10,800 across the flutter3d stack 28
Versioning Semver releases on pub.dev No semver guarantee yet, per its README

They don't compete for the same games. A 2D game with a 3D look (a top-down shooter, a side-scroller with depth, a board in perspective) or a 3D scene that wants Flame's HUD, effects and input fits this one.

What's in the PR

  • packages/flame_flutter3d: the package and its minimal example.
  • doc/bridge_packages/flame_flutter3d: a docs page, linked from the bridge packages index.
  • examples/lib/stories/bridge_libraries/flame_flutter3d: three stories. One toggles post-processing live under a Flame HUD. One drives both engines from one loop: Flame effects move 3D boxes, taps hit the 3D boxes, and a physics landing arrives as onCollisionStart. The third is a Tiled maze in 3D. Each story shows which backend is drawing. I built the examples for web with and without the WebGPU flag and checked that all three stories render under both.
  • examples/games/river_sortie: a small River Raid-style game built on the bridge, with 54 tests. This copy is silent; the section on Flutter 3.44 says why.
  • New words in the cspell dictionaries.

Nothing else in Flame changes: no package, dependency constraint or workflow.

Porting to Flame 2.0

Most of it was mechanical: synchronous add, HasGameRef, MouseMove*, list-based collision points, forge2d on Box2D v3, tiled 0.12. One change wasn't. The bridged components overrode updateTree to write their transform after their effects had run, and with the flattened traversal those overrides would have silently stopped being called. They use CustomTraversal.updateSubtree now.

Resolving on Flutter 3.44

The flutter3d packages used to ask for Dart ^3.12.2, and flutter3d_game pulled in flutter_soloud, whose native build needs hooks 2.2 and a newer meta than Flutter 3.44 pins. I didn't want to touch Flame's constraints for that, so I published 0.8.x patch releases of the stack with relaxed constraints and split the audio code so flutter3d_game no longer depends on SoLoud. It's also why River Sortie is silent here: a real audio backend would bring SoLoud back into the workspace.

The workspace resolves from pub.dev on Flutter 3.44.0 and 3.47.0. CI passes analyze on both versions, DCM, format, markdownlint and the full test run.

Open questions

  • Is the monorepo the right home, or would you rather keep it as an external package and link it from the docs? I'm happy to maintain it either way.
  • The package's prose uses British spelling (colour, behaviour), and a few public parameters are named colour. I added the words to the dictionary rather than renaming anything, but I can switch to color if you prefer.
  • For the examples site to try WebGPU, gh-pages.yml would need customArgs: --dart-define=FLUTTER3D_WEBGPU=true. I left the workflow alone.

Checklist

  • I have followed the Contributor Guide when preparing my PR.
  • I have updated/added tests for ALL new/updated/fixed functionality.
  • I have updated/added relevant documentation in docs and added dartdoc comments with ///.
  • I have updated/added relevant examples in examples or docs.

Breaking Change?

  • Yes, this PR is a breaking change.
  • No, this PR is not a breaking change.

Related Issues

Moves flame_flutter3d 0.8.4 from the flutter3d repository into the Flame
monorepo as 0.9.0-dev.0 and ports it to Flame 2.0: synchronous add,
HasGameRef, MouseMoveCallbacks, list-based collision points, the forge2d
on Box2D v3, tiled 0.12, and CustomTraversal in place of the updateTree
overrides that write a component's transform after its effects.

Adds a bridge package page covering the shared loop, the bridge plane,
post-processing through HasFlutter3d.renderSettings, and WebGL2/WebGPU
builds.
Three stories: post-processing toggled at runtime under a Flame HUD, one
game loop driving both engines, and a Tiled map stood up in 3D. Each
shows which backend draws it, WebGL2 or WebGPU in a browser.
A River Raid-style game built on the bridge: Flame components and
effects, flutter3d physics, particles and a chase camera, on one loop.
This copy is silent, since a real audio backend would bring SoLoud's
native build into the workspace.

@spydon spydon left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey, this looks really cool and it looks like a massive amount of work!
This isn't something that we would like to bring into the repository right now though since it is huge and adds a lot of maintenance.
I see that you already have it published as a package so I would recommend keeping it as maintained by you.

On another note, it's such a coincidence that you open this today, since I'm also opening a PR related to 3D today, but with a much smaller scope. You can check it out here:
#4086

@spydon spydon closed this Oct 1, 2026
@dzolotov

dzolotov commented Oct 1, 2026

Copy link
Copy Markdown
Author

Thanks for taking a look; that makes sense. 20k lines is a lot to take on, and I'd rather maintain it myself than hand you something that big.

I'll keep flame_flutter3d on pub.dev and keep it working with new Flame releases. Would you be open to a link from awesome-flame? I can open the PR there.

Funny timing with #4086. Most Flame games are 2D, so being able to drop one 3D model into the normal component tree is probably what most people will use.

@spydon

spydon commented Oct 1, 2026

Copy link
Copy Markdown
Member

Would you be open to a link from awesome-flame? I can open the PR there.

Absolutely, go for it!

spydon pushed a commit to flame-engine/awesome-flame that referenced this pull request Oct 2, 2026
Adds flame_flutter3d, a bridge between Flame and the flutter3d 3D
engine. It's published on pub.dev:
https://pub.dev/packages/flame_flutter3d

Follow-up to flame-engine/flame#4085, where it
was suggested to keep the package outside the main repo.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants