// a native, declarative 3D package for Flutter

Flutterflutter_fiber

Build 3D scenes the way you build widget trees: procedural shapes from Dart parameters, PBR materials, lights, image/text/live-widget textures, and glTF/GLB models, rendered directly against OpenGL ES. Inspired by react-three-fiber. No WebView. No bundled engine.

$ flutter pub add flutter_fiber

// why

3D built from Dart data, not loaded from a file

Most 3D-on-Flutter packages point a viewer at a finished .glb file either a WebView running three.js, or a bundled native engine. That's great when you already have a finished asset. flutter_fiber is for everything before and beyond that.

Data-driven, live app-state 3D

Every shape is Dart data, not a loaded file. A fitness app's progress ring can thicken with weekly activity, a fintech dashboard's shape can shift with portfolio risk driven by the same state that drives the rest of your UI.

Composable primitives

Fiber3DGroup chains multiple shapes into one rigid composite a capsule + two spheres becomes a limb, a cone + cylinder + sphere becomes a party hat built directly in widget code, animated as one unit or independently per part.

No bundled engine

No WebView, no Filament-class native engine shipped just for one 3D object. A thin OpenGL ES binding and plain Dart math real APK/IPA size and battery savings when you only need one thing to render.

Full pipeline ownership

Shader compilation, buffer upload, lighting, and lifecycle are plain Dart and GLSL you can read and patch yourself not a black box waiting on someone else's engine team.

// install & quick start

Install & Quick Start

Add the package with the Flutter CLI:

bash
flutter pub add flutter_fiber

Or add it manually to pubspec.yaml:

yaml
dependencies:
  flutter_fiber: ^0.2.0

Platform support: Android is the primary, fully supported target. iOS is experimental and physical-device-only no simulator support, since Apple deprecated OpenGL ES.

Required Android manifest fix (temporary, will be removed in a future release): flutter_fiber's native OpenGL binding bundles its own small manifest, which currently conflicts with your app's android:label. Add tools:replace="android:label" to your android/app/src/main/AndroidManifest.xml's <application> tag, and the xmlns:tools namespace to the root <manifest> tag:

xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
                xmlns:tools="http://schemas.android.com/tools">
                <application
                    android:label="your_app_name"
                    android:name="${applicationName}"
                    android:icon="@mipmap/ic_launcher"
                    tools:replace="android:label">
                    <!-- ...rest of your existing <application> content stays unchanged... -->
                </application>
            </manifest>

Without this, the build fails with a manifest merge error: Attribute application@label value=(...) is also present at [com.futouapp:threeegl...].

A complete, working scene a lit, shaded, animated, orbit-controllable object in one file:

dart
import 'package:flutter/material.dart';
import 'package:flutter_fiber/flutter_fiber.dart';


void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: const Text('flutter_fiber')),
        body: Fiber3DCanvas(
          backgroundColor: 0x1e1e1e,
          camera: Fiber3DCamera(
            position: const Fiber3DVector3(0, 0, 5),
            target: const Fiber3DVector3.zero(),
          ),
          orbitEnabled: true, // drag to orbit, pinch to zoom
          lights: [
            const Fiber3DAmbientLight(color: 0xffffff, intensity: 0.6),
            Fiber3DPointLight(
              color: 0xffffff,
              intensity: 3.0,
              position: const Fiber3DVector3(3, 4, 5),
            ),
          ],
          children: [
            Fiber3DMesh(
              geometry: Fiber3DTorusKnot(radius: 1.0, tube: 0.3),
              material: Fiber3DStandardMaterial(
                color: 0x3366ff,
                roughness: 0.5,
                metalness: 0.2,
              ),
              onFrame: (elapsed, delta, transform) {
                final t = delta.inMicroseconds / 1e6;
                transform.rotation.y += t;
                transform.rotation.x += t * 0.4;
              },
            ),
          ],
        ),
      ),
    );
  }
}

Upgrading from 0.1.x: version 0.2.0 has three breaking changes.

  • Remove const in front of Fiber3DStandardMaterial(...). It now holds texture references, so its constructor is no longer const.
  • Fiber3DCanvas no longer has skyColor / groundColor. Add a Fiber3DHemisphereLight to lights instead.
  • Lighting is now physically based, with environment reflections, so existing scenes will look different. Re-tune light intensities, roughness and metalness.

// shape gallery

Shape Gallery

