diff --git a/example/mapboxExample.html b/example/mapboxExample.html
new file mode 100644
index 000000000..1de2d29bc
--- /dev/null
+++ b/example/mapboxExample.html
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+ 3D Tiles Mapbox GL JS Example
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/example/mapboxExample.js b/example/mapboxExample.js
new file mode 100644
index 000000000..f5d231fde
--- /dev/null
+++ b/example/mapboxExample.js
@@ -0,0 +1,175 @@
+import * as THREE from 'three';
+import CameraSync, {
+ projectToWorld,
+ projectedUnitsPerMeter,
+} from './mapboxExampleCamera.js';
+import { DebugTilesRenderer as TilesRenderer } from '../src/index.js';
+import { GUI } from 'three/examples/jsm/libs/lil-gui.module.min.js';
+
+// To use any of Mapbox's tools, APIs, or SDKs, you'll need a Mapbox access token
+// https://docs.mapbox.com/help/getting-started/access-tokens/
+const params = {
+
+ 'accessToken': 'put-your-api-key-here',
+ 'reload': reload,
+
+};
+
+// GUI
+const gui = new GUI();
+gui.width = 300;
+
+const mapboxglOptions = gui.addFolder( 'mapboxgl' );
+mapboxglOptions.add( params, 'accessToken' );
+mapboxglOptions.add( params, 'reload' );
+mapboxglOptions.open();
+
+function rotationBetweenDirections( dir1, dir2 ) {
+
+ const rotation = new THREE.Quaternion();
+ const a = new THREE.Vector3().crossVectors( dir1, dir2 );
+ rotation.x = a.x;
+ rotation.y = a.y;
+ rotation.z = a.z;
+ rotation.w = 1 + dir1.clone().dot( dir2 );
+ rotation.normalize();
+
+ return rotation;
+
+}
+
+let map;
+const origin = [ 116, 35 ];
+
+init();
+
+function init() {
+
+ // TO MAKE THE MAP APPEAR YOU MUST
+ // ADD YOUR ACCESS TOKEN FROM
+ // https://account.mapbox.com
+ window.mapboxgl.accessToken = params.accessToken;
+ map = new window.mapboxgl.Map( {
+ container: 'map',
+ style: 'mapbox://styles/mapbox/dark-v9',
+ zoom: 17.86614600777933,
+ pitch: 70,
+ bearing: - 40,
+ center: origin,
+ } );
+
+ map.on( 'load', () => {
+
+ let renderer, scene, camera, world, tiles, tilesGroup;
+
+ map.addLayer( {
+ id: 'custom_layer',
+ type: 'custom',
+ onAdd: function ( map, mbxContext ) {
+
+ renderer = new THREE.WebGLRenderer( {
+ alpha: true,
+ antialias: true,
+ canvas: map.getCanvas(),
+ context: mbxContext,
+ } );
+
+ renderer.shadowMap.enabled = true;
+ renderer.autoClear = false;
+
+ scene = new THREE.Scene();
+ camera = new THREE.Camera();
+
+ world = new THREE.Group();
+ scene.add( world );
+
+ tilesGroup = new THREE.Group();
+
+ world.add( tilesGroup );
+
+ // Synchronize Camera change information to Threejs
+ new CameraSync( map, camera, world );
+
+ const url = 'https://raw.githubusercontent.com/NASA-AMMOS/3DTilesSampleData/master/msl-dingo-gap/0528_0260184_to_s64o256_colorize/0528_0260184_to_s64o256_colorize/0528_0260184_to_s64o256_colorize_tileset.json';
+
+ tiles = new TilesRenderer( url );
+
+ tiles.setCamera( camera );
+ tiles.setResolutionFromRenderer( camera, renderer );
+
+ tiles.onLoadTileSet = ( tileset ) => {
+
+ const box = new THREE.Box3();
+ const sphere = new THREE.Sphere();
+ const matrix = new THREE.Matrix4();
+
+ let position;
+ let distanceToEllipsoidCenter;
+
+ if ( tiles.getOrientedBounds( box, matrix ) ) {
+
+ position = new THREE.Vector3().setFromMatrixPosition( matrix );
+ distanceToEllipsoidCenter = position.length();
+
+ } else if ( tiles.getBoundingSphere( sphere ) ) {
+
+ position = sphere.center.clone();
+ distanceToEllipsoidCenter = position.length();
+
+ }
+
+ const surfaceDirection = position.normalize();
+ const up = new THREE.Vector3( 0, 0, 0 );
+ const rotationToNorthPole = rotationBetweenDirections(
+ surfaceDirection,
+ up
+ );
+
+ tiles.group.quaternion.x = rotationToNorthPole.x;
+ tiles.group.quaternion.y = rotationToNorthPole.y;
+ tiles.group.quaternion.z = rotationToNorthPole.z;
+ tiles.group.quaternion.w = rotationToNorthPole.w;
+
+ tiles.group.position.y = - distanceToEllipsoidCenter;
+
+ };
+
+ tilesGroup.add( tiles.group );
+
+ const pos = projectToWorld( origin );
+ tilesGroup.position.copy( pos );
+ tilesGroup.position.z = 1;
+
+ // Since the 3D model is in real world meters, a scale transform needs to be
+ // applied since the CustomLayerInterface expects units in MercatorCoordinates.
+ const scale = projectedUnitsPerMeter( origin[ 1 ] );
+ tilesGroup.scale.set( scale, scale, scale );
+
+ tilesGroup.rotation.x = Math.PI;
+
+ },
+
+ render: function ( gl, matrix ) {
+
+ renderer.resetState();
+
+ // update tiles
+ camera.updateMatrixWorld();
+ tiles.update();
+
+ renderer.render( scene, camera );
+ map.triggerRepaint();
+
+ },
+ } );
+
+ } );
+
+}
+
+function reload() {
+
+ map.remove();
+ init();
+
+}
diff --git a/example/mapboxExampleCamera.js b/example/mapboxExampleCamera.js
new file mode 100644
index 000000000..c438227ed
--- /dev/null
+++ b/example/mapboxExampleCamera.js
@@ -0,0 +1,209 @@
+import * as THREE from 'three';
+
+// The code in this file comes from https://github.com/jscastro76/threebox/tree/master I have simplified and modified some content
+// 512 * 2000
+// 512: TILE_SIZE
+const WORLD_SIZE = 10240000;
+const EARTH_RADIUS = 6371008.8; // from Mapbox https://github.com/mapbox/mapbox-gl-js/blob/0063cbd10a97218fb6a0f64c99bf18609b918f4c/src/geo/lng_lat.js#L11
+const MapConstants = {
+ WORLD_SIZE: WORLD_SIZE,
+ PROJECTION_WORLD_SIZE: WORLD_SIZE / ( EARTH_RADIUS * Math.PI * 2 ),
+ MERCATOR_A: EARTH_RADIUS,
+ DEG2RAD: Math.PI / 180,
+ EARTH_CIRCUMFERENCE: 2 * Math.PI * EARTH_RADIUS,
+ TILE_SIZE: 512,
+};
+
+function projectToWorld( coords ) {
+
+ // Spherical mercator forward projection, re-scaling to WORLD_SIZE
+ var projected = [
+ - MapConstants.MERCATOR_A * MapConstants.DEG2RAD * coords[ 0 ] * MapConstants.PROJECTION_WORLD_SIZE,
+ - MapConstants.MERCATOR_A * Math.log( Math.tan( Math.PI * 0.25 + 0.5 * MapConstants.DEG2RAD * coords[ 1 ] ) ) * MapConstants.PROJECTION_WORLD_SIZE,
+ ];
+
+ // z dimension, defaulting to 0 if not provided
+ if ( ! coords[ 2 ] ) projected.push( 0 );
+ else {
+
+ var pixelsPerMeter = projectedUnitsPerMeter( coords[ 1 ] );
+ projected.push( coords[ 2 ] * pixelsPerMeter );
+
+ }
+
+ var result = new THREE.Vector3( projected[ 0 ], projected[ 1 ], projected[ 2 ] );
+
+ return result;
+
+}
+
+function projectedUnitsPerMeter( latitude ) {
+
+ return Math.abs(
+ MapConstants.WORLD_SIZE / Math.cos( MapConstants.DEG2RAD * latitude ) / MapConstants.EARTH_CIRCUMFERENCE );
+
+}
+
+class CameraSync {
+
+ constructor( map, camera, world ) {
+
+ this.map = map;
+ this.camera = camera;
+ this.active = true;
+ // We're in charge of the camera now!
+ this.camera.matrixAutoUpdate = false;
+ // Postion and configure the world group so we can scale it appropriately when the camera zooms
+ this.world = world || new THREE.Group();
+ this.world.position.x = this.world.position.y =
+ MapConstants.WORLD_SIZE / 2;
+ this.world.matrixAutoUpdate = false;
+ // set up basic camera state
+ this.state = {
+ translateCenter: new THREE.Matrix4().makeTranslation(
+ MapConstants.WORLD_SIZE / 2,
+ - MapConstants.WORLD_SIZE / 2,
+ 0
+ ),
+ worldSizeRatio: MapConstants.TILE_SIZE / MapConstants.WORLD_SIZE,
+ };
+ // Listen for move events from the map and update the Three.js camera
+
+ this.map
+ .on( 'move', () =>{
+
+ this.updateCamera();
+
+ } )
+ .on( 'resize', () =>{
+
+ this.setupCamera();
+
+ } );
+
+ this.setupCamera();
+
+ }
+
+ setupCamera() {
+
+ const t = this.map.transform;
+ // if aspect is not reset raycast will fail on map resize
+ this.camera.aspect = t.width / t.height;
+ this.halfFov = t._fov / 2;
+ this.cameraToCenterDistance = ( 0.5 / Math.tan( this.halfFov ) ) * t.height;
+ const maxPitch = ( t._maxPitch * Math.PI ) / 180;
+ this.acuteAngle = Math.PI / 2 - maxPitch;
+ this.updateCamera();
+
+ }
+
+ updateCamera( ev ) {
+
+ if ( ! this.camera ) {
+
+ console.log( 'nocamera' );
+ return;
+
+ }
+
+ const t = this.map.transform;
+
+ this.camera.aspect = t.width / t.height;
+
+ this.halfFov = t._fov / 2;
+ // pitch seems to influence heavily the depth calculation and cannot be more than 60 = PI/3 < v1 and 85 > v2
+ const pitchAngle = Math.cos( Math.PI / 2 - t._pitch );
+ this.cameraToCenterDistance = ( 0.5 / Math.tan( this.halfFov ) ) * t.height;
+
+ const worldSize = this.worldSize();
+
+ this.cameraTranslateZ = new THREE.Matrix4().makeTranslation(
+ 0,
+ 0,
+ this.cameraToCenterDistance
+ );
+ // someday @ansis set further near plane to fix precision for deckgl,so we should fix it to use mapbox-gl v1.3+ correctly
+ // https://github.com/mapbox/mapbox-gl-js/commit/5cf6e5f523611bea61dae155db19a7cb19eb825c#diff-5dddfe9d7b5b4413ee54284bc1f7966d
+ const nz = t.height / 50; // min near z as coded by @ansis
+ const nearZ = Math.max( nz * pitchAngle, nz ); // on changes in the pitch nz could be too low
+
+ const h = t.height;
+ const w = t.width;
+ this.camera.projectionMatrix.elements = t._camera.getCameraToClipPerspective(
+ t._fov,
+ w / h,
+ nearZ,
+ t._farZ
+ );
+ // Unlike the Mapbox GL JS camera, separate camera translation and rotation out into its world matrix
+ // If this is applied directly to the projection matrix, it will work OK but break raycasting
+ const cameraWorldMatrix = this.calcCameraMatrix( t._pitch, t.angle );
+ // When terrain layers are included, height of 3D layers must be modified from t_camera.z * worldSize
+ if ( t.elevation )
+ cameraWorldMatrix.elements[ 14 ] = t._camera.position[ 2 ] * worldSize;
+ // this.camera.matrixWorld.elements is equivalent to t._camera._transform
+ this.camera.matrixWorld.copy( cameraWorldMatrix );
+
+ const zoomPow = t.scale * this.state.worldSizeRatio;
+ // Handle scaling and translation of objects in the map in the world's matrix transform, not the camera
+ const scale = new THREE.Matrix4();
+ const translateMap = new THREE.Matrix4();
+ const rotateMap = new THREE.Matrix4();
+
+ scale.makeScale( zoomPow, zoomPow, zoomPow );
+
+ const x = t.x || t.point.x;
+ const y = t.y || t.point.y;
+ translateMap.makeTranslation( - x, y, 0 );
+ rotateMap.makeRotationZ( Math.PI );
+
+ this.world.matrix = new THREE.Matrix4()
+ .premultiply( rotateMap )
+ .premultiply( this.state.translateCenter )
+ .premultiply( scale )
+ .premultiply( translateMap );
+
+ }
+
+ worldSize() {
+
+ const t = this.map.transform;
+ return t.tileSize * t.scale;
+
+ }
+
+ mercatorZfromAltitude( altitude, lat ) {
+
+ return altitude / this.circumferenceAtLatitude( lat );
+
+ }
+
+ circumferenceAtLatitude( latitude ) {
+
+ return (
+ MapConstants.EARTH_CIRCUMFERENCE *
+ Math.cos( ( latitude * Math.PI ) / 180 )
+ );
+
+ }
+
+ calcCameraMatrix( pitch, angle, trz ) {
+
+ const t = this.map.transform;
+ const _pitch = pitch === undefined ? t._pitch : pitch;
+ const _angle = angle === undefined ? t.angle : angle;
+ const _trz = trz === undefined ? this.cameraTranslateZ : trz;
+
+ return new THREE.Matrix4()
+ .premultiply( _trz )
+ .premultiply( new THREE.Matrix4().makeRotationX( _pitch ) )
+ .premultiply( new THREE.Matrix4().makeRotationZ( _angle ) );
+
+ }
+
+}
+
+export { projectToWorld, projectedUnitsPerMeter };
+
+export default CameraSync;