Guys, I need you to help me please!
I have a globe (three-globe) at the center of my scene, a perspective camera that sits slightly above the horizon (and is tilted slightly down) pointing at the globe at the center of the scene, and an orbit controller.
All over the globe, I have locations - with latitude and longitude coordinates - for which I want the camera to rotate to and focus on a single location when I trigger an event. I want the camera to rotate around the Z axis only, and the selected location to come to rest in the center of the camera at the end of the rotation (in the horizontal). Even if the selected location is at the top or bottom of the globe, the camera angle and heights should not change.
How can I calculate the point that the camera moves to this point, which is on the globe?
I have tried to visualize the above once again. The small red dot represents the known location on the globe and the end point of the example. W and H are constant distances over the rotation.
(And here is the perspective from camera view)
If you are interested in what I have done so far. This is what the project looks like up to this point.
main.js
import * as TWEEN from "@tweenjs/tween.js";
import * as THREE from "three";
import { gConf } from './config.js';
import { countriesWithHQ, Globe, initGlobe } from './globe.js';
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
import { CSS2DRenderer } from 'three/examples/jsm/renderers/CSS2DRenderer.js';
const domNodes = {
map: '.mp-world__map',
btn: '.mp-world__actions button',
};
export let renderer, camera, scene, controls;
init();
initGlobe();
animate();
// SECTION Initializing core ThreeJS elements
function init() {
// Initialize renderer
renderer = [new THREE.WebGLRenderer({ antialias: true }), new CSS2DRenderer()];
renderer.forEach((r, idx) => {
r.setSize(gConf.window.width, gConf.window.height);
if (idx > 0) {
// overlay additional on top of main renderer
r.domElement.style.position = 'absolute';
r.domElement.style.top = '0px';
r.domElement.style.pointerEvents = 'none';
}
document.querySelector(domNodes.map).appendChild(r.domElement);
});
// Initialize scene, light
scene = new THREE.Scene();
scene.add(new THREE.AmbientLight(0xffffff, 1));
scene.background = new THREE.Color(0xffffff);
// Initialize camera, light
camera = new THREE.PerspectiveCamera();
camera.aspect = gConf.window.width / gConf.window.height;
camera.position.z = 300;
camera.updateProjectionMatrix();
scene.add(camera);
// Helpers
const axesHelper = new THREE.AxesHelper(400);
scene.add(axesHelper);
const cameraPerspectiveHelper = new THREE.CameraHelper(camera);
scene.add(cameraPerspectiveHelper);
// Initialize controls
controls = new OrbitControls(camera, renderer[0].domElement);
controls.enableDamping = true;
controls.dynamicDampingFactor = 0.01;
controls.enablePan = false;
controls.enableZoom = false;
controls.rotateSpeed = 0.8;
controls.zoomSpeed = 1;
controls.autoRotate = false;
// Adjust the angle of the globe so that all our countries are best displayed.
const maxPoleAngle = .6;
controls.minPolarAngle = maxPoleAngle;
controls.maxPolarAngle = maxPoleAngle;
}
function goToCoords(country = 'DE') {
const coordinates = countriesWithHQ.mids[country].geometry.coordinates;
// For whatever reason, the geojson coordinates lat and lng are swapped.
// So the first value is the longitude and the second is the latitude.
const newPos = new THREE.Vector3(coordinates[1], coordinates[0], 0);
// converts (lat, lng) to world position
const coords = Globe.getCoords(coordinates[1], coordinates[0], 0)
/* ---------------------------------------------------- */
// Here the camera should be turned to the right position
/* ---------------------------------------------------- */
}
function animate() {
renderer.forEach((r) => r.render(scene, camera));
camera.lookAt(scene.position);
TWEEN.update();
controls.update();
requestAnimationFrame(animate);
}
// Button interactions
const buttons = document.querySelectorAll(domNodes.btn);
buttons.forEach(b => b.addEventListener('click', (e) => goToCoords(e.target.dataset.cid)));
globe.js
import * as THREE from "three";
import ThreeGlobe from "three-globe";
import { gConf } from './config.js';
import { camera, scene, controls } from './main.js';
import countries from './globe-data-min.json';
import * as turf from '@turf/turf'
const domNodes = {
ctr: '.mp-world',
};
// Get globe data from the data attribute
const gData = JSON.parse(document.querySelector(domNodes.ctr).dataset.locations);
const markerSvg = `<svg viewBox="0 0 36 36">
<circle fill="white" stroke="currentColor" cx="18" cy="18" stroke-width="3" r="16"></circle>
<circle fill="currentColor" cx="18" cy="18" r="6"></circle>
</svg>`;
export let Globe;
// Store all country cIds without dublicates / That means we have one or more offices
export const countriesWithHQ = { cIds: [...new Set(gData.map(d => d.cid))], mids: {} };
export function initGlobe() {
const countriesCenterPoints = Array();
countriesWithHQ.cIds.forEach(c => {
// Set geojson features (country coordinates and so on) of countries where we have an office
// Get the center lat & lng coordinates of the center of the coutries bounding box
const midObj = getCenterCoordinates(countries.features.filter(f => f.properties.ISO_A2 === c)[0]);
countriesWithHQ.mids[c] = midObj;
countriesCenterPoints.push({ lat: midObj.geometry.coordinates[1], lng: midObj.geometry.coordinates[0] });
});
Globe = new ThreeGlobe({
waitForGlobeReady: true,
})
// Adds little "dots" to the globe with represent the countries
.hexPolygonsData(countries.features)
.hexPolygonResolution(3)
.hexPolygonMargin(0.35)
// If we have a office in that given country, change the default color to our primary color
.hexPolygonColor((e) => {
if (countriesWithHQ.cIds.includes(e.properties.ISO_A2)) {
return gConf.colors.primary;
} else {
return gConf.colors.primary_20;
}
})
.showAtmosphere(true)
.atmosphereColor(gConf.colors.primary)
.atmosphereAltitude(0.25)
// Places the offie markers
.htmlAltitude(0)
.htmlElementsData(gData)
.htmlElement((d) => {
const el = document.createElement('div');
el.innerHTML = markerSvg;
el.style.color = gConf.colors.secondary;
el.style.width = `${gConf.marker.size}px`;
return el;
});
const globeMaterial = Globe.globeMaterial();
globeMaterial.color = new THREE.Color(0xffffff);
Globe.setPointOfView(camera.position, Globe.position);
controls.addEventListener('change', () => {
Globe.setPointOfView(camera.position, Globe.position)
});
const lineThickness = 0.1;
const lineAltitude = 0.2;
const globeRadius = Globe.getGlobeRadius();
const globeColor = gConf.colors.primary_light;
let longMesh, latMesh;
// Lines of longitude
for (let n = 0; n < 24; ++n) {
const line = new THREE.CylinderGeometry(globeRadius + lineAltitude, globeRadius + lineAltitude, lineThickness, 50, 1, true);
line.translate(0, -lineThickness / 2, 0);
line.rotateX(Math.PI / 2);
line.rotateY(n * Math.PI / 12);
longMesh = new THREE.Mesh(line, new THREE.MeshBasicMaterial({ color: globeColor }));
Globe.add(longMesh);
}
// Lines of latitude
for (let n = 1; n < 12; ++n) {
const lat = (n - 6) * Math.PI / 12;
const r = globeRadius * Math.cos(lat);
const y = globeRadius * Math.sin(lat);
const r1 = r - lineThickness * Math.sin(lat) / 2;
const r2 = r + lineThickness * Math.sin(lat) / 2;
const line = new THREE.CylinderGeometry(r1 + lineAltitude, r2 + lineAltitude, Math.cos(lat) * lineThickness, 50, 8, true);
line.translate(0, -Math.cos(lat) * lineThickness / 2 + y, 0);
latMesh = new THREE.Mesh(line, new THREE.MeshBasicMaterial({ color: globeColor }));
Globe.add(latMesh);
}
scene.add(Globe);
}
function getCenterCoordinates(feature) {
const { type, coordinates } = feature.geometry;
let polygons;
if (type === "Polygon") {
polygons = turf.polygon(coordinates);
} else {
polygons = turf.multiPolygon(coordinates);
}
return turf.centroid(polygons);
}