Skip to content

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:

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

ParameterRangeDescription
exaggeration0.0 - 3.0Multiplier for terrain height. 1.0 = real scale
tileSize256 / 512Resolution of terrain tiles
tilt0 - 85Camera tilt angle for 3D effect

Next Steps


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.