Skip to main content

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

ApproachRenders withBest forMain cost
Transform and Matrix4 on widgetsFlutter's compositorCard flips, parallax, cover-flow, 2.5DNot real 3D — no depth buffer, no lighting, painter's order only
CustomPainter with drawVerticesFlutter canvasWireframes, low-poly shapes, charts in perspectiveYou implement projection, culling, and shading in Dart on the CPU
Fragment shadersGPU, per pixelRaymarched and procedural scenes, materials, backgroundsNo meshes or scene graph; cost scales with painted area
flutter_scene on Flutter GPUImpeller, in processReal glTF models, cameras, lights, inside the widget treePreview-stage APIs; Impeller required; smaller ecosystem
flutter_gpu directlyImpeller, in processCustom render pipelines and enginesYou own buffers, shaders, and pipeline state
Filament via thermionNative renderer into a texturePhysically based rendering, image-based lighting, mature engineLarger binary, per-platform support to verify
model_viewer_plusWebViewShowing a .glb quickly, AR quick lookWebView overhead, weak interoperability with Flutter state
flutter_unity_widgetUnity runtimeGames or scenes already built in UnityTwo runtimes, heavy binary, complex build
ARCore / ARKit pluginsNative AR frameworksCamera-anchored augmented realityDevice 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_scene or Filament.
  • You need photoreal lighting or already have a Filament pipeline → Filament.
  • You just need to display a .glb with 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:

MechanismHow it worksCost
TextureThe renderer draws into a GPU texture that Flutter composites via the Texture widgetCheap; stays on the GPU, composites like any other layer
Platform viewA native view is embedded in the Flutter hierarchyExpensive; 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

  1. Author or acquire the model. Keep the polygon count low and bake lighting detail into textures rather than adding geometry.
  2. Export glTF or GLB. GLB is a single binary file, which is simpler to bundle. Agree on axis convention and units before anyone exports.
  3. 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.
  4. 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.
  5. Bundle it by declaring the asset in pubspec.yaml. Some renderers need a build-time conversion step to their own runtime format — see the flutter_scene page.
  6. 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:

MeasureHow
Frame time (UI and raster)DevTools frame chart in profile mode
Cold load timeTimestamp from route push to first rendered frame of the model
Binary size deltaflutter build apk --analyze-size before and after
InteroperabilityCan 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.

Next: 3D with Flutter GPU and flutter_scene.