3D Building Visualization in Flutter
This tutorial shows how to display 3D extruded buildings on your MapMetrics Flutter map — great for urban planning, real estate, and city exploration apps.
Prerequisites
Before you begin, ensure you have:
- Completed the Flutter Setup Guide
- A MapMetrics API key and style URL from the MapMetrics Portal
Enable 3D Buildings
Display 3D buildings by tilting the camera and adding a fill-extrusion layer:
dart
import 'package:flutter/material.dart';
import 'package:mapmetrics/mapmetrics.dart';
class Buildings3DScreen extends StatefulWidget {
@override
_Buildings3DScreenState createState() => _Buildings3DScreenState();
}
class _Buildings3DScreenState extends State<Buildings3DScreen> {
MapMetricsController? mapController;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('3D Buildings')),
body: MapMetrics(
styleUrl:
'https://gateway.mapmetrics.org/styles/YOUR_STYLE_ID?token=YOUR_API_KEY',
onMapCreated: (MapMetricsController controller) {
mapController = controller;
},
initialCameraPosition: CameraPosition(
target: LatLng(48.8584, 2.2945), // Eiffel Tower area
zoom: 16.0,
tilt: 60.0, // Tilt to see buildings in 3D
bearing: 45.0, // Rotate for a better perspective
),
onStyleLoaded: () {
_add3DBuildings();
},
),
floatingActionButton: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
FloatingActionButton(
heroTag: 'tilt_up',
onPressed: () => _setTilt(60.0),
child: Icon(Icons.landscape),
tooltip: '3D View',
),
SizedBox(height: 8),
FloatingActionButton(
heroTag: 'tilt_down',
onPressed: () => _setTilt(0.0),
child: Icon(Icons.map),
tooltip: '2D View',
),
],
),
);
}
void _add3DBuildings() {
// Add a 3D fill-extrusion layer for buildings
mapController?.addFillExtrusionLayer(
'3d-buildings',
'composite', // source (depends on your style)
sourceLayer: 'building',
fillExtrusionColor: '#aaaaaa',
fillExtrusionOpacity: 0.6,
fillExtrusionHeight: ['get', 'height'],
fillExtrusionBase: ['get', 'min_height'],
minZoom: 14.0,
);
}
void _setTilt(double tilt) async {
final position = await mapController?.getCameraPosition();
if (position != null) {
mapController?.animateCamera(
CameraUpdate.newCameraPosition(
CameraPosition(
target: position.target,
zoom: position.zoom,
tilt: tilt,
bearing: position.bearing,
),
),
);
}
}
}3D Buildings with Custom Colors
Color buildings based on their height for a dramatic visualization:
dart
import 'package:flutter/material.dart';
import 'package:mapmetrics/mapmetrics.dart';
class ColoredBuildings3DScreen extends StatefulWidget {
@override
_ColoredBuildings3DScreenState createState() =>
_ColoredBuildings3DScreenState();
}
class _ColoredBuildings3DScreenState extends State<ColoredBuildings3DScreen> {
MapMetricsController? mapController;
String colorScheme = 'height'; // 'height', 'blue', 'warm'
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Colored 3D Buildings')),
body: Column(
children: [
// Color scheme selector
Container(
padding: EdgeInsets.all(8),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
_schemeChip('Height', 'height'),
_schemeChip('Cool Blue', 'blue'),
_schemeChip('Warm Sunset', 'warm'),
],
),
),
Expanded(
child: MapMetrics(
styleUrl:
'https://gateway.mapmetrics.org/styles/YOUR_STYLE_ID?token=YOUR_API_KEY',
onMapCreated: (MapMetricsController controller) {
mapController = controller;
},
initialCameraPosition: CameraPosition(
target: LatLng(48.8606, 2.3376), // Louvre area
zoom: 16.0,
tilt: 55.0,
bearing: -20.0,
),
onStyleLoaded: () {
_applyColorScheme();
},
),
),
],
),
);
}
Widget _schemeChip(String label, String scheme) {
return ChoiceChip(
label: Text(label),
selected: colorScheme == scheme,
onSelected: (selected) {
if (selected) {
setState(() => colorScheme = scheme);
_applyColorScheme();
}
},
);
}
void _applyColorScheme() {
String color;
switch (colorScheme) {
case 'blue':
color = '#4a90d9';
break;
case 'warm':
color = '#e8775a';
break;
default: // height-based
color = '#8a8a8a';
}
// Remove existing layer if present
mapController?.removeLayer('3d-buildings');
mapController?.addFillExtrusionLayer(
'3d-buildings',
'composite',
sourceLayer: 'building',
fillExtrusionColor: color,
fillExtrusionOpacity: 0.7,
fillExtrusionHeight: ['get', 'height'],
fillExtrusionBase: ['get', 'min_height'],
minZoom: 14.0,
);
}
}Interactive 3D Building Explorer
Tap on buildings to see their details, rotate around them:
dart
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:mapmetrics/mapmetrics.dart';
class BuildingExplorerScreen extends StatefulWidget {
@override
_BuildingExplorerScreenState createState() =>
_BuildingExplorerScreenState();
}
class _BuildingExplorerScreenState extends State<BuildingExplorerScreen> {
MapMetricsController? mapController;
Timer? rotationTimer;
double currentBearing = 0.0;
bool isRotating = false;
final List<Map<String, dynamic>> landmarks = [
{
'name': 'Eiffel Tower Area',
'lat': 48.8584,
'lng': 2.2945,
'zoom': 17.0,
'tilt': 60.0,
},
{
'name': 'Louvre Area',
'lat': 48.8606,
'lng': 2.3376,
'zoom': 16.5,
'tilt': 55.0,
},
{
'name': 'Notre-Dame Area',
'lat': 48.8530,
'lng': 2.3499,
'zoom': 17.0,
'tilt': 65.0,
},
{
'name': 'Opera Area',
'lat': 48.8720,
'lng': 2.3316,
'zoom': 16.5,
'tilt': 55.0,
},
];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('3D Explorer')),
body: Stack(
children: [
MapMetrics(
styleUrl:
'https://gateway.mapmetrics.org/styles/YOUR_STYLE_ID?token=YOUR_API_KEY',
onMapCreated: (MapMetricsController controller) {
mapController = controller;
},
initialCameraPosition: CameraPosition(
target: LatLng(48.8584, 2.2945),
zoom: 17.0,
tilt: 60.0,
bearing: 0.0,
),
onStyleLoaded: () {
mapController?.addFillExtrusionLayer(
'3d-buildings',
'composite',
sourceLayer: 'building',
fillExtrusionColor: '#667799',
fillExtrusionOpacity: 0.7,
fillExtrusionHeight: ['get', 'height'],
fillExtrusionBase: ['get', 'min_height'],
minZoom: 14.0,
);
},
),
// Landmark buttons
Positioned(
bottom: 24,
left: 8,
right: 8,
child: SizedBox(
height: 44,
child: ListView.separated(
scrollDirection: Axis.horizontal,
itemCount: landmarks.length,
separatorBuilder: (_, __) => SizedBox(width: 8),
itemBuilder: (context, i) {
final lm = landmarks[i];
return ElevatedButton(
onPressed: () {
mapController?.animateCamera(
CameraUpdate.newCameraPosition(
CameraPosition(
target: LatLng(lm['lat'], lm['lng']),
zoom: lm['zoom'],
tilt: lm['tilt'],
bearing: currentBearing,
),
),
);
},
child: Text(lm['name'], style: TextStyle(fontSize: 12)),
);
},
),
),
),
// Rotate toggle
Positioned(
top: 16,
right: 16,
child: FloatingActionButton.small(
onPressed: _toggleRotation,
child: Icon(isRotating ? Icons.pause : Icons.rotate_right),
tooltip: 'Auto Rotate',
),
),
],
),
);
}
void _toggleRotation() {
if (isRotating) {
rotationTimer?.cancel();
setState(() => isRotating = false);
} else {
setState(() => isRotating = true);
rotationTimer = Timer.periodic(Duration(milliseconds: 50), (_) async {
currentBearing = (currentBearing + 0.5) % 360;
final pos = await mapController?.getCameraPosition();
if (pos != null) {
mapController?.moveCamera(
CameraUpdate.newCameraPosition(
CameraPosition(
target: pos.target,
zoom: pos.zoom,
tilt: pos.tilt,
bearing: currentBearing,
),
),
);
}
});
}
}
@override
void dispose() {
rotationTimer?.cancel();
super.dispose();
}
}3D Building Properties
| Property | Description |
|---|---|
fillExtrusionColor | Building wall/roof color |
fillExtrusionOpacity | Transparency (0.0 - 1.0) |
fillExtrusionHeight | Building height in meters |
fillExtrusionBase | Base height (for elevated structures) |
minZoom | Minimum zoom level to show buildings |
Next Steps
- 3D Terrain — Enable terrain elevation
- Set Pitch and Bearing — Control 3D camera angles
- Animate Camera Around Point — Orbit around buildings
Tip: 3D buildings look best at zoom levels 15-18 with a tilt of 45-65 degrees. Combine with a bearing rotation for dramatic fly-through effects.