Every shape below is generated live in the example app clone the repo and run it to page through all of them interactively, with toggles for wireframe edges and flat/smooth shading.

Box

dart
Fiber3DBox(width: 1.4, height: 1.4, depth: 1.4)

Capsule

dart
Fiber3DCapsule(radius: 0.6, height: 1.2)

Cone

dart
Fiber3DCone(radius: 1.0, height: 1.8, radialSegments: 32)

Cylinder

dart
Fiber3DCylinder(radiusTop: 0.8, radiusBottom: 0.8, height: 1.8)

Icosahedron

dart
Fiber3DIcosahedron(radius: 1.2)

Lathe (basket)

dart
Fiber3DLathe(
  points: const [
    [0.0, -0.9],
    [0.5, -0.85],
    [0.85, -0.5],
    [0.95, 0.0],
    [0.85, 0.5],
    [0.5, 0.85],
  ],
  segments: 24,
)

Ring

dart
Fiber3DRing(innerRadius: 0.5, outerRadius: 1.2)

Sphere

dart
Fiber3DSphere(radius: 1.2)

Torus (donut)

dart
Fiber3DTorus(radius: 1.0, tube: 0.4)

Torus Knot

dart
Fiber3DTorusKnot(radius: 1.0, tube: 0.3)

Circle

dart
Fiber3DCircle(radius: 1.2, segments: 32)

Octahedron

dart
Fiber3DOctahedron(radius: 1.2)

Tetrahedron

dart
Fiber3DTetrahedron(radius: 1.2)

Star

dart
Fiber3DStar(outerRadius: 1.2, innerRadius: 0.5, points: 5, depth: 0.3)

Prism (hexagonal)

dart
Fiber3DPrism(radius: 1.1, height: 1.6, sides: 6)

Spring

dart
Fiber3DSpring(radius: 0.8, tubeRadius: 0.15, turns: 4, pitch: 0.5)

Chamfered Box

dart
Fiber3DChamferedBox(width: 1.4, height: 1.4, depth: 1.4, chamferAmount: 0.4)

Arrow (widget)

dart
// Fiber3DArrow is a widget, not a geometry:
// place it directly in children, not inside Fiber3DMesh.
Fiber3DArrow(
  length: 2.0,
  shaftMaterial: material,
  headMaterial: material,
)

Plane

dart
Fiber3DPlane(width: 2, height: 2)

Dodecahedron

dart
Fiber3DDodecahedron(radius: 1.1)

Polyhedron (custom)

dart
Fiber3DPolyhedron(
  // flat [x, y, z, x, y, z, ...] vertex list
  const [1.0, 1.0, 1.0, -1.0, -1.0, 1.0, -1.0, 1.0, -1.0, 1.0, -1.0, -1.0],
  // triangles, as indices into the vertex list above
  const [2, 1, 0, 0, 3, 2, 1, 3, 0, 2, 3, 1],
  radius: 1.2,
  detail: 0,
)

Wedge (ramp)

dart
Fiber3DWedge(width: 1.5, height: 1.0, depth: 1.5)

// composing shapes

Combining Shapes with Fiber3DGroup

Primitives aren't just decoration they're building blocks. Group several meshes to move, rotate, and scale them together as one composite object, the same way most real 3D content is actually built. This toy train combines a box, two cylinders, and a cone into one rigid group that spins as a whole, while each wheel also spins independently about its own axle.

dart
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter_fiber/flutter_fiber.dart';

void main() {
  runApp(const FiberTrainDemo());
}

class FiberTrainDemo extends StatelessWidget {
  const FiberTrainDemo({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: Scaffold(
        appBar: AppBar(
          title: const Text('flutter_fiber toy train (composed shapes)'),
          backgroundColor: const Color(0xFF828282),
        ),
        body: Fiber3DCanvas(
          backgroundColor: 0x87ceeb,
          camera: Fiber3DCamera(
            position: const Fiber3DVector3(0, 1.0, 7),
            target: const Fiber3DVector3(0, 0.3, 0),
          ),
          orbitEnabled: true,
          lights: [
            const Fiber3DAmbientLight(color: 0xffffff, intensity: 0.7),
            Fiber3DPointLight(
              color: 0xffffff,
              intensity: 3.0,
              position: const Fiber3DVector3(3, 4, 5),
            ),
            Fiber3DPointLight(
              color: 0xffffff,
              intensity: 1.2,
              position: const Fiber3DVector3(-3, 2, -2),
            ),
          ],
          children: const [_ToyTrain()],
        ),
      ),
    );
  }
}

