Created
January 24, 2019 06:38
-
-
Save morlay/fab29e0fb5f0c04ed2a0760ceb8514bd to your computer and use it in GitHub Desktop.
MapWorld.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { mat4 } from "gl-matrix"; | |
import { Map, Transform } from "mapbox-gl"; | |
import { | |
Camera, | |
Group, | |
Light, | |
Matrix4, | |
Object3D, | |
Scene, | |
Vector3, | |
WebGLRenderer, | |
} from "three"; | |
export class MapWorld { | |
private camera = new Camera(); | |
private scene = new Scene(); | |
private world = new Group(); | |
private renderer: WebGLRenderer; | |
MERCATOR_A = 6378137.0; // 3857 projection property | |
EARTH_CIRCUMFERENCE = 40075000; // meters | |
WORLD_SIZE: number; | |
PROJECTION_WORLD_SIZE: number; | |
constructor(public map: Map, gl: WebGLRenderingContext) { | |
this.WORLD_SIZE = this.map.transform.tileSize; | |
this.PROJECTION_WORLD_SIZE = this.WORLD_SIZE / (6378137.0 * Math.PI) / 2; | |
this.renderer = new WebGLRenderer({ | |
alpha: true, | |
antialias: true, | |
canvas: map.getCanvas(), | |
context: gl, | |
}); | |
this.renderer.autoClear = false; | |
this.renderer.shadowMap.enabled = true; | |
this.scene.add(this.world); | |
this.world.matrixAutoUpdate = false; | |
this.camera.matrixAutoUpdate = false; | |
} | |
setLight(light: Light) { | |
this.scene.add(light); | |
} | |
add(...objs: Object3D[]) { | |
this.world.add(...objs); | |
} | |
positionViewpoint( | |
obj: Object3D, | |
coordinates: [number, number] | [number, number, number], | |
) { | |
return this.position(obj, coordinates, { | |
preScale: 1 / this.map.transform.scale, | |
scaleToLatitude: false, | |
}); | |
} | |
position( | |
obj: Object3D, | |
coordinates: [number, number] | [number, number, number], | |
{ | |
preScale = 1, | |
scaleToLatitude = true, | |
}: { preScale?: number; scaleToLatitude?: boolean } = {}, | |
) { | |
const scale = new Vector3(preScale, preScale, preScale); | |
if (scaleToLatitude) { | |
const pixelsPerMeter = this.projectedUnitsPerMeter(coordinates[1]); | |
scale.multiplyScalar(pixelsPerMeter); | |
} | |
obj.position.copy( | |
this.project(coordinates[0], coordinates[1], coordinates[2]), | |
); | |
obj.scale.copy(scale); | |
obj.userData.coordinates = coordinates; | |
obj.userData.scaleToLatitude = scaleToLatitude; | |
return obj; | |
} | |
project(lng: number, lat: number, alt: number = 0) { | |
return new Vector3( | |
((-this.MERCATOR_A * lng * Math.PI) / 180) * this.PROJECTION_WORLD_SIZE, | |
-this.MERCATOR_A * | |
Math.log(Math.tan(Math.PI / 4 + (0.5 * lat * Math.PI) / 180)) * | |
this.PROJECTION_WORLD_SIZE, | |
alt * this.projectedUnitsPerMeter(lat), | |
); | |
} | |
projectedUnitsPerMeter(latitude: number) { | |
return Math.abs( | |
(this.WORLD_SIZE * (1 / Math.cos((latitude * Math.PI) / 180))) / | |
this.EARTH_CIRCUMFERENCE, | |
); | |
} | |
destroy() {} | |
update() { | |
this.updateCameraAndWorld(); | |
this.renderer.state.reset(); | |
this.renderer.render(this.scene, this.camera); | |
} | |
updateCameraAndWorld() { | |
const trans = this.map.transform; | |
{ | |
this.camera.projectionMatrix.elements = mat4.perspective( | |
mat4.create(), | |
trans._fov, | |
trans.width / trans.height, | |
1, | |
calcFarZ(trans), | |
); | |
} | |
// 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 = new Matrix4() | |
.premultiply( | |
new Matrix4().makeTranslation(0, 0, trans.cameraToCenterDistance), | |
) | |
.premultiply(new Matrix4().makeRotationX(trans._pitch)) | |
.premultiply(new Matrix4().makeRotationZ(trans.angle)); | |
this.camera.matrixWorld.copy(cameraWorldMatrix); | |
} | |
{ | |
const { x, y } = trans.point; | |
const scale = trans.scale; | |
const tileSize = trans.tileSize; | |
const worldMatrix = new Matrix4() | |
.premultiply(new Matrix4().makeRotationZ(Math.PI)) | |
.premultiply( | |
new Matrix4().makeTranslation(tileSize / 2, -tileSize / 2, 0), | |
) | |
.premultiply(new Matrix4().makeScale(scale, scale, scale)) | |
.premultiply(new Matrix4().makeTranslation(-x, y, 0)); | |
this.world.matrix.copy(worldMatrix); | |
} | |
} | |
} | |
function calcFarZ(trans: Transform) { | |
// copy from mapbox-gl/src/geo/Transform._calcMatrices | |
const halfFov = trans._fov / 2; | |
const groundAngle = Math.PI / 2 + trans._pitch; | |
const topHalfSurfaceDistance = | |
(Math.sin(halfFov) * trans.cameraToCenterDistance) / | |
Math.sin(Math.PI - groundAngle - halfFov); | |
// Calculate z distance of the farthest fragment that should be rendered. | |
const furthestDistance = | |
Math.cos(Math.PI / 2 - trans._pitch) * topHalfSurfaceDistance + | |
trans.cameraToCenterDistance; | |
// Add a bit extra to avoid precision problems when a fragment's distance is exactly `furthestDistance` | |
return furthestDistance * 1.01; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment