From 5a2ffe281e12f9beef1bfbcc5e3679f32af884b5 Mon Sep 17 00:00:00 2001 From: xxf <876029169@qq.com> Date: Fri, 29 Dec 2023 18:28:42 +0800 Subject: [PATCH 1/5] Add Mapbox GL JS example --- example/mapboxExample.html | 48 ++++++ example/mapboxExample.js | 145 +++++++++++++++++ example/mapboxExampleCamera.js | 287 +++++++++++++++++++++++++++++++++ 3 files changed, 480 insertions(+) create mode 100644 example/mapboxExample.html create mode 100644 example/mapboxExample.js create mode 100644 example/mapboxExampleCamera.js diff --git a/example/mapboxExample.html b/example/mapboxExample.html new file mode 100644 index 00000000..1de2d29b --- /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 00000000..05526de6 --- /dev/null +++ b/example/mapboxExample.js @@ -0,0 +1,145 @@ +import * as THREE from 'three'; +import CameraSync, { projectToWorld, projectedUnitsPerMeter } from './mapboxExampleCamera.js'; +import { DebugTilesRenderer as TilesRenderer } from '../src/index.js'; + +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; + +} + +const origin = [ 116, 35 ]; + +// TO MAKE THE MAP APPEAR YOU MUST +// ADD YOUR ACCESS TOKEN FROM +// https://account.mapbox.com +// This is my private key, it is prohibited to use in the project +window.mapboxgl.accessToken = + 'pk.eyJ1IjoieGlheGlhbmdmbmVnIiwiYSI6ImNscTRzc245NTA5c3cya3BhdXA4MDY0NWQifQ._9T_J9z7Hvcd00nQL3UuSw'; +const 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 ); + + // lights + const dirLight = new THREE.DirectionalLight( 0xffffff, 0.8 ); + dirLight.position.set( 1, 2, 3 ); + world.add( dirLight ); + + const ambLight = new THREE.AmbientLight( 0xffffff, 1 ); + world.add( ambLight ); + + tilesGroup = new THREE.Group(); + + world.add( tilesGroup ); + + new CameraSync( map, camera, world ); + + const url = 'https://xiaxiangfeng.github.io/3DTilesRendererJS.Test/baowei21/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, 1, 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 ); + + const scale = projectedUnitsPerMeter( origin[ 1 ] ); + tilesGroup.scale.set( + scale, + scale, + scale + ); + + tilesGroup.rotation.x = Math.PI / 2; + + }, + + render: function ( gl, matrix ) { + + renderer.resetState(); + tiles.update(); + renderer.render( scene, camera ); + map.triggerRepaint(); + + }, + } ); + +} ); diff --git a/example/mapboxExampleCamera.js b/example/mapboxExampleCamera.js new file mode 100644 index 00000000..2a47075a --- /dev/null +++ b/example/mapboxExampleCamera.js @@ -0,0 +1,287 @@ +import * as THREE from 'three'; + +const utils = { + clamp: function ( n, min, max ) { + + return Math.min( max, Math.max( min, n ) ); + + }, + makePerspectiveMatrix: function ( fovy, aspect, near, far ) { + + var out = new THREE.Matrix4(); + var f = 1.0 / Math.tan( fovy / 2 ), + nf = 1 / ( near - far ); + var newMatrix = [ + f / aspect, + 0, + 0, + 0, + 0, + f, + 0, + 0, + 0, + 0, + ( far + near ) * nf, + - 1, + 0, + 0, + 2 * far * near * nf, + 0, + ]; + + out.elements = newMatrix; + return out; + + }, +}; + +const WORLD_SIZE = 10240000; +const FOV_ORTHO = ( 0.1 / 180 ) * Math.PI; +const FOV = Math.atan( 3 / 4 ); +const EARTH_RADIUS = 6371008.8; +const EARTH_CIRCUMFERENCE_EQUATOR = 40075017; +const MapConstants = { + WORLD_SIZE: WORLD_SIZE, + PROJECTION_WORLD_SIZE: WORLD_SIZE / ( EARTH_RADIUS * Math.PI * 2 ), + MERCATOR_A: EARTH_RADIUS, + DEG2RAD: Math.PI / 180, + RAD2DEG: 180 / Math.PI, + EARTH_RADIUS: EARTH_RADIUS, + EARTH_CIRCUMFERENCE: 2 * Math.PI * EARTH_RADIUS, + EARTH_CIRCUMFERENCE_EQUATOR: EARTH_CIRCUMFERENCE_EQUATOR, + FOV_ORTHO: FOV_ORTHO, + FOV: FOV, + FOV_DEGREES: ( FOV * 180 ) / Math.PI, + TILE_SIZE: 512, +}; + +const Constants = MapConstants; + +function projectToWorld( coords ) { + + var projected = [ + - Constants.MERCATOR_A * + Constants.DEG2RAD * + coords[ 0 ] * + Constants.PROJECTION_WORLD_SIZE, + - Constants.MERCATOR_A * + Math.log( Math.tan( Math.PI * 0.25 + 0.5 * Constants.DEG2RAD * coords[ 1 ] ) ) * + Constants.PROJECTION_WORLD_SIZE, + ]; + + 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( + Constants.WORLD_SIZE / + Math.cos( Constants.DEG2RAD * latitude ) / + Constants.EARTH_CIRCUMFERENCE + ); + +} + +class CameraSync { + + constructor( map, camera, world ) { + + this.map = map; + this.camera = camera; + this.active = true; + + this.camera.matrixAutoUpdate = false; + + this.world = world || new THREE.Group(); + this.world.position.x = this.world.position.y = + MapConstants.WORLD_SIZE / 2; + this.world.matrixAutoUpdate = false; + + this.state = { + translateCenter: new THREE.Matrix4().makeTranslation( + MapConstants.WORLD_SIZE / 2, + - MapConstants.WORLD_SIZE / 2, + 0 + ), + worldSizeRatio: MapConstants.TILE_SIZE / MapConstants.WORLD_SIZE, + worldSize: MapConstants.TILE_SIZE * this.map.transform.scale, + }; + + const _this = this; + this.map + .on( 'move', function () { + + _this.updateCamera(); + + } ) + .on( 'resize', function () { + + _this.setupCamera(); + + } ); + + this.setupCamera(); + + } + + setupCamera() { + + const t = this.map.transform; + 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; + let farZ = 0; + let furthestDistance = 0; + this.halfFov = t._fov / 2; + const groundAngle = Math.PI / 2 + t._pitch; + const pitchAngle = Math.cos( Math.PI / 2 - t._pitch ); + this.cameraToCenterDistance = ( 0.5 / Math.tan( this.halfFov ) ) * t.height; + + let pixelsPerMeter = 1; + const worldSize = this.worldSize(); + + pixelsPerMeter = this.mercatorZfromAltitude( 1, t.center.lat ) * worldSize; + const fovAboveCenter = t._fov * ( 0.5 + t.centerOffset.y / t.height ); + + + const minElevationInPixels = t.elevation + ? t.elevation.getMinElevationBelowMSL() * pixelsPerMeter + : 0; + + const cameraToSeaLevelDistance = + ( t._camera.position[ 2 ] * worldSize - minElevationInPixels ) / + Math.cos( t._pitch ); + + const topHalfSurfaceDistance = + ( Math.sin( fovAboveCenter ) * cameraToSeaLevelDistance ) / + Math.sin( + utils.clamp( + Math.PI - groundAngle - fovAboveCenter, + 0.01, + Math.PI - 0.01 + ) + ); + + + furthestDistance = + pitchAngle * topHalfSurfaceDistance + cameraToSeaLevelDistance; + + const horizonDistance = cameraToSeaLevelDistance * ( 1 / t._horizonShift ); + farZ = Math.min( furthestDistance * 1.01, horizonDistance ); + + this.cameraTranslateZ = new THREE.Matrix4().makeTranslation( + 0, + 0, + this.cameraToCenterDistance + ); + + const nz = t.height / 50; + const nearZ = Math.max( nz * pitchAngle, nz ); + + const h = t.height; + const w = t.width; + this.camera.projectionMatrix = utils.makePerspectiveMatrix( + t._fov, + w / h, + nearZ, + farZ + ); + + const cameraWorldMatrix = this.calcCameraMatrix( t._pitch, t.angle ); + + if ( t.elevation ) + cameraWorldMatrix.elements[ 14 ] = t._camera.position[ 2 ] * worldSize; + + this.camera.matrixWorld.copy( cameraWorldMatrix ); + + const zoomPow = t.scale * this.state.worldSizeRatio; + 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; From 5c17cffb847cbb51b32611fd4ce365670daef8a3 Mon Sep 17 00:00:00 2001 From: xxf <876029169@qq.com> Date: Tue, 2 Jan 2024 18:36:47 +0800 Subject: [PATCH 2/5] Update mapboxExample --- example/mapboxExample.js | 209 +++++++++++++++++++-------------- example/mapboxExampleCamera.js | 44 ++++--- 2 files changed, 147 insertions(+), 106 deletions(-) diff --git a/example/mapboxExample.js b/example/mapboxExample.js index 05526de6..20a9e7ce 100644 --- a/example/mapboxExample.js +++ b/example/mapboxExample.js @@ -1,6 +1,26 @@ import * as THREE from 'three'; -import CameraSync, { projectToWorld, projectedUnitsPerMeter } from './mapboxExampleCamera.js'; +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'; + +const params = { + + 'accessToken': 'pk.eyJ1IjoieGlheGlhbmdmbmVnIiwiYSI6ImNscTRzc245NTA5c3cya3BhdXA4MDY0NWQifQ._9T_J9z7Hvcd00nQL3UuSw', + '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 ) { @@ -16,130 +36,141 @@ function rotationBetweenDirections( dir1, dir2 ) { } +let map; const origin = [ 116, 35 ]; -// TO MAKE THE MAP APPEAR YOU MUST -// ADD YOUR ACCESS TOKEN FROM -// https://account.mapbox.com -// This is my private key, it is prohibited to use in the project -window.mapboxgl.accessToken = - 'pk.eyJ1IjoieGlheGlhbmdmbmVnIiwiYSI6ImNscTRzc245NTA5c3cya3BhdXA4MDY0NWQifQ._9T_J9z7Hvcd00nQL3UuSw'; -const map = new window.mapboxgl.Map( { - container: 'map', - style: 'mapbox://styles/mapbox/dark-v9', - zoom: 17.86614600777933, - pitch: 70, - bearing: - 40, - center: origin, -} ); +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', () => { -map.on( 'load', () => { + let renderer, scene, camera, world, tiles, tilesGroup; - let renderer, scene, camera, world, tiles, tilesGroup; + map.addLayer( { + id: 'custom_layer', + type: 'custom', + onAdd: function ( map, mbxContext ) { - 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 = new THREE.WebGLRenderer( { - alpha: true, - antialias: true, - canvas: map.getCanvas(), - context: mbxContext, - } ); + renderer.shadowMap.enabled = true; + renderer.autoClear = false; - renderer.shadowMap.enabled = true; - renderer.autoClear = false; + scene = new THREE.Scene(); + camera = new THREE.Camera(); - scene = new THREE.Scene(); - camera = new THREE.Camera(); + world = new THREE.Group(); + scene.add( world ); - world = new THREE.Group(); - scene.add( world ); + // lights + const dirLight = new THREE.DirectionalLight( 0xffffff, 0.8 ); + dirLight.position.set( 1, 2, 3 ); + world.add( dirLight ); - // lights - const dirLight = new THREE.DirectionalLight( 0xffffff, 0.8 ); - dirLight.position.set( 1, 2, 3 ); - world.add( dirLight ); + const ambLight = new THREE.AmbientLight( 0xffffff, 1 ); + world.add( ambLight ); - const ambLight = new THREE.AmbientLight( 0xffffff, 1 ); - world.add( ambLight ); + tilesGroup = new THREE.Group(); - tilesGroup = new THREE.Group(); + world.add( tilesGroup ); - world.add( tilesGroup ); + // Synchronize Camera change information to Threejs + new CameraSync( map, camera, world ); - 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'; - const url = 'https://xiaxiangfeng.github.io/3DTilesRendererJS.Test/baowei21/tileset.json'; + tiles = new TilesRenderer( url ); - tiles = new TilesRenderer( url ); + tiles.setCamera( camera ); + tiles.setResolutionFromRenderer( camera, renderer ); - tiles.setCamera( camera ); - tiles.setResolutionFromRenderer( camera, renderer ); + tiles.onLoadTileSet = ( tileset ) => { - tiles.onLoadTileSet = ( tileset ) => { + const box = new THREE.Box3(); + const sphere = new THREE.Sphere(); + const matrix = new THREE.Matrix4(); - const box = new THREE.Box3(); - const sphere = new THREE.Sphere(); - const matrix = new THREE.Matrix4(); + let position; + let distanceToEllipsoidCenter; - let position; - let distanceToEllipsoidCenter; + if ( tiles.getOrientedBounds( box, matrix ) ) { - if ( tiles.getOrientedBounds( box, matrix ) ) { + position = new THREE.Vector3().setFromMatrixPosition( matrix ); + distanceToEllipsoidCenter = position.length(); - position = new THREE.Vector3().setFromMatrixPosition( matrix ); - distanceToEllipsoidCenter = position.length(); + } else if ( tiles.getBoundingSphere( sphere ) ) { - } else if ( tiles.getBoundingSphere( sphere ) ) { + position = sphere.center.clone(); + distanceToEllipsoidCenter = position.length(); - position = sphere.center.clone(); - distanceToEllipsoidCenter = position.length(); + } - } + const surfaceDirection = position.normalize(); + const up = new THREE.Vector3( 0, 0, 0 ); + const rotationToNorthPole = rotationBetweenDirections( + surfaceDirection, + up + ); - const surfaceDirection = position.normalize(); - const up = new THREE.Vector3( 0, 1, 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.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; - tiles.group.position.y = - distanceToEllipsoidCenter; + }; - }; + tilesGroup.add( tiles.group ); - tilesGroup.add( tiles.group ); + const pos = projectToWorld( origin ); + tilesGroup.position.copy( pos ); - const pos = projectToWorld( origin ); - tilesGroup.position.copy( pos ); + // 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 ); - const scale = projectedUnitsPerMeter( origin[ 1 ] ); - tilesGroup.scale.set( - scale, - scale, - scale - ); + tilesGroup.rotation.x = Math.PI; - tilesGroup.rotation.x = Math.PI / 2; + }, - }, + render: function ( gl, matrix ) { - render: function ( gl, matrix ) { + renderer.resetState(); + tiles.update(); + renderer.render( scene, camera ); + map.triggerRepaint(); - renderer.resetState(); - tiles.update(); - renderer.render( scene, camera ); - map.triggerRepaint(); + }, + } ); - }, } ); -} ); +} + +function reload() { + + map.remove(); + init(); + +} diff --git a/example/mapboxExampleCamera.js b/example/mapboxExampleCamera.js index 2a47075a..a54fc4af 100644 --- a/example/mapboxExampleCamera.js +++ b/example/mapboxExampleCamera.js @@ -36,11 +36,13 @@ const utils = { }, }; +// 512 * 2000 +// 512: TILE_SIZE const WORLD_SIZE = 10240000; -const FOV_ORTHO = ( 0.1 / 180 ) * Math.PI; -const FOV = Math.atan( 3 / 4 ); -const EARTH_RADIUS = 6371008.8; -const EARTH_CIRCUMFERENCE_EQUATOR = 40075017; +const FOV_ORTHO = ( 0.1 / 180 ) * Math.PI; // Mapbox doesn't accept 0 as FOV +const FOV = Math.atan( 3 / 4 ); // from Mapbox https://github.com/mapbox/mapbox-gl-js/blob/main/src/geo/transform.js#L93 +const EARTH_RADIUS = 6371008.8; // from Mapbox https://github.com/mapbox/mapbox-gl-js/blob/0063cbd10a97218fb6a0f64c99bf18609b918f4c/src/geo/lng_lat.js#L11 +const EARTH_CIRCUMFERENCE_EQUATOR = 40075017; // from Mapbox https://github.com/mapbox/mapbox-gl-js/blob/0063cbd10a97218fb6a0f64c99bf18609b918f4c/src/geo/lng_lat.js#L117 const MapConstants = { WORLD_SIZE: WORLD_SIZE, PROJECTION_WORLD_SIZE: WORLD_SIZE / ( EARTH_RADIUS * Math.PI * 2 ), @@ -60,6 +62,7 @@ const Constants = MapConstants; function projectToWorld( coords ) { + // Spherical mercator forward projection, re-scaling to WORLD_SIZE var projected = [ - Constants.MERCATOR_A * Constants.DEG2RAD * @@ -70,6 +73,7 @@ function projectToWorld( coords ) { Constants.PROJECTION_WORLD_SIZE, ]; + // z dimension, defaulting to 0 if not provided if ( ! coords[ 2 ] ) projected.push( 0 ); else { @@ -101,14 +105,14 @@ class CameraSync { 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, @@ -118,7 +122,7 @@ class CameraSync { worldSizeRatio: MapConstants.TILE_SIZE / MapConstants.WORLD_SIZE, worldSize: MapConstants.TILE_SIZE * this.map.transform.scale, }; - + // Listen for move events from the map and update the Three.js camera const _this = this; this.map .on( 'move', function () { @@ -139,6 +143,7 @@ class CameraSync { 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; @@ -164,6 +169,7 @@ class CameraSync { let furthestDistance = 0; this.halfFov = t._fov / 2; const groundAngle = Math.PI / 2 + t._pitch; + // 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; @@ -173,7 +179,8 @@ class CameraSync { pixelsPerMeter = this.mercatorZfromAltitude( 1, t.center.lat ) * worldSize; const fovAboveCenter = t._fov * ( 0.5 + t.centerOffset.y / t.height ); - + // Adjust distance to MSL by the minimum possible elevation visible on screen, + // this way the far plane is pushed further in the case of negative elevation. const minElevationInPixels = t.elevation ? t.elevation.getMinElevationBelowMSL() * pixelsPerMeter : 0; @@ -192,10 +199,10 @@ class CameraSync { ) ); - + // Calculate z distance of the farthest fragment that should be rendered. furthestDistance = pitchAngle * topHalfSurfaceDistance + cameraToSeaLevelDistance; - + // Add a bit extra to avoid precision problems when a fragment's distance is exactly `furthestDistance` const horizonDistance = cameraToSeaLevelDistance * ( 1 / t._horizonShift ); farZ = Math.min( furthestDistance * 1.01, horizonDistance ); @@ -204,9 +211,10 @@ class CameraSync { 0, this.cameraToCenterDistance ); - - const nz = t.height / 50; - const nearZ = Math.max( nz * pitchAngle, nz ); + // 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; @@ -216,15 +224,17 @@ class CameraSync { nearZ, 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(); From 9f5a7895fff18d7b8c50a57f8a1f7d368fe5d589 Mon Sep 17 00:00:00 2001 From: xxf <876029169@qq.com> Date: Tue, 2 Jan 2024 19:22:00 +0800 Subject: [PATCH 3/5] Update mapboxExampleCamera --- example/mapboxExampleCamera.js | 37 +++++++++++----------------------- 1 file changed, 12 insertions(+), 25 deletions(-) diff --git a/example/mapboxExampleCamera.js b/example/mapboxExampleCamera.js index a54fc4af..3e8b63ef 100644 --- a/example/mapboxExampleCamera.js +++ b/example/mapboxExampleCamera.js @@ -64,13 +64,8 @@ function projectToWorld( coords ) { // Spherical mercator forward projection, re-scaling to WORLD_SIZE var projected = [ - - Constants.MERCATOR_A * - Constants.DEG2RAD * - coords[ 0 ] * - Constants.PROJECTION_WORLD_SIZE, - - Constants.MERCATOR_A * - Math.log( Math.tan( Math.PI * 0.25 + 0.5 * Constants.DEG2RAD * coords[ 1 ] ) ) * - Constants.PROJECTION_WORLD_SIZE, + - Constants.MERCATOR_A * Constants.DEG2RAD * coords[ 0 ] * Constants.PROJECTION_WORLD_SIZE, + - Constants.MERCATOR_A * Math.log( Math.tan( Math.PI * 0.25 + 0.5 * Constants.DEG2RAD * coords[ 1 ] ) ) * Constants.PROJECTION_WORLD_SIZE, ]; // z dimension, defaulting to 0 if not provided @@ -91,10 +86,7 @@ function projectToWorld( coords ) { function projectedUnitsPerMeter( latitude ) { return Math.abs( - Constants.WORLD_SIZE / - Math.cos( Constants.DEG2RAD * latitude ) / - Constants.EARTH_CIRCUMFERENCE - ); + Constants.WORLD_SIZE / Math.cos( Constants.DEG2RAD * latitude ) / Constants.EARTH_CIRCUMFERENCE ); } @@ -185,23 +177,18 @@ class CameraSync { ? t.elevation.getMinElevationBelowMSL() * pixelsPerMeter : 0; - const cameraToSeaLevelDistance = - ( t._camera.position[ 2 ] * worldSize - minElevationInPixels ) / - Math.cos( t._pitch ); + const cameraToSeaLevelDistance = ( t._camera.position[ 2 ] * worldSize - minElevationInPixels ) / Math.cos( t._pitch ); - const topHalfSurfaceDistance = - ( Math.sin( fovAboveCenter ) * cameraToSeaLevelDistance ) / - Math.sin( - utils.clamp( - Math.PI - groundAngle - fovAboveCenter, - 0.01, - Math.PI - 0.01 - ) - ); + const topHalfSurfaceDistance = ( Math.sin( fovAboveCenter ) * cameraToSeaLevelDistance ) / Math.sin( + utils.clamp( + Math.PI - groundAngle - fovAboveCenter, + 0.01, + Math.PI - 0.01 + ) + ); // Calculate z distance of the farthest fragment that should be rendered. - furthestDistance = - pitchAngle * topHalfSurfaceDistance + cameraToSeaLevelDistance; + furthestDistance = pitchAngle * topHalfSurfaceDistance + cameraToSeaLevelDistance; // Add a bit extra to avoid precision problems when a fragment's distance is exactly `furthestDistance` const horizonDistance = cameraToSeaLevelDistance * ( 1 / t._horizonShift ); farZ = Math.min( furthestDistance * 1.01, horizonDistance ); From 67cc2cab31281da29af0e70d4ca6f618f1cdda3a Mon Sep 17 00:00:00 2001 From: xxf <876029169@qq.com> Date: Mon, 8 Jan 2024 14:42:21 +0800 Subject: [PATCH 4/5] Update mapboxExample --- example/mapboxExample.js | 17 +++--- example/mapboxExampleCamera.js | 100 ++++----------------------------- 2 files changed, 20 insertions(+), 97 deletions(-) diff --git a/example/mapboxExample.js b/example/mapboxExample.js index 20a9e7ce..f5d231fd 100644 --- a/example/mapboxExample.js +++ b/example/mapboxExample.js @@ -6,9 +6,11 @@ import CameraSync, { 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': 'pk.eyJ1IjoieGlheGlhbmdmbmVnIiwiYSI6ImNscTRzc245NTA5c3cya3BhdXA4MDY0NWQifQ._9T_J9z7Hvcd00nQL3UuSw', + 'accessToken': 'put-your-api-key-here', 'reload': reload, }; @@ -81,14 +83,6 @@ function init() { world = new THREE.Group(); scene.add( world ); - // lights - const dirLight = new THREE.DirectionalLight( 0xffffff, 0.8 ); - dirLight.position.set( 1, 2, 3 ); - world.add( dirLight ); - - const ambLight = new THREE.AmbientLight( 0xffffff, 1 ); - world.add( ambLight ); - tilesGroup = new THREE.Group(); world.add( tilesGroup ); @@ -144,6 +138,7 @@ function init() { 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. @@ -157,7 +152,11 @@ function init() { render: function ( gl, matrix ) { renderer.resetState(); + + // update tiles + camera.updateMatrixWorld(); tiles.update(); + renderer.render( scene, camera ); map.triggerRepaint(); diff --git a/example/mapboxExampleCamera.js b/example/mapboxExampleCamera.js index 3e8b63ef..1cfa9edb 100644 --- a/example/mapboxExampleCamera.js +++ b/example/mapboxExampleCamera.js @@ -1,71 +1,24 @@ import * as THREE from 'three'; -const utils = { - clamp: function ( n, min, max ) { - - return Math.min( max, Math.max( min, n ) ); - - }, - makePerspectiveMatrix: function ( fovy, aspect, near, far ) { - - var out = new THREE.Matrix4(); - var f = 1.0 / Math.tan( fovy / 2 ), - nf = 1 / ( near - far ); - var newMatrix = [ - f / aspect, - 0, - 0, - 0, - 0, - f, - 0, - 0, - 0, - 0, - ( far + near ) * nf, - - 1, - 0, - 0, - 2 * far * near * nf, - 0, - ]; - - out.elements = newMatrix; - return out; - - }, -}; - // 512 * 2000 // 512: TILE_SIZE const WORLD_SIZE = 10240000; -const FOV_ORTHO = ( 0.1 / 180 ) * Math.PI; // Mapbox doesn't accept 0 as FOV -const FOV = Math.atan( 3 / 4 ); // from Mapbox https://github.com/mapbox/mapbox-gl-js/blob/main/src/geo/transform.js#L93 const EARTH_RADIUS = 6371008.8; // from Mapbox https://github.com/mapbox/mapbox-gl-js/blob/0063cbd10a97218fb6a0f64c99bf18609b918f4c/src/geo/lng_lat.js#L11 -const EARTH_CIRCUMFERENCE_EQUATOR = 40075017; // from Mapbox https://github.com/mapbox/mapbox-gl-js/blob/0063cbd10a97218fb6a0f64c99bf18609b918f4c/src/geo/lng_lat.js#L117 const MapConstants = { WORLD_SIZE: WORLD_SIZE, PROJECTION_WORLD_SIZE: WORLD_SIZE / ( EARTH_RADIUS * Math.PI * 2 ), MERCATOR_A: EARTH_RADIUS, DEG2RAD: Math.PI / 180, - RAD2DEG: 180 / Math.PI, - EARTH_RADIUS: EARTH_RADIUS, EARTH_CIRCUMFERENCE: 2 * Math.PI * EARTH_RADIUS, - EARTH_CIRCUMFERENCE_EQUATOR: EARTH_CIRCUMFERENCE_EQUATOR, - FOV_ORTHO: FOV_ORTHO, - FOV: FOV, - FOV_DEGREES: ( FOV * 180 ) / Math.PI, TILE_SIZE: 512, }; -const Constants = MapConstants; - function projectToWorld( coords ) { // Spherical mercator forward projection, re-scaling to WORLD_SIZE var projected = [ - - Constants.MERCATOR_A * Constants.DEG2RAD * coords[ 0 ] * Constants.PROJECTION_WORLD_SIZE, - - Constants.MERCATOR_A * Math.log( Math.tan( Math.PI * 0.25 + 0.5 * Constants.DEG2RAD * coords[ 1 ] ) ) * Constants.PROJECTION_WORLD_SIZE, + - 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 @@ -86,7 +39,7 @@ function projectToWorld( coords ) { function projectedUnitsPerMeter( latitude ) { return Math.abs( - Constants.WORLD_SIZE / Math.cos( Constants.DEG2RAD * latitude ) / Constants.EARTH_CIRCUMFERENCE ); + MapConstants.WORLD_SIZE / Math.cos( MapConstants.DEG2RAD * latitude ) / MapConstants.EARTH_CIRCUMFERENCE ); } @@ -112,19 +65,18 @@ class CameraSync { 0 ), worldSizeRatio: MapConstants.TILE_SIZE / MapConstants.WORLD_SIZE, - worldSize: MapConstants.TILE_SIZE * this.map.transform.scale, }; // Listen for move events from the map and update the Three.js camera - const _this = this; + this.map - .on( 'move', function () { + .on( 'move', () =>{ - _this.updateCamera(); + this.updateCamera(); } ) - .on( 'resize', function () { + .on( 'resize', () =>{ - _this.setupCamera(); + this.setupCamera(); } ); @@ -157,42 +109,14 @@ class CameraSync { const t = this.map.transform; this.camera.aspect = t.width / t.height; - let farZ = 0; - let furthestDistance = 0; + this.halfFov = t._fov / 2; - const groundAngle = Math.PI / 2 + t._pitch; // 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; - let pixelsPerMeter = 1; const worldSize = this.worldSize(); - pixelsPerMeter = this.mercatorZfromAltitude( 1, t.center.lat ) * worldSize; - const fovAboveCenter = t._fov * ( 0.5 + t.centerOffset.y / t.height ); - - // Adjust distance to MSL by the minimum possible elevation visible on screen, - // this way the far plane is pushed further in the case of negative elevation. - const minElevationInPixels = t.elevation - ? t.elevation.getMinElevationBelowMSL() * pixelsPerMeter - : 0; - - const cameraToSeaLevelDistance = ( t._camera.position[ 2 ] * worldSize - minElevationInPixels ) / Math.cos( t._pitch ); - - const topHalfSurfaceDistance = ( Math.sin( fovAboveCenter ) * cameraToSeaLevelDistance ) / Math.sin( - utils.clamp( - Math.PI - groundAngle - fovAboveCenter, - 0.01, - Math.PI - 0.01 - ) - ); - - // Calculate z distance of the farthest fragment that should be rendered. - furthestDistance = pitchAngle * topHalfSurfaceDistance + cameraToSeaLevelDistance; - // Add a bit extra to avoid precision problems when a fragment's distance is exactly `furthestDistance` - const horizonDistance = cameraToSeaLevelDistance * ( 1 / t._horizonShift ); - farZ = Math.min( furthestDistance * 1.01, horizonDistance ); - this.cameraTranslateZ = new THREE.Matrix4().makeTranslation( 0, 0, @@ -205,11 +129,11 @@ class CameraSync { const h = t.height; const w = t.width; - this.camera.projectionMatrix = utils.makePerspectiveMatrix( + this.camera.projectionMatrix.elements = t._camera.getCameraToClipPerspective( t._fov, w / h, nearZ, - farZ + 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 @@ -217,7 +141,7 @@ class CameraSync { // 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.elements is equivalent to t._camera._transform this.camera.matrixWorld.copy( cameraWorldMatrix ); const zoomPow = t.scale * this.state.worldSizeRatio; From bc0e526acea5ff805b72c384c94b7041f2cb71ee Mon Sep 17 00:00:00 2001 From: xxf <876029169@qq.com> Date: Wed, 10 Jan 2024 16:28:47 +0800 Subject: [PATCH 5/5] Note CameraSync comes from threebox --- example/mapboxExampleCamera.js | 1 + 1 file changed, 1 insertion(+) diff --git a/example/mapboxExampleCamera.js b/example/mapboxExampleCamera.js index 1cfa9edb..c438227e 100644 --- a/example/mapboxExampleCamera.js +++ b/example/mapboxExampleCamera.js @@ -1,5 +1,6 @@ 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;