3D rendering overview
Flutter has no single "3D mode". Putting 3D on screen means picking a renderer and living with its trade-offs, so this page is about the choice. The pages that follow cover each path in detail.
Contents
- Choosing an approach
- Decision shortcut
- Prerequisites
- Integration mechanisms
- The asset pipeline
- Conventions and units
- Worked example
- When not to use 3D
Choosing an approach
| Approach | Renders with | Best for | Main cost |
|---|---|---|---|
Transform and Matrix4 on widgets | Flutter's compositor | Card flips, parallax, cover-flow, 2.5D | Not real 3D — no depth buffer, no lighting, painter's order only |
CustomPainter with drawVertices | Flutter canvas | Wireframes, low-poly shapes, charts in perspective | You implement projection, culling, and shading in Dart on the CPU |
| Fragment shaders | GPU, per pixel | Raymarched and procedural scenes, materials, backgrounds | No meshes or scene graph; cost scales with painted area |
flutter_scene on Flutter GPU | Impeller, in process | Real glTF models, cameras, lights, inside the widget tree | Preview-stage APIs; Impeller required; smaller ecosystem |
flutter_gpu directly | Impeller, in process | Custom render pipelines and engines | You own buffers, shaders, and pipeline state |
Filament via thermion | Native renderer into a texture | Physically based rendering, image-based lighting, mature engine | Larger binary, per-platform support to verify |
model_viewer_plus | WebView | Showing a .glb quickly, AR quick look | WebView overhead, weak interoperability with Flutter state |
flutter_unity_widget | Unity runtime | Games or scenes already built in Unity | Two runtimes, heavy binary, complex build |
| ARCore / ARKit plugins | Native AR frameworks | Camera-anchored augmented reality | Device support, permissions, platform-specific code |
The first three are covered in Custom painting and Fragment shaders. The Flutter GPU path is in 3D with Flutter GPU and flutter_scene, and the external engines in Embedded 3D renderers.
Decision shortcut
- The depth is decorative — a tilting card, a parallax header → widget transforms.
- The subject is procedural — a material, an abstract scene, a background → fragment shader.
- The user manipulates a model you exported from a 3D tool →
flutter_sceneor Filament. - You need photoreal lighting or already have a Filament pipeline → Filament.
- You just need to display a
.glbwith minimal effort and can accept a WebView →model_viewer_plus. - It is a game, or the content already exists in Unity → Unity.
- It must sit in the camera feed → an AR plugin.
Prerequisites
Impeller. The Flutter GPU path requires the Impeller backend. Confirm the backend in use for your target platforms and the Flutter version you ship, and have a plan for any platform that falls back — the feature must degrade to a static image or a simpler view rather than crashing.
A real device. GPU behaviour, memory limits, and thermal throttling do not reproduce on a simulator. Baseline the work on one mid-range Android phone, not only on a flagship or a desktop.
Platform support matrix. Every package in the table above supports a different subset of Android, iOS, desktop, and web. Fill in the matrix from each package's own documentation at the time you adopt it, and record it in the project — this is the detail most likely to be out of date in any tutorial, including this one.
Integration mechanisms
External renderers reach the screen in one of two ways, and the difference is a real performance decision:
| Mechanism | How it works | Cost |
|---|---|---|
| Texture | The renderer draws into a GPU texture that Flutter composites via the Texture widget | Cheap; stays on the GPU, composites like any other layer |
| Platform view | A native view is embedded in the Flutter hierarchy | Expensive; forces the engine to interleave native and Flutter composition, and can degrade scrolling |
Prefer texture-based integration when the package offers it. If you must use a platform view, keep it stationary and avoid putting it inside a scrolling list.
The asset pipeline
- Author or acquire the model. Keep the polygon count low and bake lighting detail into textures rather than adding geometry.
- Export glTF or GLB. GLB is a single binary file, which is simpler to bundle. Agree on axis convention and units before anyone exports.
- Validate the file with a glTF validator before it reaches the app. Most "the model does not load" reports are malformed or unsupported-extension files.
- Compress. Resize textures to what the screen actually shows, strip unused UV sets and vertex colours, and consider mesh compression if the package supports it.
- Bundle it by declaring the asset in
pubspec.yaml. Some renderers need a build-time conversion step to their own runtime format — see theflutter_scenepage. - Measure cold model load time separately from frame time. They have different causes and different fixes.
Budgets to start from, then adjust with measurements: tens of thousands of triangles rather than hundreds of thousands, textures at 1024 or 2048 pixels rather than 4096, and a single material where possible.
Conventions and units
Fix these once, in writing, or you will spend a day on a model that appears upside down and a kilometre away:
- Axis convention. glTF is Y-up, right-handed, with -Z forward. Blender is Z-up internally and converts on export — check the export settings rather than assuming.
- Units. glTF uses metres. Set your modelling scale to match.
- Origin. Place the model's origin where you want it to rotate, usually the centre of its base or its bounding box.
- Winding and normals. Consistent counter-clockwise winding; recalculate normals before export. Inverted normals produce faces that disappear when back faces are culled.
Worked example
The 3D pages share one example so the comparison is concrete: a product viewer — a full-screen model with orbit and pinch-zoom, tap-to-highlight on a part, and a Flutter UI overlay (title, chips, bottom sheet) composited above it as ordinary widgets.
It is built twice, with flutter_scene and with model_viewer_plus, and
compared on:
| Measure | How |
|---|---|
| Frame time (UI and raster) | DevTools frame chart in profile mode |
| Cold load time | Timestamp from route push to first rendered frame of the model |
| Binary size delta | flutter build apk --analyze-size before and after |
| Interoperability | Can the overlay read the camera angle? Can a tap select a part? Does theming apply? |
Reporting those four numbers is more useful than any general claim about which renderer is faster.
When not to use 3D
3D is expensive in binary size, battery, engineering time, and accessibility. It is the wrong choice when:
- A well-shot image sequence or a short video would communicate the same thing.
- The content is a chart — perspective makes quantitative comparison harder, not easier.
- The app must run on low-end devices where the GPU budget is already spent on the interface.
- Nobody on the team owns the asset pipeline. Models age, get re-exported, and break; without an owner that becomes a permanent bug source.
If you do ship 3D, provide a non-3D fallback path for unsupported devices and for users who have reduced-motion enabled.