/// Cab (box), boiler (horizontal cylinder), chimney (nested group), and
/// six wheels — one Fiber3DGroup, whole train spins as one turntable
/// unit while each wheel also spins independently about its own axle.
class _ToyTrain extends StatelessWidget {
  const _ToyTrain();

  @override
  Widget build(BuildContext context) {
    return Fiber3DGroup(
      onFrame: (elapsed, delta, transform) {
        final t = delta.inMicroseconds / 1e6;
        transform.rotation.y += t * 0.3;
      },
      children: const [
        _TrainBody(),
        _TrainBoiler(),
        _Chimney(),
        _Wheel(axleX: 1.0, radius: 0.32, side: 1),
        _Wheel(axleX: 1.0, radius: 0.32, side: -1),
        _Wheel(axleX: -0.9, radius: 0.5, side: 1),
        _Wheel(axleX: -0.9, radius: 0.5, side: -1),
        _Wheel(axleX: 0, radius: 0.32, side: 1),
        _Wheel(axleX: 0, radius: 0.32, side: -1),
      ],
    );
  }
}

class _TrainBody extends StatelessWidget {
  const _TrainBody();
  @override
  Widget build(BuildContext context) {
    return Fiber3DMesh(
      geometry: Fiber3DBox(width: 1.6, height: 1.4, depth: 1.3),
      material: Fiber3DStandardMaterial(
        color: 0xcc1111,
        roughness: 0.4,
        metalness: 0.3,
        flatShading: true,
      ),
      onFrame: (elapsed, delta, transform) {
        transform.position.set(-0.9, 0.8, 0);
      },
    );
  }
}

class _TrainBoiler extends StatelessWidget {
  const _TrainBoiler();
  @override
  Widget build(BuildContext context) {
    return Fiber3DMesh(
      geometry: Fiber3DCylinder(
        radiusTop: 0.5,
        radiusBottom: 0.5,
        height: 2.0,
        radialSegments: 24,
      ),
      material: Fiber3DStandardMaterial(
        color: 0xcc1111,
        roughness: 0.4,
        metalness: 0.3,
        flatShading: true,
      ),
      onFrame: (elapsed, delta, transform) {
        transform.rotation.z = -pi / 2;
        transform.position.set(0.6, 0.5, 0);
      },
    );
  }
}

class _Chimney extends StatelessWidget {
  const _Chimney();
  @override
  Widget build(BuildContext context) {
    return Fiber3DGroup(
      children: [
        Fiber3DMesh(
          geometry: Fiber3DCylinder(
            radiusTop: 0.12,
            radiusBottom: 0.14,
            height: 0.35,
            radialSegments: 16,
          ),
          material: Fiber3DStandardMaterial(
            color: 0x1a1a1a,
            roughness: 0.5,
            metalness: 0.4,
            flatShading: true,
          ),
          onFrame: (elapsed, delta, transform) {
            transform.position.set(1.0, 1.175, 0);
          },
        ),
        Fiber3DMesh(
          geometry: Fiber3DCone(radius: 0.16, height: 0.18, radialSegments: 16),
          material:Fiber3DStandardMaterial(
            color: 0x1a1a1a,
            roughness: 0.5,
            metalness: 0.4,
            flatShading: true,
          ),
          onFrame: (elapsed, delta, transform) {
            transform.position.set(1.0, 1.44, 0);
          },
        ),
      ],
    );
  }
}

class _Wheel extends StatelessWidget {
  final double axleX;
  final double radius;
  final int side;

  const _Wheel({required this.axleX, required this.radius, required this.side});

  @override
  Widget build(BuildContext context) {
    const wheelThickness = 0.28;
    const bodyHalfDepth = 0.5;

    return Fiber3DMesh(
      geometry: Fiber3DCylinder(
        radiusTop: radius,
        radiusBottom: radius,
        height: wheelThickness,
        radialSegments: 20,
      ),
      material:Fiber3DStandardMaterial(
        color: 0x1a3d33,
        roughness: 0.5,
        metalness: 0.3,
        flatShading: true,
      ),
      onFrame: (elapsed, delta, transform) {
        final z = side * (bodyHalfDepth + wheelThickness / 2);
        final y = radius;
        transform.position.set(axleX, y, z);
        transform.rotation.x = pi / 2;
        final t = delta.inMicroseconds / 1e6;
        transform.rotation.y += t * 4;
      },
    );
  }
}

