Interactive 3D Globe with Geographic Data Pins

Stunning Three.js 3D globe visualization with animated location pins marking major world cities. Features realistic globe rendering, latitude/longitude grid lines, auto-rotation, and customizable pins for geographic data visualization and interactive mapping.

What this code does

- Three.js Globe Geometry: realistic Earth sphere (64x64 segments) with Phong material for smooth lighting and subtle ocean-blue coloring.
- Geographic Pin System: 10 major world cities (New York, London, Tokyo, Sydney, Mumbai, São Paulo, Cairo, Moscow, Singapore, Cape Town) marked with 3D pin markers.
- Latitude/Longitude Grid: mathematically generated wireframe grid lines overlay the globe showing 20° intervals for geographic reference.
- Pin Geometry: each pin consists of a cylindrical stick and spherical head, positioned using spherical coordinates converted from lat/lon.
- Animated Pins: pulsing animation on pin heads creates attention-grabbing effects synchronized with different phase offsets.
- Color-Coded Locations: each city pin has a unique vibrant color (red, green, blue, yellow, magenta, cyan, orange, purple) for easy differentiation.
- Auto-Rotation: smooth automatic globe rotation with orbit controls for manual inspection and exploration.
- Rim Lighting: directional lights from multiple angles create realistic globe illumination with atmospheric edge glow.
- Interactive Controls: real-time GUI adjustment of rotation speed, pin height, pulse animation, grid opacity, and globe color.
- Geographic Coordinate System: demonstrates conversion of latitude/longitude to 3D Cartesian coordinates using spherical math.
- Data Visualization Ready: easily extendable to display custom geographic datasets, population data, or real-time global statistics.

JavaScript (plain)

import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
import { CSS2DRenderer, CSS2DObject } from 'three/examples/jsm/renderers/CSS2DRenderer.js'

const scene = new THREE.Scene()
scene.background = new THREE.Color(0x0a0a0f)

const camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1000)
camera.position.set(0, 0, 3.5)

const renderer = new THREE.WebGLRenderer({ antialias: true })
renderer.setSize(window.innerWidth, window.innerHeight)

document.body.appendChild(renderer.domElement)

// CSS2D renderer for labels
const labelRenderer = new CSS2DRenderer()
labelRenderer.setSize(window.innerWidth, window.innerHeight)

labelRenderer.domElement.style.position = 'absolute'
labelRenderer.domElement.style.top = '0'
labelRenderer.domElement.style.pointerEvents = 'none'
document.body.appendChild(labelRenderer.domElement)

const controls = new OrbitControls(camera, renderer.domElement)
controls.enableDamping = true
controls.autoRotate = true
controls.autoRotateSpeed = 0.5

// Load Earth texture and create globe
const textureLoader = new THREE.TextureLoader()
const earthTexture = textureLoader.load(
  'https://raw.githubusercontent.com/mrdoob/three.js/dev/examples/textures/planets/earth_atmos_2048.jpg'
)

const globe = new THREE.Mesh(
  new THREE.SphereGeometry(1, 64, 64),
  new THREE.MeshPhongMaterial({
    map: earthTexture,
    shininess: 15
  })
)
scene.add(globe)

// Add grid lines (latitude)
const gridMaterial = new THREE.LineBasicMaterial({ color: 0x4da6ff, transparent: true, opacity: 0.3 })
for (let lat = -80; lat <= 80; lat += 20) {
  const phi = (90 - lat) * (Math.PI / 180)
  const points = []
  const radius = Math.sin(phi)
  const y = Math.cos(phi)
  for (let i = 0; i <= 64; i++) {
    const theta = (i / 64) * Math.PI * 2
    points.push(new THREE.Vector3(radius * Math.cos(theta), y, radius * Math.sin(theta)))
  }
  globe.add(new THREE.Line(new THREE.BufferGeometry().setFromPoints(points), gridMaterial))
}

// Add pins at major cities
const locations = [
  { name: 'New York', lat: 40.7128, lon: -74.0060, color: 0xff3333 },
  { name: 'London', lat: 51.5074, lon: -0.1278, color: 0x33ff33 },
  { name: 'Tokyo', lat: 35.6762, lon: 139.6503, color: 0x3333ff },
  { name: 'Sydney', lat: -33.8688, lon: 151.2093, color: 0xffff33 },
  { name: 'Mumbai', lat: 19.0760, lon: 72.8777, color: 0xff33ff }
]

const pins = []
locations.forEach((loc) => {
  // Convert lat/lon to 3D coordinates (with +180 offset for texture alignment)
  const phi = (90 - loc.lat) * (Math.PI / 180)
  const theta = (loc.lon + 180) * (Math.PI / 180)
  const x = -Math.sin(phi) * Math.cos(theta)
  const z = Math.sin(phi) * Math.sin(theta)
  const y = Math.cos(phi)

  const pin = new THREE.Group()
  pin.userData = {}
  pin.position.set(x, y, z)
  pin.lookAt(x * 2, y * 2, z * 2)

  // Pin stick
  const stick = new THREE.Mesh(
    new THREE.CylinderGeometry(0.01, 0.01, 0.15, 8),
    new THREE.MeshPhongMaterial({ color: loc.color, emissive: loc.color, emissiveIntensity: 0.5 })
  )
  stick.position.z = 0.075
  pin.add(stick)
  pin.userData.stick = stick

  // Pin head
  const head = new THREE.Mesh(
    new THREE.SphereGeometry(0.025, 16, 16),
    new THREE.MeshPhongMaterial({ color: loc.color, emissive: loc.color, emissiveIntensity: 0.8 })
  )
  head.position.z = 0.15
  pin.add(head)
  pin.userData.head = head

  // City label
  const labelDiv = document.createElement('div')
  labelDiv.textContent = loc.name
  labelDiv.style.color = '#' + loc.color.toString(16).padStart(6, '0')
  labelDiv.style.fontSize = '12px'
  labelDiv.style.fontWeight = 'bold'
  labelDiv.style.textShadow = '1px 1px 2px rgba(0,0,0,0.8)'

  const label = new CSS2DObject(labelDiv)
  label.position.z = 0.2
  pin.add(label)
  pin.userData.label = label

  // Add phase for animation
  pin.userData.phase = Math.random() * Math.PI * 2

  globe.add(pin)
  pins.push(pin)
})

// Lighting
scene.add(new THREE.AmbientLight(0xffffff, 0.4))

const light = new THREE.DirectionalLight(0xffffff, 0.8)
light.position.set(5, 3, 5)
scene.add(light)

// Animation loop
const clock = new THREE.Clock()

function animate () {
  requestAnimationFrame(animate)
  controls.update()

  const elapsed = clock.getElapsedTime()
  pins.forEach((pin) => {
    if (pin.userData.head && pin.userData.phase !== undefined) {
      const scale = 1 + Math.sin(elapsed * 2 + pin.userData.phase) * 0.2
      pin.userData.head.scale.setScalar(scale)
    }
  })

  renderer.render(scene, camera)
  labelRenderer.render(scene, camera)
}
animate()