Skip to content

Commit 01ff918

Browse files
evil159github-actions[bot]
authored andcommitted
[maps-ios] [maps-android] Expose ModelSource (#9292)
This PR exposes `ModelSource` in Android and iOS. * On iOS existing manually written `Model` entity was replaced by one generated from style speck(no API breakage). * On Android there is already exposed`StyleModelExtension`, so for model source I opted for `ModelSourceModel` name. Additionally 3 new 3D examples were ported from GL JS: * updating 3d model appearance by modifying model source directly * using feature state expressions to update 3d model appearance * animating 3d model along a route Addresses: https://mapbox.atlassian.net/browse/MAPSAND-2482 https://mapbox.atlassian.net/browse/MAPSIOS-2055 https://mapbox.atlassian.net/browse/MAPSIOS-2079 https://mapbox.atlassian.net/browse/MAPSAND-2449 cc @mapbox/gl-native cc @mapbox/maps-android cc @mapbox/maps-ios cc @mapbox/sdk-platform GitOrigin-RevId: 3f201974d0f4475a41ccbddfe2f0080fe07e84cb
1 parent 6920b2b commit 01ff918

35 files changed

Lines changed: 4188 additions & 17 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ Mapbox welcomes participation and contributions from everyone.
2020
* Update gl-native to [v11.18.1](https://github.com/mapbox/mapbox-maps-android/releases/tag/v11.18.1), common to [v24.18.1](https://github.com/mapbox/mapbox-maps-android/releases/tag/v11.18.1).
2121

2222

23+
## Features ✨ and improvements 🏁
24+
* Add `ModelSource` support with `ModelSourceModel`, `ModelMaterialOverride`, and `ModelNodeOverride` to enable interactive 3D models. Material overrides allow customization of color, emissive strength, opacity, and color mix intensity. Node overrides enable control of model part transformations such as rotating doors, landing gear, or propellers. Models can be updated via source-driven approach (modifying `ModelSource.models` directly) or feature-state driven approach (using expressions with feature state for dynamic control). For implementation examples, see `Interactive3DModelFeatureStateActivity` (Compose), `Interactive3DModelSourceActivity` (View), and `Animated3DModelActivity` (Compose).
25+
2326
# 11.19.0-beta.1 January 28, 2026
2427

2528
## Features ✨ and improvements 🏁

app/src/main/AndroidManifest.xml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1470,5 +1470,16 @@
14701470
android:name="android.support.PARENT_ACTIVITY"
14711471
android:value=".ExampleOverviewActivity" />
14721472
</activity>
1473+
<activity android:name=".examples.Interactive3DModelSourceActivity"
1474+
android:description="@string/description_3d_model_source_interactions"
1475+
android:label="@string/description_3d_model_source_interactions_label"
1476+
android:exported="true">
1477+
<meta-data
1478+
android:name="category"
1479+
android:value="@string/category_3D" />
1480+
<meta-data
1481+
android:name="android.support.PARENT_ACTIVITY"
1482+
android:value=".examples.Interactive3DModelSourceActivity" />
1483+
</activity>
14731484
</application>
14741485
</manifest>
Lines changed: 310 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,310 @@
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

Comments
 (0)