Skip to content

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:

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

PropertyDescription
fillExtrusionColorBuilding wall/roof color
fillExtrusionOpacityTransparency (0.0 - 1.0)
fillExtrusionHeightBuilding height in meters
fillExtrusionBaseBase height (for elevated structures)
minZoomMinimum zoom level to show buildings

Next Steps


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.