Skip to content

Instantly share code, notes, and snippets.

@morlay
Created January 24, 2019 06:38
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save morlay/fab29e0fb5f0c04ed2a0760ceb8514bd to your computer and use it in GitHub Desktop.
Save morlay/fab29e0fb5f0c04ed2a0760ceb8514bd to your computer and use it in GitHub Desktop.
MapWorld.ts
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