> For the complete documentation index, see [llms.txt](https://docs.mapbox.com/android/maps/llms.txt)

# Interactive 3D model source

![Control a 3D model's doors, hood, trunk, and colors by updating model source properties.](https://docs.mapbox.com/android/assets/ideal-img/maps-compose-examples-display-model-source.399fedf.480.png)

This example demonstrates how to interactively control a 3D car model by modifying [`ModelSource`](https://docs.mapbox.com/android/maps/api/latest/mapbox-maps-android/com.mapbox.maps.extension.style.sources.generated/-model-source/) properties directly using the **Mapbox Maps SDK for Android**.

The car is loaded into a `ModelSource` with [`modelNodeOverride`](https://docs.mapbox.com/android/maps/api/latest/mapbox-maps-android/com.mapbox.maps.extension.style.sources.generated/model-node-override.html) and [`modelMaterialOverride`](https://docs.mapbox.com/android/maps/api/latest/mapbox-maps-android/com.mapbox.maps.extension.style.sources.generated/model-material-override.html) names configured, then displayed via a [`ModelLayer`](https://docs.mapbox.com/android/maps/api/latest/mapbox-maps-android/com.mapbox.maps.extension.style.layers.generated/-model-layer/). Sliders and color pickers update the model in real time:

-   **Node overrides** control the front-left door, front-right door, trunk, and hood openings by setting a value on each named node.
-   **Material overrides** change the vehicle body color and toggle brake lights by updating named materials on the source.

> **Note: Android Examples App Available**
> 
> This example code is part of the **Maps SDK for Android Examples App**, a working Android project [available on GitHub](https://github.com/mapbox/mapbox-maps-android/tree/v11.29.0/). Android developers are encouraged to run the examples app locally to interact with this example in an emulator and explore other features of the Maps SDK.
> 
> See our [Run the Maps SDK for Android Examples App](https://docs.mapbox.com/help/tutorials/maps-sdk-android-examples-app/) tutorial for step-by-step instructions.

**Kotlin**

Title: `Interactive3DModelSourceActivity.kt`

[View on GitHub](https://github.com/mapbox/mapbox-maps-android/blob/v11.29.0/app/src/main/java/com/mapbox/maps/testapp/examples/Interactive3DModelSourceActivity.kt)

```kt
package com.mapbox.maps.testapp.examples

import android.graphics.Color
import android.graphics.drawable.GradientDrawable
import android.os.Bundle
import android.widget.SeekBar
import androidx.appcompat.app.AppCompatActivity
import com.mapbox.bindgen.Value
import com.mapbox.geojson.Point
import com.mapbox.maps.MapboxExperimental
import com.mapbox.maps.Style
import com.mapbox.maps.dsl.cameraOptions
import com.mapbox.maps.extension.style.layers.generated.modelLayer
import com.mapbox.maps.extension.style.layers.properties.generated.ModelType
import com.mapbox.maps.extension.style.light.dynamicLight
import com.mapbox.maps.extension.style.light.generated.ambientLight
import com.mapbox.maps.extension.style.light.generated.directionalLight
import com.mapbox.maps.extension.style.sources.generated.ModelSourceModel
import com.mapbox.maps.extension.style.sources.generated.modelMaterialOverride
import com.mapbox.maps.extension.style.sources.generated.modelNodeOverride
import com.mapbox.maps.extension.style.sources.generated.modelSource
import com.mapbox.maps.extension.style.sources.generated.modelSourceModel
import com.mapbox.maps.extension.style.style
import com.mapbox.maps.testapp.databinding.ActivityInteractive3dModelSourceBinding
import kotlin.math.roundToInt

/**
 * Showcase interactive 3D model with source-based updates.
 * Demonstrates node overrides for doors/hood/trunk and material overrides for colors/lights.
 */
@MapboxExperimental
class Interactive3DModelSourceActivity : AppCompatActivity() {

  private lateinit var binding: ActivityInteractive3dModelSourceBinding

  // Vehicle parameters
  private var doorsFrontLeft = 0.5
  private var doorsFrontRight = 0.0
  private var trunk = 0.0
  private var hood = 0.0
  private var brakeLights = 0.0
  private var vehicleColor = Color.WHITE
  private lateinit var model: ModelSourceModel

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    binding = ActivityInteractive3dModelSourceBinding.inflate(layoutInflater)
    setContentView(binding.root)

    model = createCarModel()

    binding.mapView.mapboxMap.apply {
      setCamera(
        cameraOptions {
          center(CAR_POSITION)
          zoom(19.3)
          bearing(45.0)
          pitch(60.0)
        }
      )

      loadStyle(
        style(Style.STANDARD) {
          +dynamicLight(
            ambientLight("environment") {
              intensity(0.4)
            },
            directionalLight("sun_light") {
              castShadows(true)
            }
          )
          +modelSource(SOURCE_ID) {
            models(listOf(model))
          }
          +modelLayer(LAYER_ID, SOURCE_ID) {
            modelScale(listOf(10.0, 10.0, 10.0))
            modelType(ModelType.LOCATION_INDICATOR)
          }
        }
      ) {
        binding.mapView.mapboxMap.setStyleImportConfigProperty(
          "basemap",
          "show3dObjects",
          Value.valueOf(false)
        )
        setupControls()
      }
    }
  }

  private fun setupControls() {
    // Color picker view
    updateColorPickerBackground()
    binding.colorPickerButton.setOnClickListener {
      showColorPickerDialog()
    }

    // Trunk slider
    binding.seekBarTrunk.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
      override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
        trunk = progress / 100.0
        model.nodeOverrides(
          listOf(
            modelNodeOverride("trunk") {
              orientation(listOf(mix(trunk, 0.0, -60.0), 0.0, 0.0))
            }
          )
        )
      }
      override fun onStartTrackingTouch(seekBar: SeekBar?) {}
      override fun onStopTrackingTouch(seekBar: SeekBar?) {}
    })

    // Hood slider
    binding.seekBarHood.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
      override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
        hood = progress / 100.0
        model.nodeOverrides(
          listOf(
            modelNodeOverride("hood") {
              orientation(listOf(mix(hood, 0.0, 45.0), 0.0, 0.0))
            }
          )
        )
      }
      override fun onStartTrackingTouch(seekBar: SeekBar?) {}
      override fun onStopTrackingTouch(seekBar: SeekBar?) {}
    })

    // Front left door slider
    binding.seekBarDoorLeft.progress = (doorsFrontLeft * 100).roundToInt()
    binding.seekBarDoorLeft.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
      override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
        doorsFrontLeft = progress / 100.0
        model.nodeOverrides(
          listOf(
            modelNodeOverride("doors_front-left") {
              orientation(listOf(0.0, mix(doorsFrontLeft, 0.0, -80.0), 0.0))
            }
          )
        )
      }
      override fun onStartTrackingTouch(seekBar: SeekBar?) {}
      override fun onStopTrackingTouch(seekBar: SeekBar?) {}
    })

    // Front right door slider
    binding.seekBarDoorRight.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
      override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
        doorsFrontRight = progress / 100.0
        model.nodeOverrides(
          listOf(
            modelNodeOverride("doors_front-right") {
              orientation(listOf(0.0, mix(doorsFrontRight, 0.0, 80.0), 0.0))
            }
          )
        )
      }
      override fun onStartTrackingTouch(seekBar: SeekBar?) {}
      override fun onStopTrackingTouch(seekBar: SeekBar?) {}
    })

    // Brake lights slider
    binding.seekBarBrake.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
      override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
        brakeLights = progress / 100.0
        model.materialOverrides(
          listOf(
            modelMaterialOverride("lights_brakes") {
              modelColor(Color.rgb(224, 0, 0))
              modelColorMixIntensity(brakeLights)
              modelEmissiveStrength(brakeLights)
            },
            modelMaterialOverride("lights-brakes_reverse") {
              modelColor(Color.rgb(224, 0, 0))
              modelColorMixIntensity(brakeLights)
              modelEmissiveStrength(brakeLights)
            },
            modelMaterialOverride("lights_brakes_volume") {
              modelColor(Color.rgb(224, 0, 0))
              modelColorMixIntensity(1.0)
              modelEmissiveStrength(0.8)
              modelOpacity(brakeLights)
            },
            modelMaterialOverride("lights-brakes_reverse_volume") {
              modelColor(Color.rgb(224, 0, 0))
              modelColorMixIntensity(1.0)
              modelEmissiveStrength(0.8)
              modelOpacity(brakeLights)
            }
          )
        )
      }
      override fun onStartTrackingTouch(seekBar: SeekBar?) {}
      override fun onStopTrackingTouch(seekBar: SeekBar?) {}
    })
  }

  private fun showColorPickerDialog() {
    val colors = intArrayOf(
      Color.WHITE,
      Color.BLACK,
      Color.RED,
      Color.rgb(0, 100, 200), // Blue
      Color.rgb(0, 150, 0), // Green
      Color.YELLOW,
      Color.rgb(150, 75, 0), // Brown
      Color.GRAY
    )

    androidx.appcompat.app.AlertDialog.Builder(this)
      .setTitle("Vehicle Color")
      .setItems(arrayOf("White", "Black", "Red", "Blue", "Green", "Yellow", "Brown", "Gray")) { _, which ->
        vehicleColor = colors[which]
        updateColorPickerBackground()

        model.materialOverrides(
          listOf(
            modelMaterialOverride("body") {
              modelColor(vehicleColor)
              modelColorMixIntensity(1.0)
            }
          )
        )
      }
      .show()
  }

  private fun updateColorPickerBackground() {
    val drawable = GradientDrawable().apply {
      shape = GradientDrawable.RECTANGLE
      setColor(vehicleColor)
      cornerRadius = 8f * resources.displayMetrics.density
      setStroke((2 * resources.displayMetrics.density).toInt(), Color.GRAY)
    }
    binding.colorPickerButton.background = drawable
  }

  // Create initial model with all overrides
  private fun createCarModel(): ModelSourceModel {
    val doorOpeningDegMax = 80.0

    // Material overrides
    val materialOverrides = listOf(
      modelMaterialOverride("body") {
        modelColor(vehicleColor)
        modelColorMixIntensity(1.0)
      },
      modelMaterialOverride("lights_brakes") {
        modelColor(Color.rgb(224, 0, 0))
        modelColorMixIntensity(brakeLights)
        modelEmissiveStrength(brakeLights)
      },
      modelMaterialOverride("lights-brakes_reverse") {
        modelColor(Color.rgb(224, 0, 0))
        modelColorMixIntensity(brakeLights)
        modelEmissiveStrength(brakeLights)
      },
      modelMaterialOverride("lights_brakes_volume") {
        modelColor(Color.rgb(224, 0, 0))
        modelColorMixIntensity(1.0)
        modelEmissiveStrength(0.8)
        modelOpacity(brakeLights)
      },
      modelMaterialOverride("lights-brakes_reverse_volume") {
        modelColor(Color.rgb(224, 0, 0))
        modelColorMixIntensity(1.0)
        modelEmissiveStrength(0.8)
        modelOpacity(brakeLights)
      }
    )

    // Node overrides for door/hood/trunk animations
    val nodeOverrides = listOf(
      modelNodeOverride("doors_front-left") {
        orientation(listOf(0.0, mix(doorsFrontLeft, 0.0, -doorOpeningDegMax), 0.0))
      },
      modelNodeOverride("doors_front-right") {
        orientation(listOf(0.0, mix(doorsFrontRight, 0.0, doorOpeningDegMax), 0.0))
      },
      modelNodeOverride("hood") {
        orientation(listOf(mix(hood, 0.0, 45.0), 0.0, 0.0))
      },
      modelNodeOverride("trunk") {
        orientation(listOf(mix(trunk, 0.0, -60.0), 0.0, 0.0))
      }
    )

    return modelSourceModel(CAR_MODEL_KEY) {
      uri(CAR_MODEL_URI)
      position(listOf(CAR_POSITION.longitude(), CAR_POSITION.latitude()))
      orientation(listOf(0.0, 0.0, 0.0))
      nodeOverrides(nodeOverrides)
      materialOverrides(materialOverrides)
    }
  }

  // Helper function to mix values (linear interpolation)
  private fun mix(t: Double, a: Double, b: Double): Double {
    return b * t - a * (t - 1)
  }

  private companion object {
    const val SOURCE_ID = "3d-model-source"
    const val LAYER_ID = "3d-model-layer-for-source-based-updates"
    const val CAR_MODEL_KEY = "car"
    const val CAR_MODEL_URI = "https://docs.mapbox.com/mapbox-gl-js/assets/ego_car.glb"
    val CAR_POSITION: Point = Point.fromLngLat(-74.0138, 40.7154)
  }
}
```