Interactive 3D Globe in Three.js

Interactive Three.js 3D globe visualization with geospatial location pins, latitude/longitude grid overlays, smooth auto-rotation, and configurable styling for geographic data storytelling and map-based dashboards.

Key topics: three.js globe, 3d globe visualization, interactive world map, geographic data visualization, three.js earth, 3d map pins, location markers, latitude longitude grid, rotating globe, world map three.js, data visualization globe, animated globe

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.

Continue Learning

JavaScript Implementation

ES6+
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()