mapbox-gl | 3.3 添加模型
简述
随着版本的更新,mapbox逐渐支持更多的三维效果,加载模型是很多场景中常见的功能,本篇文章带来三种官方加载模型的方式。
1. mapbox加载
该方法为mapbox3.x进行支持,并处于实验性阶段
map.on('style.load', () => {
// set the light preset to be in dusk mode.
map.setConfigProperty('basemap', 'lightPreset', 'dusk');
// add a geojson source with a polygon to be used in the clip layer.
map.addSource('eraser', {
'type': 'geojson',
'data': {
'type': 'FeatureCollection',
'features': [
{
'type': 'Feature',
'properties': {},
'geometry': {
'coordinates': [
[
[-0.12573, 51.53222],
[-0.12458, 51.53219],
[-0.12358, 51.53492],
[-0.12701, 51.53391],
[-0.12573, 51.53222]
]
],
'type': 'Polygon'
}
}
]
}
});
// add a geojson source which specifies the custom model to be used by the model layer.
map.addSource('model', {
'type': 'geojson',
'data': {
'type': 'Feature',
'properties': {
'model-uri':
'https://docs.mapbox.com/mapbox-gl-js/assets/tower.glb'
},
'geometry': {
'coordinates': [-0.12501974, 51.5332374],
'type': 'Point'
}
}
});
// add the clip layer and configure it to also remove symbols and trees.
// `clip-layer-scope` layout property is used to specify that only models from the Mapbox Standard Style should be clipped.
// this will prevent the newly added model from getting clipped.
map.addLayer({
'id': 'eraser',
'type': 'clip',
'source': 'eraser',
'layout': {
// specify the layer types to be removed by this clip layer
'clip-layer-types': ['symbol', 'model'],
'clip-layer-scope': ['basemap']
}
});
// add the model layer and specify the appropriate `slot` to ensure the symbols are rendered correctly.
map.addLayer({
'id': 'tower',
'type': 'model',
'slot': 'middle',
'source': 'model',
'minzoom': 15,
'layout': {
'model-id': ['get', 'model-uri']
},
'paint': {
'model-opacity': 1,
'model-rotation': [0.0, 0.0, 35.0],
'model-scale': [0.8, 0.8, 1.2],
'model-color-mix-intensity': 0,
'model-cast-shadows': true,
'model-emissive-strength': 0.8
}
});
});
代码结构很简单,添加了两个数据源和两个图层,分别对应着裁剪面和模型。
先说一下裁剪面,同样是3.x新的特性,自3.x版本开始,地图样式开始增加更多的3d样式,如:树、标志性建筑等,当我们想将一部分默认模型替换成自己更加精细的模型时,便可以使用裁剪面将默认模型裁剪掉,所以上述代码中,添加一个面的数据源,然后以此创建裁剪面图层,裁剪默认模型。
添加模型,首先添加了一个点数据源,其中属性包含模型地址,然后添加图层,type为model,设置mode-id为模型地址,然后设置其基本属性,如缩放,旋转,阴影等。
从上面的逻辑来看,mapbox对模型的态度,更希望它是一种丰富地图的元素,和点线面地位相同,对于模型动画这类目前没看到要支持的意思,所以对于大部分城市模型来说,这样足够业务使用了,但并不能覆盖所有场景。
2. three加载
// parameters to ensure the model is georeferenced correctly on the map
const modelOrigin = [148.9819, -35.39847];
const modelAltitude = 0;
const modelRotate = [Math.PI / 2, 0, 0];
const modelAsMercatorCoordinate = mapboxgl.MercatorCoordinate.fromLngLat(
modelOrigin,
modelAltitude
);
// transformation parameters to position, rotate and scale the 3D model onto the map
const modelTransform = {
translateX: modelAsMercatorCoordinate.x,
translateY: modelAsMercatorCoordinate.y,
translateZ: modelAsMercatorCoordinate.z,
rotateX: modelRotate[0],
rotateY: modelRotate[1],
rotateZ: modelRotate[2],
/* Since the 3D model is in real world meters, a scale transform needs to be
* applied since the CustomLayerInterface expects units in MercatorCoordinates.
*/
scale: modelAsMercatorCoordinate.meterInMercatorCoordinateUnits()
};
const THREE = window.THREE;
// configuration of the custom layer for a 3D model per the CustomLayerInterface
const customLayer = {
id: '3d-model',
type: 'custom',
renderingMode: '3d',
onAdd: function (map, gl) {
this.camera = new THREE.Camera();
this.scene = new THREE.Scene();
// create two three.js lights to illuminate the model
const directionalLight = new THREE.DirectionalLight(0xffffff);
directionalLight.position.set(0, -70, 100).normalize();
this.scene.add(directionalLight);
const directionalLight2 = new THREE.DirectionalLight(0xffffff);
directionalLight2.position.set(0, 70, 100).normalize();
this.scene.add(directionalLight2);
// use the three.js GLTF loader to add the 3D model to the three.js scene
const loader = new THREE.GLTFLoader();
loader.load(
'https://docs.mapbox.com/mapbox-gl-js/assets/34M_17/34M_17.gltf',
(gltf) => {
this.scene.add(gltf.scene);
}
);
this.map = map;
// use the Mapbox GL JS map canvas for three.js
this.renderer = new THREE.WebGLRenderer({
canvas: map.getCanvas(),
context: gl,
antialias: true
});
this.renderer.autoClear = false;
},
render: function (gl, matrix) {
const rotationX = new THREE.Matrix4().makeRotationAxis(
new THREE.Vector3(1, 0, 0),
modelTransform.rotateX
);
const rotationY = new THREE.Matrix4().makeRotationAxis(
new THREE.Vector3(0, 1, 0),
modelTransform.rotateY
);
const rotationZ = new THREE.Matrix4().makeRotationAxis(
new THREE.Vector3(0, 0, 1),
modelTransform.rotateZ
);
const m = new THREE.Matrix4().fromArray(matrix);
const l = new THREE.Matrix4()
.makeTranslation(
modelTransform.translateX,
modelTransform.translateY,
modelTransform.translateZ
)
.scale(
new THREE.Vector3(
modelTransform.scale,
-modelTransform.scale,
modelTransform.scale
)
)
.multiply(rotationX)
.multiply(rotationY)
.multiply(rotationZ);
this.camera.projectionMatrix = m.multiply(l);
this.renderer.resetState();
this.renderer.render(this.scene, this.camera);
this.map.triggerRepaint();
}
};
map.on('style.load', () => {
map.addLayer(customLayer, 'waterway-label');
});
CustomLayer是mapbox提供的使用webgl的上层入口,大部分情况下,以这种形式结合three去使用,如果webgl熟练度高的话,也可以使用原生去写。
CustomLayerr有两种写法,一种是上述这种Object配置文件的形式,另一种是Class类的形式,殊途同归,没有太大区别,关键在于onAdd和render两个方法,即初始化和每次渲染要执行的内容。
首先看一下声明的一些变量 modelOrigin modelAltitude modelRotate 定义了模型的坐标、海拔和旋转,然后通过模型mapboxgl.MercatorCoordinate.fromLngLat 将坐标转换为墨卡托坐标,需要注意的是,这个墨卡托坐标范围是[0,0]~[1,1].
modelTransform设置了平移、旋转和缩放,缩放,meterInMercatorCoordinateUnits是获取当前坐标下,像素与米的比例.
onAdd中是初始化Three场景,添加一个模型到场景,模型没有重新设置位置,默认在场景中心,render中设置了矩阵,如果不太熟悉这一块的内容也没关系,你只需要知道这样设置Three.Camera的投影矩阵后,就可以让两个场景叠加展示了。
需要注意的是,这样的矩阵转换是不完善的,虽然模型展示正常,但如果按Three的思路去设置点击(raycast)或精灵图等与相机相关的功能时,你会发现它并不会正常工作,所以这样的方式,要比第一种应用面更广一些,但仍不会覆盖全场景应用。
3.threebox加载
const tb = (window.tb = new Threebox(
map,
map.getCanvas().getContext('webgl'),
{
defaultLights: true
}
));
map.on('style.load', () => {
map.addLayer({
id: 'custom-threebox-model',
type: 'custom',
renderingMode: '3d',
onAdd: function () {
// Creative Commons License attribution: Metlife Building model by https://sketchfab.com/NanoRay
// https://sketchfab.com/3d-models/metlife-building-32d3a4a1810a4d64abb9547bb661f7f3
const scale = 3.2;
const options = {
obj: 'https://docs.mapbox.com/mapbox-gl-js/assets/metlife-building.gltf',
type: 'gltf',
scale: { x: scale, y: scale, z: 2.7 },
units: 'meters',
rotation: { x: 90, y: -90, z: 0 }
};
tb.loadObj(options, (model) => {
model.setCoords([-73.976799, 40.754145]);
model.setRotation({ x: 0, y: 0, z: 241 });
tb.add(model);
});
},
render: function () {
tb.update();
}
});
});
threebox是一个第三方工具库,它会帮助你完成矩阵转换和坐标转换,是你能在mapbox中使用更多的three功能。关于其版本,原作者不再维护,jscastro76fork仓库继续维护(近期更新也很少了),在球形地图之前,他的矩阵转换是正常的,球形地图版本后,在地图开始展现球形时,矩阵转换失效,非球形时,仍然有效。
代码比较简洁,首先初始化threebox,通过loadObj方法加载模型,用setCoords转换坐标,随后添加进场景。
threebox相对与第二种方法,他的矩阵转换是完善的,可以让相机相关功能正常工作。
总结
以上是三种官方案例中的加载模型,随着版本的更新,mapbox自身也会更加支持这些功能,不过学习一下threebox的实现原理,对我们理解mapbox的代码是有帮助的。