Awesome 3D shapes in Mapbox React Native, Part I
This is part I of a multipart series — stay tuned for more 3D shapes! This is cross-posted from our blog over on DigitalMapmaking.com.
Now, everyone knows there’s lots of great ways to integrate Mapbox with 3D shapes, either directly or using a helper library like Threebox. However, when it comes to using Mapbox mobile SDKs, 3D support for custom models is lacking and/or difficult to figure out.
In Mapbox SDK 10.0, there is some existing 3D support. In the early notes during development of v10, Mapbox planned to add a 3D layer that could handle loading true 3D objects, and placing them around the map. However, they scaled that back and cut that feature altogether once v10 hit production — leaving only the ability to have a 3D “puck” for the user location. So it’s possible, for instance, to have a 3D model for your user location, but not for other models on the map, such as 3D buildings, armies, and so on.
(A note: it looks like Mapbox has added a ModelLayer to v10.6.2 — this is great! This isn’t integrated into libraries like React Mapbox GL yet, though, and I haven’t tested it directly yet.)
This is a bit disappointing as Mapbox is a great solution in a lot of ways for mobile games or creative applications, when your developers are strong with React Native or native app development and you don’t want to have to make a shift to a fully different platform like Unity just to get 3D into your app.
However, there is a way to make 3D models in mobile Mapbox! It’s restricted and fairly difficult, and not for everyone, but it is doable and pretty fun. You basically have to use 3D extrusions in creative ways to get this done. We’ll go over a few examples below to give you an idea of how this can work.
This is done using Mapbox GL JS, for the sake of ease, but can easily be ported over to use in a mobile native SDK, or also using a library like React Native Mapbox.
We’ll be using one of our favorite libraries, TurfJS — and some basic math for this.
The Project
We’re working with some great clients on a mobile application that makes use of 3D in some small ways. These clients wanted to have some 3D graphics, but we were just too limited by Mapbox. So we decided to explore the possibility of building the 3D graphics using various combinations of fill extrusions. In many cases, this means the 3D will be fairly bulky and blocky, but with the right graphical theme, it might work.
We had three major items to create: a traffic cone, a “Parking” sign, and a model car. I’ve put these here in order of difficulty, so we can go through them in these posts accordingly and build up your understanding.
The Traffic Cone
To get straight to the full code, click here for the HTML example.
Our first task was creating a traffic cone. This is a relatively easy place to start, since a traffic cone is essentially a series of circles, getting progressively smaller, with a square base. It’s not a perfectly diagonal line (x + 1, y + 1), but rather something a bit more strongly sloped upwards (x + 0.3, y + 1).
In this case, our input variables consisted of two points. These points were two ends of a rectangular area where the traffic cone would go. This gives us a general area within which we can place the traffic cone, and a sense of the maximum size of the traffic cone relative to the space it’s placed in.
Using these variables, we first use TurfJS to find the midpoint (turf.midpoint()
). This will be our origin point for our circles.
Then we use the length to estimate a reasonable width for the base circle. I figure that probably using one-third the length of the initial rectangle is reasonable — but this could be adjusted. So we use turf.length(p1, p2) * 0.3
.
From here, we generate our first base circle:
const midpoint = turf.midpoint(p1, p2);
const length = turf.length(p1, p2) * 0.3;
const firstCircle = turf.circle(midpoint, length/2);
We add a bit of extrusion to this, so it has a bit of height. We add this into the properties instead of directly, so that as we add more shapes to our geoJSON, they’ll be dynamically set to the right base and height.
firstCircle.properties.base = 0;
firstCircle.properties.height = 1;
firstCircle.properties.color = "#FF7221";
map.addSource('circle', {
type : "geojson",
data : firstCircle
});
map.addLayer({
id : 'circle-3d',
source : 'circle',
type : 'fill-extrusion',
paint : {
'fill-extrusion-base': {
type: 'identity',
property: 'base'
},
'fill-extrusion-height': {
type: 'identity',
property: 'height'
},
'fill-extrusion-color': {
type: 'identity',
property: 'color'
},
}
});
OK. We check how this looks:
OK, not bad! Now let’s add the base. This is pretty easy. We envelop the shape using turf.envelop()
to make a rectangle, make it a little larger than the original circle using turf.buffer()
, and color it black.
const baseGeometry = turf.envelope(turf.buffer(firstCircle, 0.01).geometry;
// Adjust the buffer size as needed based on the scale
We add this to our geoJSON, setting the color to black, base to 0, and height to 1. Everything looks like this now:
It kind of swallowed up our original circle, but that’s OK. We know how to adjust the sizes and the heights, so we can fix that in the next part.
This is the tricky one. We need to loop over the “levels” of our cone, and adjust the radius appropriately so that the cone will grow upwards at the right scale.
I decided on trying out 20 levels of the cone, just for the hell of it. And every time the level increases one, I am going to make the new circle around 90% of the previous circle’s radius (I just sort of guesstimate it). So I do a loop like this:
let levelsOfCone = 20;
for(let i=0; i<levelsOfCone; i++) {
let currentCircleGeometry = turf.circle(startingPoint, (length - (length * ((i + 1) * 0.8/levelsOfCone)));
}
The code looks a bit complicated, but let’s break it down. I’m taking the original circle size (startingPoint and length), and subtracting a certain amount from it on every loop. This amount increases (i + 1) with each loop, so that I’m removing more and more length every time.
For example, if we take i = 1, we get a length of length - (0.08 * length), or around 92% of the original length. This continues as we keep going. i = 2 is 88%, and so on.
It ends up looking like this:
We’re very close now. Now we just have to add the white color in the right place. We do this by adding a little check to change the color in the geoJSON property.
let whiteLevelsBottom = levelsOfCone/3;
let whiteLevelsTop = levelsOfCone/1.5;
properties.color = i > whiteLevelsBottom && i < whiteLevelsTop ? '#FFFFFF' : "#FF7221"
OK, let’s check how this looks!
To see the full code, click here for the HTML example.
In Parts II and III, we’ll explore making a traffic sign, and making a full-sized car model as well!