You would have to use the WebGlLayer and then use a 3D library like Babylon or Three.js. Here are a few existing samples:
- https://samples.azuremaps.com/?sample=babylon-custom-webgl-layer
- https://samples.azuremaps.com/?sample=three.js-custom-webgl-layer
I played around this morning with the Babylon sample and created a reusable layer for managing a simple 3D model easily. You can try it out here. Here is the code for the custom layer class I created.
/**
* A custom WebGLLayer that renders a 3D model on the map.
* @param {string} modelPath The path to the model file.
* @param {string} modelFileName The name of the model file.
* @param {Array.<number>} modelOrigin The position of the model in [longitude, latitude] format.
* @param {number} modelAltitude The altitude of the model. Default: 0
* @param {number} headingOffset The heading offset of the model. Adjust model heading so that it points north when heading is 0.
* @param {number} modelScale The scale of the model.
* @param {Array.<number>} modelOrientation The orientation of the model in [pitch, roll, yaw] format. Specifies how the model should be rotated so it sits upright.
*/
var Simple3DModelLayer = function (modelPath, modelFileName, modelOrigin, modelAltitude, headingOffset, modelScale, modelOrientation)
{
//modelOrientation = modelOrientation || [Math.PI / 2, 0, 0];
modelScale = modelScale || 1;
modelAltitude = modelAltitude || 0;
modelOrientation = [Math.PI / 2, 0, 0];
headingOffset = headingOffset || 0;
heading = 0;
let modelCoords = atlas.data.MercatorPoint.fromPosition([...modelOrigin, modelAltitude]);
let mapModelScale;
let modelMatrix;
let _scene;
let _map;
let _engine;
let _gl;
let _modelMesh;
BABYLON.SceneLoaderFlags.ShowLoadingScreen = false;
var updateModelMatrix = () => {
modelCoords = atlas.data.MercatorPoint.fromPosition([...modelOrigin, modelAltitude]);
mapModelScale = 2 * atlas.data.MercatorPoint.meterInMercatorUnits(modelOrigin[1]) * modelScale;
var headingRadians = (heading + headingOffset) * Math.PI / 180;
var orientation = BABYLON.Quaternion.FromEulerAngles(modelOrientation[0], modelOrientation[1], modelOrientation[2]);
//Rotate the orientation by the heading.
orientation = orientation.multiply(BABYLON.Quaternion.RotationAxis(new BABYLON.Vector3(0, 1, 0), headingRadians));
modelMatrix = BABYLON.Matrix.Compose(
new BABYLON.Vector3(mapModelScale, mapModelScale, mapModelScale),
orientation,
new BABYLON.Vector3(modelCoords[0], modelCoords[1], modelCoords[2])
);
//Make the map repaint.
_map.triggerRepaint();
}
// Create a renderer that implements atlas.WebGLRenderer
var renderer = {
renderingMode: "3d",
// Method called when the layer is added to the map
onAdd: (map, gl) => {
_map = map;
_gl = gl;
// Initialize the Babylon.js engine.
if(!_engine){
_engine = new BABYLON.Engine(gl, true, { useHighPrecisionMatrix: true }, true);
}
if(!_scene || _scene.isDisposed){
_scene = new BABYLON.Scene(_engine);
_scene.autoClear = false;
_scene.detachControl();
_scene.beforeRender = () => {
_engine.wipeCaches(true);
};
}
this.camera = new BABYLON.Camera("camera", new BABYLON.Vector3(), _scene);
const light = new BABYLON.HemisphericLight("light", BABYLON.Vector3.One(), _scene);
BABYLON.SceneLoader.Append(modelPath, modelFileName, _scene);
updateModelMatrix();
},
onRemove: () => {
_scene.dispose();
_scene = null;
_engine.dispose();
_engine = null;
_map = null;
_gl = null;
},
// Method called on each animation frame
render: (gl, matrix) => {
// projection & view matrix
const cameraMatrix = BABYLON.Matrix.FromArray(matrix);
const mvpMatrix = modelMatrix.multiply(cameraMatrix);
this.camera.freezeProjectionMatrix(mvpMatrix);
_scene.render(false);
_map.triggerRepaint();
}
};
//Create a unique id for the layer.
const uniqueId = "Simple3dModelLayer_" + Math.random().toString(36).substr(2, 9);
const layer = new atlas.layer.WebGLLayer(uniqueId, { renderer });
//Extend the layer with some custom methods.
layer.setPosition = function(newModelOrigin) {
modelOrigin = newModelOrigin;
//Update model matrix.
updateModelMatrix();
}
layer.getPosition = function(){
return modelOrigin;
}
layer.setAltitude = function (newAltitude) {
modelAltitude = newAltitude;
//Update model matrix.
updateModelMatrix();
}
layer.getAltitude = function(){
return modelAltitude;
}
layer.setHeading = function(newHeading) {
heading = newHeading;
//Update model matrix.
updateModelMatrix();
}
layer.getHeading = function(){
return heading;
}
layer.setModel = function(newModelPath, newModelFileName) {
modelPath = newModelPath;
modelFileName = newModelFileName;
if(_map != null){
//Dispose the scene and reload the model.
_scene.dispose();
renderer.onAdd(_map, _gl);
}
}
layer.setScale = function(newModelScale) {
modelScale = newModelScale;
//Update model matrix.
updateModelMatrix();
}
layer.getScale = function(){
return modelScale;
}
return layer;
}
Models are loaded using Babylon.js using SceneLoader.Append which can take in a path value which would be the path to the folder that contains the model.
The modelOrientation
is used to rotate the model so that it sits up properly. With a lot of models I've found you need to pitch the model by PI/2 (I made this the default, so no need to set this unless your model needs a different orientation).
Headings in the map are such that 0 = north, 90 = east, 180 = south, and 270 = west. With this in mind you will want your model pointing north by default when heading is set to 0. You could use the model orientation, but that would likely take a lot of extra work to get right. So to simplify things I added a headingOffset
where you can specify how many degrees to rotate the model so that it's default heading is pointing north.
Here is a simple example of how to use this new layer:
// Create an instance of the simplae 3D Model Layer
layer = new Simple3DModelLayer('', 'BMW_3-Series.glb', sampleCenter, 0, -180, 1);
// Add the layer to the map
map.layers.add(layer);
Note that you will need to copy and past the Simple3DModelLayer
code I provided earlier, and also load Babylon.js into your app:
<script src="https://cdn.babylonjs.com/babylon.js"></script>
<script src="https://cdn.babylonjs.com/loaders/babylonjs.loaders.min.js"></script>
You can try this sample out here: https://rbrundritt.azurewebsites.net/Demos/AzureMaps/Simple3DModelLayer