1+ package com.mapbox.maps.testapp.examples
2+
3+ import android.graphics.Color
4+ import android.graphics.drawable.GradientDrawable
5+ import android.os.Bundle
6+ import android.widget.SeekBar
7+ import androidx.appcompat.app.AppCompatActivity
8+ import com.mapbox.bindgen.Value
9+ import com.mapbox.geojson.Point
10+ import com.mapbox.maps.MapboxExperimental
11+ import com.mapbox.maps.Style
12+ import com.mapbox.maps.dsl.cameraOptions
13+ import com.mapbox.maps.extension.style.layers.generated.modelLayer
14+ import com.mapbox.maps.extension.style.layers.properties.generated.ModelType
15+ import com.mapbox.maps.extension.style.light.dynamicLight
16+ import com.mapbox.maps.extension.style.light.generated.ambientLight
17+ import com.mapbox.maps.extension.style.light.generated.directionalLight
18+ import com.mapbox.maps.extension.style.sources.generated.ModelSourceModel
19+ import com.mapbox.maps.extension.style.sources.generated.modelMaterialOverride
20+ import com.mapbox.maps.extension.style.sources.generated.modelNodeOverride
21+ import com.mapbox.maps.extension.style.sources.generated.modelSource
22+ import com.mapbox.maps.extension.style.sources.generated.modelSourceModel
23+ import com.mapbox.maps.extension.style.style
24+ import com.mapbox.maps.testapp.databinding.ActivityInteractive3dModelSourceBinding
25+ import kotlin.math.roundToInt
26+
27+ /* *
28+ * Showcase interactive 3D model with source-based updates.
29+ * Demonstrates node overrides for doors/hood/trunk and material overrides for colors/lights.
30+ */
31+ @MapboxExperimental
32+ class Interactive3DModelSourceActivity : AppCompatActivity () {
33+
34+ private lateinit var binding: ActivityInteractive3dModelSourceBinding
35+
36+ // Vehicle parameters
37+ private var doorsFrontLeft = 0.5
38+ private var doorsFrontRight = 0.0
39+ private var trunk = 0.0
40+ private var hood = 0.0
41+ private var brakeLights = 0.0
42+ private var vehicleColor = Color .WHITE
43+ private lateinit var model: ModelSourceModel
44+
45+ override fun onCreate (savedInstanceState : Bundle ? ) {
46+ super .onCreate(savedInstanceState)
47+ binding = ActivityInteractive3dModelSourceBinding .inflate(layoutInflater)
48+ setContentView(binding.root)
49+
50+ model = createCarModel()
51+
52+ binding.mapView.mapboxMap.apply {
53+ setCamera(
54+ cameraOptions {
55+ center(CAR_POSITION )
56+ zoom(19.3 )
57+ bearing(45.0 )
58+ pitch(60.0 )
59+ }
60+ )
61+
62+ loadStyle(
63+ style(Style .STANDARD ) {
64+ + dynamicLight(
65+ ambientLight(" environment" ) {
66+ intensity(0.4 )
67+ },
68+ directionalLight(" sun_light" ) {
69+ castShadows(true )
70+ }
71+ )
72+ + modelSource(SOURCE_ID ) {
73+ models(listOf (model))
74+ }
75+ + modelLayer(LAYER_ID , SOURCE_ID ) {
76+ modelScale(listOf (10.0 , 10.0 , 10.0 ))
77+ modelType(ModelType .LOCATION_INDICATOR )
78+ }
79+ }
80+ ) {
81+ binding.mapView.mapboxMap.setStyleImportConfigProperty(
82+ " basemap" ,
83+ " show3dObjects" ,
84+ Value .valueOf(false )
85+ )
86+ setupControls()
87+ }
88+ }
89+ }
90+
91+ private fun setupControls () {
92+ // Color picker view
93+ updateColorPickerBackground()
94+ binding.colorPickerButton.setOnClickListener {
95+ showColorPickerDialog()
96+ }
97+
98+ // Trunk slider
99+ binding.seekBarTrunk.setOnSeekBarChangeListener(object : SeekBar .OnSeekBarChangeListener {
100+ override fun onProgressChanged (seekBar : SeekBar ? , progress : Int , fromUser : Boolean ) {
101+ trunk = progress / 100.0
102+ model.nodeOverrides(
103+ listOf (
104+ modelNodeOverride(" trunk" ) {
105+ orientation(listOf (mix(trunk, 0.0 , - 60.0 ), 0.0 , 0.0 ))
106+ }
107+ )
108+ )
109+ }
110+ override fun onStartTrackingTouch (seekBar : SeekBar ? ) {}
111+ override fun onStopTrackingTouch (seekBar : SeekBar ? ) {}
112+ })
113+
114+ // Hood slider
115+ binding.seekBarHood.setOnSeekBarChangeListener(object : SeekBar .OnSeekBarChangeListener {
116+ override fun onProgressChanged (seekBar : SeekBar ? , progress : Int , fromUser : Boolean ) {
117+ hood = progress / 100.0
118+ model.nodeOverrides(
119+ listOf (
120+ modelNodeOverride(" hood" ) {
121+ orientation(listOf (mix(hood, 0.0 , 45.0 ), 0.0 , 0.0 ))
122+ }
123+ )
124+ )
125+ }
126+ override fun onStartTrackingTouch (seekBar : SeekBar ? ) {}
127+ override fun onStopTrackingTouch (seekBar : SeekBar ? ) {}
128+ })
129+
130+ // Front left door slider
131+ binding.seekBarDoorLeft.progress = (doorsFrontLeft * 100 ).roundToInt()
132+ binding.seekBarDoorLeft.setOnSeekBarChangeListener(object : SeekBar .OnSeekBarChangeListener {
133+ override fun onProgressChanged (seekBar : SeekBar ? , progress : Int , fromUser : Boolean ) {
134+ doorsFrontLeft = progress / 100.0
135+ model.nodeOverrides(
136+ listOf (
137+ modelNodeOverride(" doors_front-left" ) {
138+ orientation(listOf (0.0 , mix(doorsFrontLeft, 0.0 , - 80.0 ), 0.0 ))
139+ }
140+ )
141+ )
142+ }
143+ override fun onStartTrackingTouch (seekBar : SeekBar ? ) {}
144+ override fun onStopTrackingTouch (seekBar : SeekBar ? ) {}
145+ })
146+
147+ // Front right door slider
148+ binding.seekBarDoorRight.setOnSeekBarChangeListener(object : SeekBar .OnSeekBarChangeListener {
149+ override fun onProgressChanged (seekBar : SeekBar ? , progress : Int , fromUser : Boolean ) {
150+ doorsFrontRight = progress / 100.0
151+ model.nodeOverrides(
152+ listOf (
153+ modelNodeOverride(" doors_front-right" ) {
154+ orientation(listOf (0.0 , mix(doorsFrontRight, 0.0 , 80.0 ), 0.0 ))
155+ }
156+ )
157+ )
158+ }
159+ override fun onStartTrackingTouch (seekBar : SeekBar ? ) {}
160+ override fun onStopTrackingTouch (seekBar : SeekBar ? ) {}
161+ })
162+
163+ // Brake lights slider
164+ binding.seekBarBrake.setOnSeekBarChangeListener(object : SeekBar .OnSeekBarChangeListener {
165+ override fun onProgressChanged (seekBar : SeekBar ? , progress : Int , fromUser : Boolean ) {
166+ brakeLights = progress / 100.0
167+ model.materialOverrides(
168+ listOf (
169+ modelMaterialOverride(" lights_brakes" ) {
170+ modelColor(Color .rgb(224 , 0 , 0 ))
171+ modelColorMixIntensity(brakeLights)
172+ modelEmissiveStrength(brakeLights)
173+ },
174+ modelMaterialOverride(" lights-brakes_reverse" ) {
175+ modelColor(Color .rgb(224 , 0 , 0 ))
176+ modelColorMixIntensity(brakeLights)
177+ modelEmissiveStrength(brakeLights)
178+ },
179+ modelMaterialOverride(" lights_brakes_volume" ) {
180+ modelColor(Color .rgb(224 , 0 , 0 ))
181+ modelColorMixIntensity(1.0 )
182+ modelEmissiveStrength(0.8 )
183+ modelOpacity(brakeLights)
184+ },
185+ modelMaterialOverride(" lights-brakes_reverse_volume" ) {
186+ modelColor(Color .rgb(224 , 0 , 0 ))
187+ modelColorMixIntensity(1.0 )
188+ modelEmissiveStrength(0.8 )
189+ modelOpacity(brakeLights)
190+ }
191+ )
192+ )
193+ }
194+ override fun onStartTrackingTouch (seekBar : SeekBar ? ) {}
195+ override fun onStopTrackingTouch (seekBar : SeekBar ? ) {}
196+ })
197+ }
198+
199+ private fun showColorPickerDialog () {
200+ val colors = intArrayOf(
201+ Color .WHITE ,
202+ Color .BLACK ,
203+ Color .RED ,
204+ Color .rgb(0 , 100 , 200 ), // Blue
205+ Color .rgb(0 , 150 , 0 ), // Green
206+ Color .YELLOW ,
207+ Color .rgb(150 , 75 , 0 ), // Brown
208+ Color .GRAY
209+ )
210+
211+ androidx.appcompat.app.AlertDialog .Builder (this )
212+ .setTitle(" Vehicle Color" )
213+ .setItems(arrayOf(" White" , " Black" , " Red" , " Blue" , " Green" , " Yellow" , " Brown" , " Gray" )) { _, which ->
214+ vehicleColor = colors[which]
215+ updateColorPickerBackground()
216+
217+ model.materialOverrides(
218+ listOf (
219+ modelMaterialOverride(" body" ) {
220+ modelColor(vehicleColor)
221+ modelColorMixIntensity(1.0 )
222+ }
223+ )
224+ )
225+ }
226+ .show()
227+ }
228+
229+ private fun updateColorPickerBackground () {
230+ val drawable = GradientDrawable ().apply {
231+ shape = GradientDrawable .RECTANGLE
232+ setColor(vehicleColor)
233+ cornerRadius = 8f * resources.displayMetrics.density
234+ setStroke((2 * resources.displayMetrics.density).toInt(), Color .GRAY )
235+ }
236+ binding.colorPickerButton.background = drawable
237+ }
238+
239+ // Create initial model with all overrides
240+ private fun createCarModel (): ModelSourceModel {
241+ val doorOpeningDegMax = 80.0
242+
243+ // Material overrides
244+ val materialOverrides = listOf (
245+ modelMaterialOverride(" body" ) {
246+ modelColor(vehicleColor)
247+ modelColorMixIntensity(1.0 )
248+ },
249+ modelMaterialOverride(" lights_brakes" ) {
250+ modelColor(Color .rgb(224 , 0 , 0 ))
251+ modelColorMixIntensity(brakeLights)
252+ modelEmissiveStrength(brakeLights)
253+ },
254+ modelMaterialOverride(" lights-brakes_reverse" ) {
255+ modelColor(Color .rgb(224 , 0 , 0 ))
256+ modelColorMixIntensity(brakeLights)
257+ modelEmissiveStrength(brakeLights)
258+ },
259+ modelMaterialOverride(" lights_brakes_volume" ) {
260+ modelColor(Color .rgb(224 , 0 , 0 ))
261+ modelColorMixIntensity(1.0 )
262+ modelEmissiveStrength(0.8 )
263+ modelOpacity(brakeLights)
264+ },
265+ modelMaterialOverride(" lights-brakes_reverse_volume" ) {
266+ modelColor(Color .rgb(224 , 0 , 0 ))
267+ modelColorMixIntensity(1.0 )
268+ modelEmissiveStrength(0.8 )
269+ modelOpacity(brakeLights)
270+ }
271+ )
272+
273+ // Node overrides for door/hood/trunk animations
274+ val nodeOverrides = listOf (
275+ modelNodeOverride(" doors_front-left" ) {
276+ orientation(listOf (0.0 , mix(doorsFrontLeft, 0.0 , - doorOpeningDegMax), 0.0 ))
277+ },
278+ modelNodeOverride(" doors_front-right" ) {
279+ orientation(listOf (0.0 , mix(doorsFrontRight, 0.0 , doorOpeningDegMax), 0.0 ))
280+ },
281+ modelNodeOverride(" hood" ) {
282+ orientation(listOf (mix(hood, 0.0 , 45.0 ), 0.0 , 0.0 ))
283+ },
284+ modelNodeOverride(" trunk" ) {
285+ orientation(listOf (mix(trunk, 0.0 , - 60.0 ), 0.0 , 0.0 ))
286+ }
287+ )
288+
289+ return modelSourceModel(CAR_MODEL_KEY ) {
290+ uri(CAR_MODEL_URI )
291+ position(listOf (CAR_POSITION .longitude(), CAR_POSITION .latitude()))
292+ orientation(listOf (0.0 , 0.0 , 0.0 ))
293+ nodeOverrides(nodeOverrides)
294+ materialOverrides(materialOverrides)
295+ }
296+ }
297+
298+ // Helper function to mix values (linear interpolation)
299+ private fun mix (t : Double , a : Double , b : Double ): Double {
300+ return b * t - a * (t - 1 )
301+ }
302+
303+ private companion object {
304+ const val SOURCE_ID = " 3d-model-source"
305+ const val LAYER_ID = " 3d-model-layer-for-source-based-updates"
306+ const val CAR_MODEL_KEY = " car"
307+ const val CAR_MODEL_URI = " https://docs.mapbox.com/mapbox-gl-js/assets/ego_car.glb"
308+ val CAR_POSITION : Point = Point .fromLngLat(- 74.0138 , 40.7154 )
309+ }
310+ }
0 commit comments