3D Terrain in Flutter
This tutorial shows how to enable 3D terrain elevation on your MapMetrics Flutter map — hills, mountains, and valleys rendered in 3D for immersive map experiences.
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 Terrain
Activate terrain elevation with a tilted camera to see mountains in 3D:
dart
import 'package:flutter/material.dart';
import 'package:mapmetrics/mapmetrics.dart';
class Terrain3DScreen extends StatefulWidget {
@override
_Terrain3DScreenState createState() => _Terrain3DScreenState();
}
class _Terrain3DScreenState extends State<Terrain3DScreen> {
MapMetricsController? mapController;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('3D Terrain')),
body: MapMetrics(
styleUrl:
'https://gateway.mapmetrics.org/styles/YOUR_STYLE_ID?token=YOUR_API_KEY',
onMapCreated: (MapMetricsController controller) {
mapController = controller;
},
initialCameraPosition: CameraPosition(
target: LatLng(46.8182, 8.2275), // Swiss Alps
zoom: 10.0,
tilt: 60.0,
bearing: 30.0,
),
onStyleLoaded: () {
_enableTerrain();
},
),
floatingActionButton: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
FloatingActionButton(
heroTag: '3d',
onPressed: () => _setView(60.0),
child: Icon(Icons.terrain),
tooltip: '3D View',
),
SizedBox(height: 8),
FloatingActionButton(
heroTag: '2d',
onPressed: () => _setView(0.0),
child: Icon(Icons.map),
tooltip: '2D View',
),
],
),
);
}
void _enableTerrain() {
// Add terrain source
mapController?.addRasterDemSource(
'terrain-source',
'https://gateway.mapmetrics.org/terrain/{z}/{x}/{y}.png',
tileSize: 256,
);
// Enable terrain with exaggeration
mapController?.setTerrain('terrain-source', exaggeration: 1.5);
}
void _setView(double tilt) async {
final pos = await mapController?.getCameraPosition();
if (pos != null) {
mapController?.animateCamera(
CameraUpdate.newCameraPosition(
CameraPosition(
target: pos.target,
zoom: pos.zoom,
tilt: tilt,
bearing: pos.bearing,
),
),
);
}
}
}Mountain Explorer with Location Buttons
Jump between famous mountain locations:
dart
import 'package:flutter/material.dart';
import 'package:mapmetrics/mapmetrics.dart';
class MountainExplorerScreen extends StatefulWidget {
@override
_MountainExplorerScreenState createState() =>
_MountainExplorerScreenState();
}
class _MountainExplorerScreenState extends State<MountainExplorerScreen> {
MapMetricsController? mapController;
final List<Map<String, dynamic>> mountains = [
{
'name': 'Swiss Alps',
'lat': 46.8182,
'lng': 8.2275,
'zoom': 10.0,
'bearing': 30.0,
},
{
'name': 'Mont Blanc',
'lat': 45.8326,
'lng': 6.8652,
'zoom': 12.0,
'bearing': 150.0,
},
{
'name': 'Matterhorn',
'lat': 45.9763,
'lng': 7.6586,
'zoom': 13.0,
'bearing': 220.0,
},
{
'name': 'Dolomites',
'lat': 46.4102,
'lng': 11.8440,
'zoom': 11.0,
'bearing': 90.0,
},
{
'name': 'Pyrenees',
'lat': 42.6953,
'lng': 0.0414,
'zoom': 10.0,
'bearing': 45.0,
},
];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Mountain 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(46.8182, 8.2275),
zoom: 10.0,
tilt: 60.0,
bearing: 30.0,
),
onStyleLoaded: () {
mapController?.addRasterDemSource(
'terrain-source',
'https://gateway.mapmetrics.org/terrain/{z}/{x}/{y}.png',
tileSize: 256,
);
mapController?.setTerrain('terrain-source', exaggeration: 1.5);
},
),
// Mountain selector
Positioned(
bottom: 16,
left: 8,
right: 8,
child: SizedBox(
height: 44,
child: ListView.separated(
scrollDirection: Axis.horizontal,
itemCount: mountains.length,
separatorBuilder: (_, __) => SizedBox(width: 8),
itemBuilder: (context, i) {
final m = mountains[i];
return ElevatedButton.icon(
onPressed: () {
mapController?.animateCamera(
CameraUpdate.newCameraPosition(
CameraPosition(
target: LatLng(m['lat'], m['lng']),
zoom: m['zoom'],
tilt: 60.0,
bearing: m['bearing'],
),
),
);
},
icon: Icon(Icons.terrain, size: 16),
label: Text(m['name'], style: TextStyle(fontSize: 12)),
);
},
),
),
),
],
),
);
}
}Terrain with Exaggeration Slider
Let users control how dramatically terrain is displayed:
dart
import 'package:flutter/material.dart';
import 'package:mapmetrics/mapmetrics.dart';
class TerrainSliderScreen extends StatefulWidget {
@override
_TerrainSliderScreenState createState() => _TerrainSliderScreenState();
}
class _TerrainSliderScreenState extends State<TerrainSliderScreen> {
MapMetricsController? mapController;
double exaggeration = 1.5;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Terrain Exaggeration')),
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(46.8182, 8.2275),
zoom: 10.0,
tilt: 60.0,
),
onStyleLoaded: () {
mapController?.addRasterDemSource(
'terrain-source',
'https://gateway.mapmetrics.org/terrain/{z}/{x}/{y}.png',
tileSize: 256,
);
mapController?.setTerrain(
'terrain-source', exaggeration: exaggeration);
},
),
// Exaggeration slider
Positioned(
bottom: 24,
left: 16,
right: 16,
child: Card(
child: Padding(
padding: EdgeInsets.all(12),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'Exaggeration: ${exaggeration.toStringAsFixed(1)}x',
style: TextStyle(fontWeight: FontWeight.bold),
),
Slider(
value: exaggeration,
min: 0.0,
max: 3.0,
divisions: 30,
label: '${exaggeration.toStringAsFixed(1)}x',
onChanged: (value) {
setState(() {
exaggeration = value;
});
mapController?.setTerrain(
'terrain-source',
exaggeration: value,
);
},
),
Text(
'0x = flat | 1x = real | 3x = dramatic',
style: TextStyle(color: Colors.grey, fontSize: 11),
),
],
),
),
),
),
],
),
);
}
}Terrain Parameters
| Parameter | Range | Description |
|---|---|---|
exaggeration | 0.0 - 3.0 | Multiplier for terrain height. 1.0 = real scale |
tileSize | 256 / 512 | Resolution of terrain tiles |
tilt | 0 - 85 | Camera tilt angle for 3D effect |
Next Steps
- 3D Buildings — Add extruded buildings
- Satellite Terrain — Satellite imagery with elevation
- Set Pitch and Bearing — Camera angle controls
Tip: Set exaggeration to 1.5 for a good balance between realism and visibility. Values above 2.0 create a dramatic effect but can distort distances at close zoom levels.