
Introduction
Visualizing Digital Elevation Models (DEMs) in 3D is an engaging way to explore terrain data. In this tutorial, we’ll create an interactive 3D terrain map using Three.js and a GeoTIFF file. GeoTIFF is a popular format for storing geospatial raster data, such as elevation.
By the end of this tutorial, you’ll know how to:
- Load GeoTIFF files.
- Parse elevation data.
- Render 3D terrain using Three.js.
- Apply grayscale or color shading for terrain representation.
Access Complete Source Code and Data here.
Demo
Step 1: Setting Up the Project
1.1. Prerequisites
Ensure you have the following installed:
- Node.js for running the server.
- A basic understanding of JavaScript and Three.js.
- You can follow below link for setting up basic threejs project.
https://threejs.org/docs/#manual/en/introduction/Installation
1.2. Project Structure
Create a folder with the following structure:
|
1 2 3 4 5 6 7 |
project-folder/ │ ├── index.html # Entry point for the browser ├── main.js # Three.js logic ├── study_area.tif # GeoTIFF file with elevation data ├── package.json # Dependencies and scripts |
1.3. Install Dependencies
Initialize a Node.js project and install required libraries:
|
1 2 3 4 5 6 7 8 9 |
# three.js npm install --save three # vite npm install --save-dev vite # geotiff npm install geotiff |
Step 2: Writing HTML
Create an index.html file to serve as the project entry point. It will host the terrain container.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
<!DOCTYPE html> <html> <head> <title>3D Terrain Map</title> <style> body { margin: 0; overflow: hidden; } #terrain-container { width: 100vw; height: 100vh; } </style> </head> <body> <div id="terrain-container"></div> <script type="module" src="./main.js"></script> </body> </html> |
Step 3: Loading and Parsing GeoTIFF
In the main.js file, use the GeoTIFF.js library to load and extract raster data.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
import * as THREE from 'three'; import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; import * as GeoTIFF from 'geotiff'; import Stats from 'three/addons/libs/stats.module.js'; async function loadGeoTIFF(file) { const response = await fetch(file); const arrayBuffer = await response.arrayBuffer(); const tiff = await GeoTIFF.fromArrayBuffer(arrayBuffer); const image = await tiff.getImage(); const rasters = await image.readRasters(); const width = image.getWidth(); const height = image.getHeight(); const elevationData = rasters[0]; // Assuming single-band DEM return { data: elevationData, width, height }; } |
Step 4: Generating the 3D Terrain
We’ll use a PlaneGeometry mesh for the terrain and manipulate its vertex positions to reflect elevation values.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
import * as THREE from 'three'; async function initTerrain(terrainData) { // Clear previous scene if (scene) { scene.remove(mesh); } // Create geometry const geometry = new THREE.PlaneGeometry( 7500, 7500, terrainData.width - 1, terrainData.height - 1 ); geometry.rotateX(-Math.PI / 2); // Modify vertex heights const vertices = geometry.attributes.position.array; for (let i = 0, j = 0, l = vertices.length; i < l; i++, j += 3) { vertices[j + 1] = terrainData.data[i] || 0; } // Generate texture texture = new THREE.CanvasTexture(generateTexture(terrainData.data, terrainData.width, terrainData.height)); texture.wrapS = THREE.ClampToEdgeWrapping; texture.wrapT = THREE.ClampToEdgeWrapping; texture.colorSpace = THREE.SRGBColorSpace; // Create mesh mesh = new THREE.Mesh( geometry, new THREE.MeshBasicMaterial({ map: texture }) ); scene.add(mesh); // Adjust camera controls.target.y = terrainData.data[Math.floor(terrainData.data.length / 2)] || 0; camera.position.y = controls.target.y + 2000; camera.position.x = 2000; controls.update(); } |
Step 5: Adding Colors to the Terrain
You can generate a colored texture or keep it grayscale. In this example, we create a grayscale representation.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
function generateTexture(data, width, height) { const canvas = document.createElement('canvas'); canvas.width = width; canvas.height = height; const context = canvas.getContext('2d'); const image = context.createImageData(width, height); const imageData = image.data; for (let i = 0, j = 0, l = imageData.length; i < l; i += 4, j++) { /* For Grayscale */ const normalized = (data[j] - minElevation) / (maxElevation - minElevation); // Define a gradient from blue (low) to green (mid) to red (high) const r = Math.min(255, Math.max(0, Math.round(255 * normalized))); const g = Math.min(255, Math.max(0, Math.round(255 * normalized))); const b = Math.min(255, Math.max(0, Math.round(255 * normalized))); imageData[i] = r; // R imageData[i + 1] = g; // G imageData[i + 2] = b; // B imageData[i + 3] = 255; } context.putImageData(image, 0, 0); return canvas; } |
Step 6: Setting Up Three.js
Initialize the scene, camera, renderer, and controls.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 |
async function init() { container = document.getElementById('terrain-container'); container.innerHTML = ''; // Renderer renderer = new THREE.WebGLRenderer({ antialias: true }); renderer.setPixelRatio(window.devicePixelRatio); renderer.setSize(window.innerWidth, window.innerHeight); renderer.setAnimationLoop(animate); container.appendChild(renderer.domElement); // Scene scene = new THREE.Scene(); scene.background = new THREE.Color(0xbfd1e5); // Camera camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 10, 20000); // Controls controls = new OrbitControls(camera, renderer.domElement); controls.minDistance = 1000; controls.maxDistance = 10000; controls.maxPolarAngle = Math.PI / 2; // Raycaster for interaction raycaster = new THREE.Raycaster(); pointer = new THREE.Vector2(); // Helper const geometryHelper = new THREE.ConeGeometry(20, 100, 3); geometryHelper.translate(0, 50, 0); geometryHelper.rotateX(Math.PI / 2); helper = new THREE.Mesh(geometryHelper, new THREE.MeshNormalMaterial()); scene.add(helper); // Event listeners container.addEventListener('pointermove', onPointerMove); window.addEventListener('resize', onWindowResize); // Stats stats = new Stats(); container.appendChild(stats.dom); try { const terrainData = await loadGeoTIFF('study_area.tif'); // const terrainData = await loadGeoTIFF('cdnh43e.tif'); await initTerrain(terrainData); } catch (error) { console.error('Error loading GeoTIFF:', error); } } function onWindowResize() { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); } function animate() { render(); stats.update(); } function render() { renderer.render(scene, camera); } function onPointerMove(event) { pointer.x = (event.clientX / renderer.domElement.clientWidth) * 2 - 1; pointer.y = -(event.clientY / renderer.domElement.clientHeight) * 2 + 1; raycaster.setFromCamera(pointer, camera); // See if the ray from the camera into the world hits our mesh const intersects = raycaster.intersectObject(mesh); // Update helper position if (intersects.length > 0) { helper.position.set(0, 0, 0); helper.lookAt(intersects[0].face.normal); helper.position.copy(intersects[0].point); } } // Initialize the scene init(); |
Step 7: Viewing the Terrain
Start a local server using Node.js and open index.html in your browser to view the interactive 3D terrain.
|
1 2 3 |
# Using Node.js npx vite |
Open http://localhost:5173 in your browser.
Conclusion
This tutorial showcased how to render a 3D terrain from GeoTIFF data using Three.js. You can further enhance this by adding:
- More advanced lighting and material effects.
- Interactive features like point picking.
- Displaying additional data layers.
Feel free to share your terrain visualizations or suggest improvements!
I hope this tutorial will create a good foundation for you. If you want tutorials on another GIS topic or you have any queries, please send an mail at contact@spatial-dev.guru.