// render quality

Built for quality, not just shapes

Everything above renders through a physically based pipeline: ACES Filmic tone mapping, sRGB color management, and an environment map generated from a procedural sky. The same Standard material covers matte, metal, clearcoat and glass.

Grid of spheres with roughness from 0.0 to 1.0 across five columns and metalness across three rows
One material, 15 variations: five roughness steps by three metalness steps, all with the same pink-red color.
Matte, glossy (full metal plus clearcoat) and glass, on a sphere and a chamfered box. Note: there is no depth-sorted transparency yet, so overlapping transparent meshes can show sorting artifacts.

// api reference

Key API Options

The most commonly used options, what they actually do, and a copy-pasteable example for each.

showEdges

Fiber3DMesh

Draws a second, unlit wireframe pass over the shaded surface the shaded-fill-plus-visible-edges look seen in three.js's own geometry viewers. Ignored if the material's wireframe is already true (that mode is lines-only).

dart
Fiber3DMesh(
  geometry: Fiber3DTorus(radius: 1.0, tube: 0.4),
  material: Fiber3DStandardMaterial(color: 0x3366ff),
  showEdges: true,
)

material.wireframe

Fiber3DStandardMaterial / Fiber3DBasicMaterial

Renders lines only no filled triangles at all.

dart
Fiber3DStandardMaterial(
  color: 0x3366ff,
  wireframe: true,
),
  ---
  OR
  ---
  Fiber3DBasicMaterial(
  color: 0x3366ff,
  wireframe: true,
)

material.flatShading

Fiber3DStandardMaterial

false (default): smooth, interpolated per-vertex normals. true: one flat normal per triangle the faceted, low-poly "gem" look. Pure shading-mode toggle; the underlying geometry data is untouched.

dart
Fiber3DStandardMaterial(
  color: 0x3366ff,
  flatShading: true,
)

enablePinchScale

Fiber3DMesh

Opt-in two-finger pinch-to-scale on that mesh, off by default. Anchored to the scale at gesture start so it never compounds mid-gesture, and clamped by minPinchScale/maxPinchScale so it can't shrink to invisible or grow unbounded.

dart
Fiber3DMesh(
  geometry: Fiber3DSphere(radius: 1.2),
  material: Fiber3DStandardMaterial(color: 0x3366ff),
  enablePinchScale: true,
  minPinchScale: 0.4,
  maxPinchScale: 2.5,
)

Fiber3DHemisphereLight

Fiber3DCanvas lights

A soft sky-and-ground light. Blends skyColor and groundColor by how much each surface faces direction, so undersides aren't pure black. No position, no falloff. Only the first hemisphere light is used, and it affects Fiber3DStandardMaterial only. It replaces the skyColor / groundColor parameters that Fiber3DCanvas had in 0.1.x.

dart
Fiber3DCanvas(
  lights: [
    Fiber3DHemisphereLight(
      skyColor: 0x88aaff,
      groundColor: 0x443322,
      intensity: 1.0,
    ),
  ],
  // ...
)

orbitEnabled

Fiber3DCanvas

Drag to orbit the camera, pinch to zoom, when no mesh under the gesture claims it first.

dart
Fiber3DCanvas(
  orbitEnabled: true,
  // ...
)

// lifecycle

Lifecycle

Fiber3DCanvas manages its own GPU and battery cost automatically nothing to wire up yourself.

Off-screen pause

Rendering pauses when the canvas scrolls off-screen, and resumes automatically when it's visible again.

Background pause

Rendering pauses when the app is backgrounded, and resumes on foreground no wasted GPU or battery while the user is elsewhere.

Idle-frame stop

The render loop fully stops not just throttles once nothing is animating and no gesture is in progress. It wakes on the next onFrame registration or touch.

Full disposal

All GPU resources buffers, shaders, the GL context itself are released on dispose. No manual cleanup required.

// roadmap

Roadmap

glTF / GLB models and procedural shapes both work today. Here's what's next, and what's under consideration (in no particular order).

Text and logo extrusion (next)

A port of three.js's text and shape pipeline: glyph outlines and SVG-style shapes turned into real 3D geometry at runtime.

Transparency and materials

Depth-sorted transparency, multi-material meshes, normal maps, mipmaps and wrap modes, and matcap / gradient textures.

glTF

KHR_materials_* extensions and animation.

Environment

HDR environment map loading.