flutter_fiber 0.2.0
flutter_fiber: ^0.2.0 copied to clipboard
Declarative 3D for Flutter, inspired by react-three-fiber procedural geometry, PBR materials, lighting, grouping, built from pure Dart data. No asset pipeline, no WebView.
flutter_fiber
Native, declarative 3D for Flutter.
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.
Logo built from the official Flutter and Dart brand marks to indicate framework compatibility. flutter_fiber is an independent, unofficial package and is not affiliated with or endorsed by Google, Flutter, or Dart.
Contents #
- What this is
- Install
- Quick start
- Core concepts
- Fiber3DCanvas
- Camera and orbit controls
- Lights
- Materials
- Textures
- Vertex colors
- Environment reflections and tone mapping
- Transparency
- Geometry reference
- Shape gallery
- Groups and transforms
- Interaction
- Loading glTF / GLB models
- Lifecycle
- Troubleshooting
- Known limitations
- Roadmap
- Credits
- License
What this is #
flutter_fiber puts a real 3D renderer inside a normal Flutter widget tree. You describe a scene with widgets (Fiber3DCanvas, Fiber3DMesh, Fiber3DGroup), and the package compiles the shaders, uploads the buffers and draws the frame itself.
There are two ways to get content into a scene:
- Procedural shapes. 22 built-in shapes (boxes, spheres, tori, polyhedra, springs, stars, lathe-revolved profiles, arrows and more) generated from Dart parameters. Because a shape is just code, it can be driven by your app's state like any other widget.
- Loaded models.
.glbfiles and.gltffiles with embedded data load into the same renderer throughFiber3DGltfLoader.
On top of that you get:
- A physically based material (
Fiber3DStandardMaterial) with roughness, metalness, clearcoat, opacity, and image textures, plus simpler Lambert, Phong, Toon, Matcap and Basic materials. - Ambient, point and hemisphere lights, environment reflections, tone mapping and sRGB color management.
- Textures from images, from rasterized text, and from any live Flutter widget (for example a Lottie animation) captured at a chosen frame rate.
- A scene graph with groups, per-frame animation callbacks, orbit camera controls, and tap/pan/pinch interaction.
- Automatic lifecycle management: rendering pauses off-screen or in the background, stops when idle, and GPU resources are released on dispose.
Why native rather than a WebView or a bundled engine:
- Lightweight. A thin OpenGL ES binding plus Dart-side math. No C++ engine in your APK/IPA.
- Full pipeline ownership. Shader compilation, buffer upload, lighting and lifecycle are plain Dart and GLSL you can read and debug.
- One widget tree. 3D content and the rest of your UI share the same state, gestures and lifecycle.
Install #
flutter pub add flutter_fiber
Or add it manually to pubspec.yaml:
dependencies:
flutter_fiber: ^0.2.0
Everything in this README is available from a single import:
import 'package:flutter_fiber/flutter_fiber.dart';
Platform support: Android is the primary, fully supported platform. iOS is experimental and physical-device-only (no simulator support: Apple deprecated OpenGL ES; this mirrors the same caveat React Native's own OpenGL-based 3D libraries document).
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. Until this is fixed upstream, 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:
<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...]).
Quick start #
A complete, working scene. Copy it into a fresh Flutter project's main.dart and run it:
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, two-finger pinch to zoom
lights: const [
Fiber3DAmbientLight(color: 0xffffff, intensity: 0.6),
Fiber3DPointLight(
color: 0xffffff,
intensity: 3.0,
position: Fiber3DVector3(3, 4, 5),
),
],
children: [
Fiber3DMesh(
geometry: Fiber3DTorusKnot(radius: 1.0, tube: 0.3),
material: Fiber3DStandardMaterial(
color: 0x3366ff,
roughness: 0.5,
metalness: 0.2,
),
showEdges: false, // see "Fiber3DMesh" below: edges are ON by default
onFrame: (elapsed, delta, transform) {
final t = delta.inMicroseconds / 1e6;
transform.rotation.y += t;
transform.rotation.x += t * 0.4;
},
),
],
),
),
);
}
}
A lit, shaded, animated, orbit-controllable 3D object in a handful of lines.
Fiber3DStandardMaterialcannot be used in aconstexpression. The simpler materials (Fiber3DBasicMaterial,Fiber3DLambertMaterial,Fiber3DPhongMaterial,Fiber3DToonMaterial,Fiber3DMatcapMaterial) and all lights can.
Core concepts #
| Piece | What it is |
|---|---|
Fiber3DCanvas |
The root widget. It owns the OpenGL context, camera, lights and render loop. Everything 3D goes in its children. |
Fiber3DMesh |
One geometry plus one material, drawn as one object. |
Fiber3DGroup |
A transform node with no geometry. Moves, rotates and scales its children together. Groups can nest. |
| Geometry | A plain Dart object (Fiber3DBox, Fiber3DSphere, ...) holding positions, normals, uvs, optional colors, and indices. |
| Material | A plain Dart object describing the surface (Fiber3DStandardMaterial, ...). |
onFrame |
A per-frame callback on meshes and groups: (Duration elapsed, Duration delta, Fiber3DObject transform). |
Fiber3DObject |
A transform: position, rotation (Euler, radians), scale, quaternion. Handed to you in onFrame. |
Placing objects. Fiber3DMesh and Fiber3DGroup have no position, rotation or scale parameters. You place and animate them through the transform passed to onFrame:
Fiber3DMesh(
geometry: Fiber3DSphere(radius: 0.5),
material: Fiber3DStandardMaterial(color: 0xff8800),
onFrame: (elapsed, delta, transform) {
transform.position.set(1.5, 0, 0); // fine to set every frame
},
)
The render loop. The canvas renders continuously while at least one mesh or group has an onFrame callback registered, or while a gesture is in progress. When none does, it draws and then stops its ticker completely, and wakes on the next touch, the next onFrame registration, or when the app/canvas becomes visible again. Two practical consequences:
- A scene with no
onFrameanywhere is drawn once (plus redraws during gestures). Anything that changes later without a gesture (a texture finishing decoding, a live texture updating) needs a mesh with anonFrame(an empty one is enough) to keep the loop running. - Position every static object with an
onFrameas shown above, which also keeps the loop alive.
What updates live and what does not.
| Change | Takes effect |
|---|---|
Mesh material (any property) |
Immediately, read every frame. |
Canvas lights, backgroundColor, toneMappingExposure |
Immediately, read every frame. |
Mesh geometry |
Not after the first frame. Geometry is uploaded to the GPU once per mesh. To change the shape, give the mesh a new Key (for example ValueKey(index)) so a fresh mesh is created. |
Canvas camera, toneMapping, colorManagement, envMapRadiance, envMapSize, envMapSamples |
Read once when the canvas is created. Give the canvas a new Key to apply new values. |
Colors are ints in 0xRRGGBB form. With colorManagement: true (the default) they are treated as sRGB, lit in linear space, and encoded back to sRGB for display.
Fiber3DCanvas #
Fiber3DCanvas(
backgroundColor: 0x1a1a1a,
camera: Fiber3DCamera(position: const Fiber3DVector3(0, 0, 6)),
orbitEnabled: true,
toneMapping: Fiber3DToneMapping.acesFilmic,
lights: const [Fiber3DAmbientLight(intensity: 0.3)],
children: [ /* meshes, groups, models */ ],
)
| Parameter | Type | Default | Description |
|---|---|---|---|
children |
List<Widget> |
[] |
Meshes, groups, Fiber3DGltfModel, Fiber3DArrow, Fiber3DWidgetTexture. |
lights |
List<dynamic> |
[] |
Fiber3DAmbientLight, Fiber3DPointLight, Fiber3DHemisphereLight. See Lights. |
camera |
Fiber3DCamera |
Fiber3DCamera() |
Initial camera. Read once at creation. |
orbitEnabled |
bool |
false |
Drag to orbit, two-finger pinch to zoom, when the gesture does not start on an interactive mesh. |
backgroundColor |
int |
0xFFFFFF |
Clear color, 0xRRGGBB. |
colorManagement |
bool |
true |
sRGB-to-linear conversion for colors and base-color textures; output is encoded back to sRGB. false skips the conversions. |
toneMapping |
Fiber3DToneMapping |
none |
Tone-mapping operator. See Environment reflections and tone mapping. |
toneMappingExposure |
double |
1.0 |
Brightness multiplier used by tone mapping (applied when toneMapping is not none). |
envMapRadiance |
Fiber3DRadianceFunction? |
Fiber3DProceduralSky().radiance |
Source of the environment used for reflections. |
envMapSize |
int |
128 |
Face size of the generated environment cube map. |
envMapSamples |
int |
32 |
Importance-sample count when prefiltering the environment. |
Fiber3DMesh #
| Parameter | Type | Default | Description |
|---|---|---|---|
geometry |
geometry object | required | Any geometry class from Geometry reference (or a Fiber3DGltfPrimitive). |
material |
material object | required | One material per mesh. |
onFrame |
void Function(Duration elapsed, Duration delta, Fiber3DObject transform)? |
null |
Per-frame callback. |
showEdges |
bool |
true |
Draws a second, unlit wireframe pass over the shaded surface. Pass false for a clean look. Ignored if the material's wireframe is already true. |
onTap |
VoidCallback? |
null |
See Interaction. |
onPan |
void Function(Offset delta)? |
null |
|
onPinch |
void Function(double scale)? |
null |
|
hitRadius |
double |
1.0 |
Radius of the bounding sphere used for hit-testing. |
enablePinchScale |
bool |
false |
Built-in two-finger pinch-to-scale. |
minPinchScale / maxPinchScale |
double |
0.3 / 3.0 |
Clamp for pinch-to-scale. |
Fiber3DMesh takes geometry and material as dynamic; passing something that is not a flutter_fiber geometry/material will not render.
Camera and orbit controls #
Fiber3DCamera(
fov: 50,
near: 0.1,
far: 2000,
position: const Fiber3DVector3(0, 0, 5),
target: const Fiber3DVector3.zero(),
up: const Fiber3DVector3(0, 1, 0),
)
| Parameter | Default | Description |
|---|---|---|
fov |
50 |
Vertical field of view, in degrees. |
aspect |
1 |
Leave at the default: the canvas renders with its own width/height. |
near / far |
0.1 / 2000 |
Clipping planes. |
position |
(0, 0, 5) |
World position. |
target |
(0, 0, 0) |
The point the camera looks at. |
up |
(0, 1, 0) |
Up direction. |
Fiber3DVector3 is an immutable (x, y, z) value: const Fiber3DVector3(1, 2, 3), const Fiber3DVector3.zero(), Fiber3DVector3.all(v), and a length getter.
With orbitEnabled: true, dragging orbits the camera and a two-finger pinch zooms. Orbiting pivots around the scene origin (0, 0, 0). A drag that starts on a mesh that handles gestures (onTap, onPan, onPinch or enablePinchScale) goes to that mesh instead.
Lights #
Pass lights to Fiber3DCanvas.lights. Lights are canvas-level and positioned in world space; they are not part of the scene graph, so groups do not move them.
lights: const [
Fiber3DAmbientLight(color: 0xffffff, intensity: 0.4),
Fiber3DPointLight(color: 0xfff4e8, intensity: 3.0, position: Fiber3DVector3(4, 5, 6)),
Fiber3DHemisphereLight(skyColor: 0x88aaff, groundColor: 0x443322, intensity: 1.0),
],
| Light | Parameters (default) | Notes |
|---|---|---|
Fiber3DAmbientLight |
color (0xffffff), intensity (1.0) |
Flat, uniform light on every surface. Multiple ambient lights add together. |
Fiber3DPointLight |
color (0xffffff), intensity (1.0, candela), distance (0 = unlimited), decay (2), position ((0,0,0)) |
Emits in all directions from a point. decay: 2 is physically correct. Up to 4 point lights are used; extras are ignored. No shadows. |
Fiber3DHemisphereLight |
skyColor (0xffffff), groundColor (0x444444), intensity (1.0), direction ((0,1,0)) |
Blends between sky and ground color by how much a surface faces direction. No position, no falloff. Only the first hemisphere light is used, and it affects Fiber3DStandardMaterial only. Pass a normalized direction. |
Lambert, Phong, Toon and Standard materials respond to lights. Fiber3DBasicMaterial ignores them. Fiber3DMatcapMaterial does not use scene lights.
Materials #
Colors are 0xRRGGBB ints. One material per mesh.
| Material | Lighting | const? |
Use for |
|---|---|---|---|
Fiber3DStandardMaterial |
Physically based (roughness/metalness) with environment reflections | No | Realistic surfaces, glass, metals, textured objects, glTF models |
Fiber3DLambertMaterial |
Matte diffuse only | Yes | Cheap, non-shiny surfaces |
Fiber3DPhongMaterial |
Diffuse + Blinn-Phong highlight | Yes | Classic glossy highlight |
Fiber3DToonMaterial |
Stepped (cel-shaded) bands | Yes | Cartoon look |
Fiber3DMatcapMaterial |
Shading from the view-space normal, no scene lights | Yes | Stylized, lighting-independent look |
Fiber3DBasicMaterial |
None: always its flat color | Yes | Unlit markers, helpers |
Fiber3DStandardMaterial #
Fiber3DStandardMaterial(
color: 0xffffff,
roughness: 0.06,
metalness: 1.0,
clearcoat: 1.0,
clearcoatRoughness: 0.03,
)
| Parameter | Default | Description |
|---|---|---|
color |
0xffffff |
Base color. Multiplies map and vertex colors, so use white to show a texture untinted. |
roughness |
1.0 |
0 = mirror-smooth, 1 = fully diffuse. |
metalness |
0.0 |
0 = non-metal, 1 = metal. |
emissive |
0x000000 |
Self-lit color, unaffected by lighting. |
emissiveIntensity |
1.0 |
Multiplier for emissive. |
wireframe |
false |
Lines only, no filled triangles. |
flatShading |
false |
One normal per triangle (faceted look) instead of smooth per-vertex normals. |
map |
null |
Base color texture (Fiber3DTexture). Treated as sRGB when colorManagement is on. |
roughnessMap |
null |
Multiplies roughness; read from the texture's green channel. |
metalnessMap |
null |
Multiplies metalness; read from the texture's blue channel (the same packed layout glTF uses, so one combined texture can be assigned to both). |
envMapIntensity |
1.0 |
Scales the environment reflection contribution. |
clearcoat |
0.0 |
Strength of a thin glossy lacquer layer. 1.0 = full. |
clearcoatRoughness |
0.0 |
Roughness of the clearcoat layer itself. |
opacity |
1.0 |
Alpha, used only when transparent is true. |
transparent |
false |
Enables alpha blending. See Transparency. |
Fiber3DLambertMaterial #
color (0xffffff), emissive (0x000000), emissiveIntensity (1.0), wireframe (false), flatShading (false).
Fiber3DPhongMaterial #
Everything in Lambert, plus specular (0x111111, highlight color) and shininess (30.0; higher = smaller, sharper highlight).
Fiber3DToonMaterial #
color, emissive, emissiveIntensity, wireframe, flatShading (same defaults as Lambert). Direct light is stepped into discrete bands. There is no specular term and no gradient-map option yet.
Fiber3DMatcapMaterial #
color (0xffffff), wireframe (false), flatShading (false). Shading comes from the view-space normal; custom matcap textures are not supported yet.
Fiber3DBasicMaterial #
color (0xffffff), wireframe (false). Always renders its flat color, regardless of lights.
Only
Fiber3DStandardMaterialsupports textures and transparency. The other materials render fully opaque.
Textures #
Textures apply through Fiber3DStandardMaterial.map (and roughnessMap / metalnessMap). They use the geometry's UV set, linear filtering, clamp-to-edge wrapping, and no mipmaps.
Image textures #
flutter_fiber takes raw image bytes from wherever you get them (assets, files, network). Decoding uses Flutter's own codec, so there is no extra dependency. Decoding is asynchronous: wait for it before using the texture.
final data = await rootBundle.load('assets/images/logo.png');
final bytes = data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes);
final texture = Fiber3DTexture(bytes);
await texture.ensureDecoded(); // texture.isReady is now true
// later, in the scene:
Fiber3DMesh(
geometry: Fiber3DBox(width: 1.4, height: 1.4, depth: 1.4),
material: Fiber3DStandardMaterial(color: 0xffffff, map: texture),
showEdges: false,
onFrame: (elapsed, delta, transform) {
transform.rotation.y += delta.inMicroseconds / 1e6;
},
)
Declare the image under flutter: assets: in your app's pubspec.yaml.
Fiber3DTexture members: Fiber3DTexture(Uint8List bytes), Fiber3DTexture.raw(Uint8List rgbaPixels, int width, int height), isReady, pixels, width, height, ensureDecoded(), setPixels(pixels, width, height), and needsUpdate.
Text textures #
Fiber3DTextTexture.render rasterizes a string once into a ready-to-use texture:
final texture = await Fiber3DTextTexture.render(
text: 'Hello',
fontSize: 64,
color: const Color(0xFFFFFFFF),
backgroundColor: const Color(0xFF3366FF),
);
| Parameter | Default | Description |
|---|---|---|
text |
required | The string to draw. |
fontSize |
48 |
Logical font size. |
color |
white | Text color (dart:ui Color). |
backgroundColor |
transparent | Fill behind the text. |
fontFamily |
null |
Optional font family. |
fontWeight |
FontWeight.normal |
|
textAlign |
TextAlign.center |
|
padding |
16 |
Logical padding around the text. |
pixelRatio |
2.0 |
Rasterization scale. Higher = sharper. |
The texture's size follows the text plus padding (texture.width, texture.height), so size the surface you map it on (for example a Fiber3DPlane) with a matching aspect ratio to avoid stretching.
Live widget textures #
Fiber3DWidgetTexture renders any Flutter widget off-screen and copies it into a texture at a chosen frame rate. This is how you put a Lottie animation, a video player, a chart or an animated counter on a 3D surface. It adds no dependency of its own: your app brings whichever package produces the widget.
Place it among the canvas's children, and keep the render loop running with an onFrame on a mesh that uses the texture:
final texture = Fiber3DTexture.raw(
Uint8List.fromList([255, 255, 255, 255]), 1, 1, // placeholder until the first capture
);
Fiber3DCanvas(
children: [
Fiber3DWidgetTexture(
texture: texture,
width: 256,
height: 256,
fps: 30,
child: const ColoredBox(
color: Colors.indigo,
child: Center(child: CircularProgressIndicator()),
),
),
Fiber3DMesh(
geometry: Fiber3DPlane(width: 2, height: 2),
material: Fiber3DStandardMaterial(color: 0xffffff, map: texture),
showEdges: false,
onFrame: (elapsed, delta, transform) {}, // keeps the render loop running
),
],
)
| Parameter | Default | Description |
|---|---|---|
child |
required | The widget to capture. |
texture |
required | The Fiber3DTexture to update. |
width / height |
256 / 256 |
Off-screen render size in logical pixels. Larger = sharper, slower. |
fps |
30 |
Capture rate, independent of the child's own animation rate. |
(dart:typed_data provides Uint8List.) For text that never changes, prefer Fiber3DTextTexture: one rasterization, no per-frame cost.
Vertex colors #
Every geometry has an optional colors list: three doubles (r, g, b in 0..1) per vertex. When set, it multiplies the material color per vertex; when null, the mesh renders with the material color alone.
Fiber3DBox builds its faces in the order +X, -X, +Y, -Y, +Z, -Z with 4 vertices per face (at the default single segment), so one color per face looks like this:
final box = Fiber3DBox(width: 1.4, height: 1.4, depth: 1.4);
const faceColors = [
[1.0, 0.2, 0.2], // +X
[0.2, 1.0, 0.2], // -X
[0.2, 0.2, 1.0], // +Y
[1.0, 1.0, 0.2], // -Y
[1.0, 0.2, 1.0], // +Z
[0.2, 1.0, 1.0], // -Z
];
box.colors = [
for (final c in faceColors)
for (var i = 0; i < 4; i++) ...c,
];
// mesh: Fiber3DMesh(geometry: box, material: Fiber3DStandardMaterial(color: 0xffffff), ...)
Create the geometry once (for example in initState) rather than inside build, and set colors before the mesh first renders: geometry is uploaded to the GPU once per mesh.
Environment reflections and tone mapping #
Fiber3DStandardMaterial reflects an environment generated from a radiance function. By default that is a built-in procedural sky. The environment is built once when the canvas is created.
Fiber3DCanvas(
toneMapping: Fiber3DToneMapping.acesFilmic,
toneMappingExposure: 1.1,
envMapSize: 256, // sharper reflections, slower start-up
envMapSamples: 64, // smoother blurred (rough) reflections
envMapRadiance: Fiber3DProceduralSky(
skyColor: const [0.55, 0.7, 0.95],
horizonColor: const [0.85, 0.85, 0.88],
groundColor: const [0.25, 0.24, 0.26],
sunDirection: const [0.4, 0.7, 0.4],
sunColor: const [6.0, 5.6, 5.0],
).radiance,
// ...
)
Fiber3DProceduralSky parameters (colors are linear radiance as [r, g, b], and may exceed 1.0):
| Parameter | Default | Description |
|---|---|---|
skyColor |
[0.4, 0.6, 0.9] |
Radiance straight up. |
horizonColor |
[0.7, 0.75, 0.8] |
Radiance at the horizon. |
groundColor |
[0.2, 0.2, 0.2] |
Radiance straight down. |
sunDirection |
[0.3, 0.6, 0.5] |
Direction toward the sun disc (normalized internally). null disables the sun. |
sunColor |
[8.0, 7.5, 6.5] |
Sun disc radiance. |
sunCosAngle |
0.999 |
Cosine of the sun disc's angular radius; closer to 1 = smaller, sharper. |
Fiber3DCheckerboardSky (columns 16, rows 8, lightColor, darkColor) is a high-contrast black/white pattern that makes roughness-dependent reflection blur easy to see. Both classes expose a radiance method with the signature (x, y, z, out) accepted by envMapRadiance; you can supply your own function of the same shape.
Per-material control: Fiber3DStandardMaterial.envMapIntensity. Loading an HDR image as the environment is not supported yet.
Tone mapping #
Fiber3DToneMapping |
Description |
|---|---|
none |
No tone mapping (default). |
linear |
Exposure only. |
reinhard |
Reinhard. |
cineon |
Cineon. |
acesFilmic |
ACES filmic curve, a good default for lit PBR scenes. |
agx |
AgX, Blender's default view transform. |
neutral |
Neutral. |
custom |
Placeholder that returns the color unchanged. |
Transparency #
Fiber3DStandardMaterial(
color: 0xffffff,
roughness: 0.05,
opacity: 0.35,
transparent: true,
clearcoat: 1.0,
clearcoatRoughness: 0.02,
)
transparent: true enables alpha blending for that mesh; opacity is the alpha. Transparent meshes do not write depth. A single transparent surface renders correctly. There is no depth-sorted transparent pass yet, so several overlapping transparent meshes can show sorting artifacts.
Geometry reference #
All geometries are plain Dart classes. Each exposes positions, normals, uvs, indices and an optional, settable colors. Counts and segment parameters are clamped to sensible minimums.
Solids #
| Shape | Parameters (defaults) | Notes |
|---|---|---|
Fiber3DBox |
width 1, height 1, depth 1, widthSegments 1, heightSegments 1, depthSegments 1 |
Centered on the origin. |
Fiber3DChamferedBox |
width 1, height 1, depth 1, chamferAmount 0.2 (clamped 0 to 0.95), segments 8 (min 2) |
Box with rounded edges and corners. 0 = sharp, near 1 = blob-like. Original to flutter_fiber. |
Fiber3DSphere |
radius 1, widthSegments 32 (min 3), heightSegments 16 (min 2), phiStart 0, phiLength 2π, thetaStart 0, thetaLength π |
Poles on the Y axis. Partial angles give sectors/caps. |
Fiber3DCylinder |
radiusTop 1, radiusBottom 1, height 1, radialSegments 32, heightSegments 1, openEnded false, thetaStart 0, thetaLength 2π |
Axis along Y, centered. |
Fiber3DCone |
radius 1, height 1, radialSegments 32, heightSegments 1, openEnded false, thetaStart 0, thetaLength 2π |
A cylinder with a zero top radius: apex toward +Y. |
Fiber3DCapsule |
radius 1, height 1, capSegments 4, radialSegments 8, heightSegments 1 |
height is the straight middle section; total length is height + 2 * radius. Axis along Y. |
Fiber3DTorus |
radius 1, tube 0.4, radialSegments 12, tubularSegments 48, arc 2π, thetaStart 0, thetaLength 2π |
Ring lies in the XY plane (axis along Z). |
Fiber3DTorusKnot |
radius 1, tube 0.4, tubularSegments 64, radialSegments 8, p 2, q 3 |
p and q should be coprime; otherwise the result is a torus link. |
Fiber3DPrism |
radius 1, height 1, sides 6 (min 3), thetaStart 0 |
A true N-sided prism with flat side faces (unlike a low-segment cylinder, whose sides are smooth-shaded). Axis along Y. |
Fiber3DWedge |
width 1, height 1, depth 1 |
Ramp: full height at the back (-Z), tapering to flat at the front (+Z). Sits on y = 0 (not centered vertically). Original to flutter_fiber. |
Fiber3DSpring |
radius 1, tubeRadius 0.15, turns 4, pitch 0.5, radialSegments 16, tubularSegments 8 |
A coil rising along +Y from y = 0. radialSegments is per turn. Original to flutter_fiber. |
Flat shapes (in the XY plane, facing +Z) #
| Shape | Parameters (defaults) | Notes |
|---|---|---|
Fiber3DPlane |
width 1, height 1, widthSegments 1, heightSegments 1 |
Good surface for textures. |
Fiber3DCircle |
radius 1, segments 32 (min 3), thetaStart 0, thetaLength 2π |
Filled disc or pie slice. |
Fiber3DRing |
innerRadius 0.5, outerRadius 1, thetaSegments 32 (min 3), phiSegments 1, thetaStart 0, thetaLength 2π |
Flat annulus. |
Fiber3DStar |
outerRadius 1, innerRadius 0.5, points 5 (min 3), depth 0 |
depth: 0 is a flat single-sided star; depth > 0 extrudes it into a solid. A high points count with close radii gives a gear silhouette. |
Polyhedra #
| Shape | Parameters (defaults) | Notes |
|---|---|---|
Fiber3DTetrahedron |
radius 1, detail 0 |
|
Fiber3DOctahedron |
radius 1, detail 0 |
|
Fiber3DDodecahedron |
radius 1, detail 0 |
|
Fiber3DIcosahedron |
radius 1, detail 0 |
|
Fiber3DPolyhedron |
baseVertices, baseIndices, radius 1, detail 0 |
Build your own from a flat [x, y, z, ...] vertex list and a triangle index list. |
detail: 0 gives sharp flat faces. Higher values subdivide each face and project the result onto a sphere of radius, with smooth normals.
Revolved #
| Shape | Parameters (defaults) | Notes |
|---|---|---|
Fiber3DLathe |
points (a small lens profile), segments 12, phiStart 0, phiLength 2π |
Revolves a 2D profile of [x, y] pairs around the Y axis: vases, bowls, baskets. x is the distance from the axis. A profile that returns to x = 0 at both ends is solid; leaving an end open gives an open vessel. |
Fiber3DArrow (a widget, not a geometry) #
Fiber3DArrow is a ready-made cylinder shaft plus cone head, packaged as a widget. Place it directly in children (or inside a group); do not pass it to Fiber3DMesh. It points along +Y with its base at the origin.
Fiber3DArrow(
length: 2.0,
headLengthRatio: 0.25,
shaftRadiusRatio: 0.35,
headRadius: 0.2,
radialSegments: 16,
shaftMaterial: Fiber3DStandardMaterial(color: 0x2952cc, roughness: 0.5),
headMaterial: Fiber3DStandardMaterial(color: 0xcc2929, roughness: 0.5),
)
| Parameter | Default | Description |
|---|---|---|
length |
2.0 |
Total length, base to tip. |
headLengthRatio |
0.25 |
Fraction of length taken by the head. |
shaftRadiusRatio |
0.35 |
Shaft radius as a fraction of headRadius. |
headRadius |
0.2 |
Radius of the head's base. |
radialSegments |
16 |
Smoothness. |
shaftMaterial / headMaterial |
grey Standard material | Must be Fiber3DStandardMaterial. |
Shape gallery #
Every procedural shape is generated live. Clone the repo and run the example app to page through them interactively, with toggles for wireframe edges and flat/smooth shading. The clips below are captured from that app.
Box #
Fiber3DBox(width: 1.4, height: 1.4, depth: 1.4)
Capsule #
Fiber3DCapsule(radius: 0.6, height: 1.2)
Cone #
Fiber3DCone(radius: 1.0, height: 1.8, radialSegments: 32)
Cylinder #
Fiber3DCylinder(radiusTop: 0.8, radiusBottom: 0.8, height: 1.8)
Icosahedron #
Fiber3DIcosahedron(radius: 1.2)
Lathe (revolved profile) #
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 #
Fiber3DRing(innerRadius: 0.5, outerRadius: 1.2)
Sphere #
Fiber3DSphere(radius: 1.2)
Torus (donut) #
Fiber3DTorus(radius: 1.0, tube: 0.4)
Torus Knot #
Fiber3DTorusKnot(radius: 1.0, tube: 0.3)
More shapes #
Fiber3DChamferedBox(width: 1.4, height: 1.4, depth: 1.4, chamferAmount: 0.4)
Fiber3DPrism(radius: 1.1, height: 1.6, sides: 6)
Fiber3DWedge(width: 1.5, height: 1.0, depth: 1.5)
Fiber3DSpring(radius: 0.8, tubeRadius: 0.15, turns: 4, pitch: 0.5)
Fiber3DStar(outerRadius: 1.2, innerRadius: 0.5, points: 5, depth: 0.3)
Fiber3DOctahedron(radius: 1.2)
Fiber3DTetrahedron(radius: 1.2)
Fiber3DDodecahedron(radius: 1.1)
Fiber3DCapsule(radius: 0.6, height: 1.2)
Fiber3DCircle(radius: 1.2, segments: 32)
Fiber3DPlane(width: 2, height: 2)
Note on video rendering: these
<video>tags render on GitHub. pub.dev's README sanitizer does not render embedded video, so on pub.dev this section shows code samples only; the clips are available in the GitHub repo.
Groups and transforms #
Group several meshes to move, rotate and scale them as one object:
Fiber3DGroup(
onFrame: (elapsed, delta, transform) {
transform.rotation.y += delta.inMicroseconds / 1e6; // spins the whole group
},
children: [
Fiber3DMesh(
geometry: Fiber3DCapsule(radius: 0.5, height: 1.5),
material: Fiber3DStandardMaterial(color: 0x3366ff),
showEdges: false,
onFrame: (elapsed, delta, transform) {
transform.position.set(0, 0.5, 0);
},
),
],
)
Groups nest, and group motion and per-mesh motion compose automatically. A mesh with no onFrame of its own simply rides along with its parent group. The toy train below is built entirely from these shapes (the clip shows it running):
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: Fiber3DVector3(3, 4, 5),
),
Fiber3DPointLight(
color: 0xffffff,
intensity: 1.2,
position: Fiber3DVector3(-3, 2, -2),
),
],
children: const [_ToyTrain()],
),
),
);
}
}
/// Cab (box) + boiler (horizontal cylinder) + chimney (nested group),
/// plus side-mounted wheels. The whole group does a slow turntable spin;
/// 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) {
// The cab's bottom sits flush at y = 0, the same ground line
// every wheel and the boiler use.
transform.position.set(-0.9, 0.8, 0);
},
);
}
}
/// The boiler: a horizontal cylinder joining the cab's front face.
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) {
// Rotating -90 degrees about Z lays the cylinder on its side.
transform.rotation.z = -pi / 2;
transform.position.set(0.6, 0.5, 0);
},
);
}
}
/// The chimney: a nested group (thin cylinder + cone) on top of the boiler.
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);
},
),
],
);
}
}
/// One side-mounted wheel: a cylinder tilted 90 degrees about X so its
/// flat faces point sideways. `side` is +1 (right) or -1 (left).
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);
transform.position.set(axleX, radius, z); // sits on the ground line
transform.rotation.x = pi / 2; // fixed tilt: axis along Z
final t = delta.inMicroseconds / 1e6;
transform.rotation.y += t * 4; // spin about its own axle
},
);
}
}
The transform (Fiber3DObject) #
The transform passed to onFrame is a Fiber3DObject.
| Member | Description |
|---|---|
position |
Mutable vector: position.set(x, y, z), position.x += 1. |
rotation |
Euler angles in radians, XYZ order. Mutable: rotation.y += t. |
scale |
Mutable vector, default (1, 1, 1). |
quaternion |
The same rotation as a quaternion. rotation and quaternion stay in sync. |
pivot |
Optional point that rotation and scale are applied around instead of the origin. |
rotateX(angle), rotateY(angle), rotateZ(angle) |
Rotate in local space. |
translateX(d), translateY(d), translateZ(d) |
Move along a local axis. |
lookAt(target) |
Rotate to face a world-space Fiber3DVector3. |
getWorldPosition() |
The object's world position as a Fiber3DVector3. |
name, userData |
Free-form label and map for your own use. |
Interaction #
Fiber3DMesh(
geometry: Fiber3DSphere(radius: 0.8),
material: Fiber3DStandardMaterial(color: 0xff8800),
hitRadius: 1.0,
onTap: () => debugPrint('tapped'),
onPan: (delta) => debugPrint('pan $delta'),
enablePinchScale: true,
minPinchScale: 0.5,
maxPinchScale: 2.5,
)
onTap,onPan(Offset delta)andonPinch(double scale)fire when the gesture starts on the mesh.scaleis cumulative since the pinch began.enablePinchScale: truemakes the mesh scale itself uniformly, clamped tominPinchScale..maxPinchScale.- Hit-testing is coarse by design: a bounding sphere of radius
hitRadiuscentered on the mesh's owntransform.position(local position, not including parent group transforms). SethitRadiusto roughly the mesh's extent. - Gestures that start on empty space go to the camera when
orbitEnabledistrue.
Loading glTF / GLB models #
class ModelViewer extends StatefulWidget {
const ModelViewer({super.key});
@override
State<ModelViewer> createState() => _ModelViewerState();
}
class _ModelViewerState extends State<ModelViewer> {
Fiber3DGltfScene? _scene;
Object? _error;
@override
void initState() {
super.initState();
_load();
}
Future<void> _load() async {
try {
final data = await rootBundle.load('assets/models/car.glb');
final bytes = data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes);
final scene = await Fiber3DGltfLoader.loadGlb(bytes);
if (!mounted) return;
setState(() => _scene = scene);
} catch (e) {
if (!mounted) return;
setState(() => _error = e);
}
}
@override
Widget build(BuildContext context) {
if (_error != null) return Center(child: Text('Failed to load model:\n$_error'));
final scene = _scene;
if (scene == null) return const Center(child: CircularProgressIndicator());
return Fiber3DCanvas(
backgroundColor: 0x1a1a1a,
toneMapping: Fiber3DToneMapping.acesFilmic,
camera: Fiber3DCamera(
position: const Fiber3DVector3(0, 2, 8),
target: const Fiber3DVector3.zero(),
),
orbitEnabled: true,
lights: const [
Fiber3DAmbientLight(color: 0xffffff, intensity: 0.3),
Fiber3DPointLight(
color: 0xffffff,
intensity: 3.0,
position: Fiber3DVector3(4, 5, 5),
),
Fiber3DPointLight(
color: 0xffffff,
intensity: 1.5,
position: Fiber3DVector3(-4, 2, -2),
),
],
children: [Fiber3DGltfModel(scene: scene, scale: 1.0)],
);
}
}
Declare the model under flutter: assets: in your app's pubspec.yaml.
| API | Description |
|---|---|
Fiber3DGltfLoader.loadGlb(Uint8List bytes) |
Parses a binary .glb. Returns Future<Fiber3DGltfScene>. |
Fiber3DGltfLoader.loadEmbeddedGltf(String jsonText) |
Parses a .gltf whose buffers and images are all base64 data URIs. You read the file text yourself. |
Fiber3DGltfModel(scene:, scale:) |
Widget that adds a loaded scene to the canvas. scale (default 1.0) is a uniform scale. |
Scale and framing. glTF files disagree on real-world units, so a model can arrive far smaller or larger than your camera expects. Use Fiber3DGltfModel.scale and the camera distance to frame it. To rotate or move a whole model, wrap it in a Fiber3DGroup and drive that group's transform in onFrame.
Supported:
.glband.gltfwith embedded (data URI) buffers/images; PNG/JPEG images embedded in the file.- The node hierarchy, with translation/rotation/scale or a single matrix per node.
- Triangle meshes with
POSITION,NORMAL,TEXCOORD_0and floatCOLOR_0attributes, indexed or not. - The metallic-roughness material: base color factor and texture, metallic and roughness factors, the combined metallic-roughness texture,
emissiveFactor, andalphaMode: "BLEND"(as transparency).
Not supported (yet):
- External
.binor image files referenced by URI. - Primitives with more than 65,535 vertices (the loader throws a
FormatException). - Non-triangle primitives (lines, points, strips, fans) are skipped. Skinning, morph targets, animations, cameras and lights in the file are ignored.
- Normal, emissive and occlusion textures;
alphaMode: "MASK"(treated as opaque); texture wrap modes and UV transforms; normalized (non-float) vertex colors. KHR_materials_*extensions (specular, transmission, clearcoat, emissive strength) are ignored, so glass built with transmission renders as a plain alpha blend.
Lifecycle #
Fiber3DCanvas manages its own GPU and battery cost automatically:
- Rendering pauses when the canvas scrolls off-screen, and resumes when it is visible again.
- Rendering pauses when the app is backgrounded, and resumes on foreground.
- The render loop fully stops (not just throttles) once nothing is animating and no gesture is in progress, and wakes on the next
onFrameregistration or touch. - All GPU resources (buffers, textures, shader programs, the GL context itself) are released on dispose. No manual cleanup is required.
Troubleshooting #
| Symptom | Likely cause |
|---|---|
Build fails with a manifest merge error mentioning application@label |
Add the tools:replace="android:label" fix from Install. |
| Every shape shows a wireframe overlay | Fiber3DMesh.showEdges defaults to true. Pass showEdges: false. |
| An object sits at the origin | There is no position parameter. Set transform.position in onFrame. |
| A texture stays white | The texture was not decoded yet and nothing kept the render loop running. await texture.ensureDecoded() before building the scene, or give a mesh an onFrame. |
| A live (widget/Lottie) texture is frozen | No mesh is keeping the render loop alive. Add an onFrame (even an empty one) to a mesh that uses the texture. |
Changing geometry on rebuild has no effect |
Geometry is uploaded once per mesh. Give the mesh a new Key. |
| A loaded model is invisible, tiny or huge | Adjust Fiber3DGltfModel.scale and the camera position. |
const error on Fiber3DStandardMaterial |
It is not a const constructor. Remove const. |
Known limitations #
- One material per mesh. Per-face or multi-material meshes are not supported; compose several meshes in a group instead.
- 16-bit indices. A single geometry or glTF primitive is limited to 65,535 vertices.
- No depth-sorted transparency. Overlapping transparent meshes can show sorting artifacts.
- Textures use UV set 0, clamp-to-edge wrapping, linear filtering, no mipmaps and no UV transform, and only
Fiber3DStandardMaterialaccepts them. Normal, emissive and occlusion maps are not implemented.roughnessMapandmetalnessMapfollow the glTF packing convention but have had limited testing. - Lights: ambient, point (max 4) and hemisphere (1, Standard material only). No directional or spot lights, no shadows.
- Toon and Matcap use built-in shading; gradient-map and matcap textures are not supported yet.
- Environment comes from a radiance function (procedural sky provided); loading HDR images is not supported yet.
- Hit-testing is a coarse bounding sphere at the mesh's local position.
- glTF: see the unsupported list above.
- Platforms: Android is the primary target; iOS is experimental and device-only.
Roadmap #
- Text and logo extrusion (next). A port of three.js's text/shape pipeline: glyph outlines and SVG-style shapes turned into real 3D geometry at runtime.
- Under consideration, in no particular order: depth-sorted transparency, multi-material meshes, matcap/gradient textures,
KHR_materials_*glTF extensions, mipmaps and wrap modes, normal maps, HDR environment loading, glTF animation.
Credits #
Much of the geometry and shading code is a port of three.js (MIT licensed), adapted to Dart and OpenGL ES. flutter_fiber is inspired by react-three-fiber. Fiber3DChamferedBox, Fiber3DWedge, Fiber3DSpring, Fiber3DStar, Fiber3DPrism and Fiber3DArrow are original to this package.
License #
MIT. Free to use, modify, and ship in commercial products. See LICENSE.
Author #
Built by Darwin (CoCoNuT-sTuDiOs).