• This repository has been archived on 31/Aug/2021
  • Stars
    star
    164
  • Rank 230,032 (Top 5 %)
  • Language
    JavaScript
  • Created over 8 years ago
  • Updated over 7 years ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

A tutorial on how to build VR interactives using DEM data and Three.js

vr-interactives-three-js

There are a lot of Three.js examples and tutorials out there, but very few examples of what you can do with real-world data.

In this session you'll learn how we used NASA satellite imagery and elevation data to create a 3-D rendering of the Gale Crater on Mars.

Requirements and credits

We'll make use of the WebVR Boilerplate, which also uses the WebVR polyfill to provide VR support when the WebVR spec isn't implemented.

We'll also use a terrain loader, developed by Bjørn Sandvik.

Preamble

Before we start, we'll want to start up a small webserver that can serve our page and assets. Open the terminal on your computers and navigate to the directory we'll be using (on the lab computers. If you're on your own rig, download a copy of this repository) and enter the following at the prompt.

$ cd /data/vr_interactives # or wherever you've downloaded this to
$ python -m SimpleHTTPServer

Note: If you're using Python 3, you may have to run http.server instead.

Now you'll be able to go to http://localhost:8000/three-demo.html and navigate to the page we're going to develop. That page will be blank for now.

Let's get started

Using Three.js can be compared a bit to filmmaking: you have a scene, lighting and a camera. The scene updates a certain number of times per second, otherwise known as "frames per second" (which we'll try to keep as close to 60 as we can, but will drop based on your computer and the complexity of the scene.)

We'll build this in the file three-demo.html. Go ahead and open this file in a text editor.

In the empty script tag on line 33, let's start by declaring some constants. We'll use this to set the size of the renderer on the screen, and the "size" of our world.

// Width and height of the browser window
var WINDOW_WIDTH = window.innerWidth;
var WINDOW_HEIGHT = window.innerHeight;

// Width and height of the surface we're going to create
var WORLD_WIDTH = 2000;
var WORLD_HEIGHT = 1900;

Then, we're ready to create the scene and place the camera:

// Where our lights and cameras will go
var scene = new THREE.Scene();

// Keeps track of time
var clock = new THREE.Clock();

// How we will see the scene
var camera = new THREE.PerspectiveCamera(75, WINDOW_WIDTH / WINDOW_HEIGHT, 1, 5000);

The PerspectiveCamera(fieldOfView, aspect, near, far) functions similar to a standard photo or video camera you might be familiar with. The first parameter, 75 is the vertical field of view in degrees, followed by the aspect ratio (in this case the width and height of the window), along with the minimum and maximum ranges the camera can "see." Anything outside these ranges will not be rendered in the scene.

We also want to position the camera, and tell it where to look. These settings position the camera slightly above the scene, looking at the mound in the center of the crater.

// Position the camera slightly above and in front of the scene
camera.position.set(0, -199, 75);
camera.up = new THREE.Vector3(0,0,1);

// Look at the center of the scene
camera.lookAt(scene.position);

Now let's create the renderer. We're going to be creating a WebGL Renderer, which uses the WebGL API to render our graphics.

// Think of the renderer as the engine that drives the scene
var renderer = new THREE.WebGLRenderer({antialias: true});

// Set the pixel ratio of the screen (for high DPI screens)
// This line is actually really important. Otherwise you'll have lots of weird bugs and a generally bad time.
renderer.setPixelRatio(window.devicePixelRatio);

// Set the background of the scene to a orange/red
renderer.setClearColor(0xffd4a6);

// Set renderer to the size of the window
renderer.setSize(WINDOW_WIDTH, WINDOW_HEIGHT);

// Append the renderer to the DOM
document.body.appendChild( renderer.domElement );

The "clear color" of the renderer is set to a dusty orange/red, and the size is set to the size of the window. As the last step, the renderer is appended to the DOM.

Now, we need to add a couple of helpers that allow VR-style stereo effects to our renderer, along with a manager to allow us to switch between VR and non-VR contexts.

// Apply VR stereo rendering to renderer
var effect = new THREE.VREffect(renderer);
effect.setSize(WINDOW_WIDTH, WINDOW_HEIGHT);

// Create a VR manager helper to enter and exit VR mode
var manager = new WebVRManager(renderer, effect);

If you try to load the scene now, you're just going to see a blank screen. That's because while we're initializing the scene, we're not actually rendering it. To do that, we need to write what's called a render loop.

// Render loop
// This should go at the bottom of the script.
function render() {

    // We'll add more up here later

    // Call the render function again
    requestAnimationFrame( render );

    // Update the scene through the manager.
    manager.render(scene, camera);
}

render();

Now, if you load the scene, you should see an orange/red window. There's nothing there, but we'll get to that in a bit.

Prepping the DEM data

Digital elevation model of Gale Crater. Brighter values are higher elevations. (NASA)

A digital elevation model is a 3D representation of a terrain's surface, and in this case is a greyscale heightmap, where lighter colors represent higher elevations.

The DEM data came as a GeoTIFF file (Gale_HRSC_DEM_50m_overlap.tif). Unfortunately, the TIFF file is huge (30 MB!), and TIFF isn't supported for display by most browsers anyway. We could use the incredibly useful GDAL to convert it to a PNG image, where the height values are reduced to only 256 shades of grey. This would make our terrain blocky, however.

We can, however, convert it to a format called ENVI, which stores our height values as 16-bit unsigned integers, offering 65,535 height values for each pixel in the heightmap.

We're not going to do this today, but for the project we converted the heightmap into a 300x285 ENVI file using the following command. Just remember that we've stored the color values in the heightmap as numbers.

$ gdal_translate -scale 600 1905 0 65535 -outsize 300 285 -ot UInt16 -of ENVI Gale_HRSC_DEM_50m.tif Gale_HRSC_DEM_50m.bin

If you'd like to read more about how to do this, Bjorn Sandvik has an excellent explainer.

You can see the files mentioned above in the 'data' folder.

The fun part: Creating the planet

Now, we get to create the planet surface from the DEM data. To do this, we set the URL of the data to load and initialize the loader. Let's also initialize a value for the surface.

// URL to our DEM resource
var terrainURL = "data/Gale_HRSC_DEM_50m_300x285.bin";

// Utility to load the DEM data
var terrainLoader = new THREE.TerrainLoader();

// We'll need this later
var surface;

// Create the plane geometry
var geometry = new THREE.PlaneGeometry(WORLD_WIDTH, WORLD_HEIGHT, 299, 284);

In the line above, we also create a PlaneGeometry(width, height, widthSegments, heightSegments). You'll see four arguments - these are the width and height of the "world" that we defined above, and the number of vertices the plane will have. We set these two values to 299 and 284 - the same dimensions as the DEM data, except that it's zero-indexed.

By the way, you won't see anything on the screen until we start actually rendering the scene. The screenshots in this tutorial are how it would look if it was rendering -- which we'll get to soon.

Then we'll do something crazy. Remember how the height data in the DEM file was stored as a series of numbers? We can use the TerrainLoader to iterate over those values, and adjust each corresponding vertex in the plane. Thus we morph a flat plane to take the shape of the terrain in the data. Because at the scale of the scene we're making, the final shape would be pretty boring at its natural values, we exaggerate the height, settling in at a factor that feels comfortable.

// The terrainLoader loads the DEM file and defines a function to be called when the file is successfully downloaded.
terrainLoader.load(terrainURL, function(data){

    // Adjust each vertex in the plane to correspond to the height value in the DEM file.
    for (var i = 0, l = geometry.vertices.length; i < l; i++) {
        geometry.vertices[i].z = data[i] / 65535 * 100;
    }

    // Leave this space blank for now we'll come back to it in the next section.

});

So now we have a geometry for our surface, but we can't actually load this into the scene yet. Objects in Three.js, called "meshes", require both a geometry and a material. That is, you need to define a shape, and you need to define what that shape is made out of. Yes, it's a box, but what kind of box, is it? Cardboard? Metal? Is it painted?

Let's do this next.

Inside of the callback function, the line I said we'd be coming back to, we want to define a texture loader and set the URL, much in the way that we loaded the DEM data.

    var textureLoader = new THREE.TextureLoader();
    var textureURL = "data/Gale_texture_high_4096px.jpg";

Then we load the texture similar to how we did the DEM file. This time, we define and set the material in the callback function, using the texture we loaded as a texture map.

    // This goes inside the TerrainLoader callback function
    textureLoader.load(textureURL, function(texture) {
        // Lambert is a type non-reflective material
        var material = new THREE.MeshLambertMaterial({
            map: texture
        });

        // Leave this space blank, we're going to continue here in the next section

    });

That's fine and all, but what the heck is a texturemap? Why are we doing all this?

Remember the geometry is the shape of an object, and the material is what that object is made out of. Think of a texture map as a paint job, or "skin" on an object. It's basically an image mapped onto an object's surface.

You might notice we have both our geometry and material now, so it's time to add them to the scene, inside of the textureLoader callback (so, where the "Leave this space blank" comment is).

        // This goes in the TextureLoader callback
        // Create the surface mesh and add it to the scene
        surface = new THREE.Mesh(geometry, material);
        scene.add(surface);

It's important to place these in the callbacks, otherwise you might be trying to load a texture onto a shape that hasn't loaded yet, or vice versa.

Now, let's take a look at the scene. There will be a wait while the resources load and process, but eventually you'll see this lovely scene.

Our scene

What happened? We forgot to turn on the lights! Let's do that.

// Lights!
var dirLight = new THREE.DirectionalLight( 0xffffff, 0.75);
dirLight.position.set( -1, 1, 1).normalize();

var ambiLight = new THREE.AmbientLight(0x999999);

// Add the lights to the scene
scene.add(ambiLight);
scene.add(dirLight);

That's better, right?

Just like a film scene, Three.js scenes need lighting. You can think of a DirectionalLight(hexColor, intensity) as a spotlight that you can specify the direction of and where it's pointing, while AmbientLight(hexColor) is more like the sun - it lights all objects in the scene, regardless of where they're positioned.

Standing still in a scene isn't very fun, we can't even look around. To interact with a scene, we'll need to add controls. Controls basically move the camera around the scene according to user inputs. The different parameters we use below are specific to the FlyControls we'll be using. Make sure to put this code before the render() loop:

// WASD-style movement controls
var controls = new THREE.FlyControls(camera);

// Disable automatic forward movement
controls.autoForward = false;

// Click and drag to look around with the mouse
controls.dragToLook = true;

// Movement and roll speeds, adjust these and see what happens!
controls.movementSpeed = 20;
controls.rollSpeed = Math.PI / 12;

Reload the page and... nothing happened! To actually move around in the scene, we also need to add and update the controls in the renderer loop. Find the render() function from earlier and replace the code inside it like so:

    // Render loop
    function render() {

        // Get the difference from when the clock was last updated and update the controls based on that value.
        var delta = clock.getDelta();
        controls.update(delta);

        // Call the render function again
        requestAnimationFrame( render );

        // Update the scene through the manager.
        manager.render(scene, camera);
    }

Now when you reload the scene you can look around by clicking and dragging with the mouse, and use the WASD keys to move around the crater.

That's great and all but what if we want to try this on mobile? Instead, let's load controls conditionally, depending on the device we're on. This will load the VRControls if you load the page on your phone, which use the accelerometer to track movement.

// Detect mobile devices in the user agent
var is_mobile= /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);

// Conditionally load VR or Fly controls, based on whether we're on a mobile device
if (is_mobile) {
    var controls = new THREE.VRControls(camera);
} else {
    // WASD-style movement controls
    var controls = new THREE.FlyControls(camera);

    // Disable automatic forward movement
    controls.autoForward = false;

    // Click and drag to look around with the mouse
    controls.dragToLook = true;

    // Movement and roll speeds, adjust these and see what happens!
    controls.movementSpeed = 20;
    controls.rollSpeed = Math.PI / 12;
}

Now, if you visit this page on a mobile phone, you'll be able to look around the scene just by moving your device.

VR Mode

One great thing about the VR manager is it provides an interface to allow users to easily switch in and out of VR mode, so that you can use a cardboard or other VR device. This ability is automatically loaded based on the device that you're viewing the page on, so it'll show up on phones, but not your desktops. We can override this though.

At the very top of the page you'll see a line (line 20, in fact.) that says FORCE_ENABLE_VR: false. Change this option to true.

Now, if you reload the page, you'll see a small cardboard icon in the lower right-hand side of the page. Click this, and you'll see the page distort as if you were viewing the page through a cardboard.

Voila!

Other Things you may want to do

  • Load in different surface detail and texture sizes based on the device.
  • Add in collision detection
  • Call out specific points in the crater
  • Progressively load terrain and texture

So what can you do with this?

A couple people have taken the techniques outlined here and integrated them into story pages

More Repositories

1

notebooks

All of our computational notebooks
Python
516
star
2

california-coronavirus-data

The Los Angeles Times' open-source archive of California coronavirus data
Jupyter Notebook
214
star
3

census-data-downloader

Download U.S. census data and reformat it for humans
Python
191
star
4

python-elections

A Python wrapper for the Associated Press' U.S. election data service.
Python
175
star
5

django-for-data-analysis-nicar-2016

So you've gone through a Django tutorial or two, maybe even built an app, and want to know how to use it for your reporting.
Python
104
star
6

web-map-maker

Use Natural Earth and OpenStreetMap data to export an image or a vector file.
JavaScript
94
star
7

california-2016-election-precinct-maps

Precinct-level results for the 2016 general election in California
Python
83
star
8

latimes-calculate

Some simple math we use to do journalism.
Python
80
star
9

latimes-table-stacker

Publish spreadsheets as interactive tables. And do it on deadline.
Python
75
star
10

jquery-geocodify

Autocomplete for address searches
JavaScript
66
star
11

kobe-every-shot-ever

A Los Angeles Times analysis of Every shot in Kobe Bryant's NBA career
Jupyter Notebook
65
star
12

python-documentcloud

A deprecated Python wrapper for the DocumentCloud API
Python
63
star
13

tutorials

All of our code examples and tutorials
Python
63
star
14

lapd-crime-classification-analysis

A Los Angeles Times analysis of serious assaults misclassified by LAPD
Jupyter Notebook
60
star
15

print-map-maker

Tool for generating map images from Mapbox tiles
HTML
58
star
16

homeless-arrests-analysis

A Los Angeles Times analysis of arrests of the homeless by the LAPD
Jupyter Notebook
55
star
17

california-coronavirus-scrapers

The open-source web scrapers that feed the Los Angeles Times California coronavirus tracker.
Jupyter Notebook
51
star
18

california-crop-production-wages-analysis

A Los Angeles Times analysis of crop worker pay in California
Jupyter Notebook
42
star
19

census-data-aggregator

Combine U.S. census data responsibly
Python
42
star
20

osm-quiet-la

A template for a muted base layer about Southern California.
Python
39
star
21

django-shp2svg

Convert a shapefile into an SVG you can use with JavaScript libraries
HTML
38
star
22

django-softhyphen

A Python library for hyphenating HTML in your Django project
Python
38
star
23

census-map-downloader

Easily download U.S. census maps
Python
33
star
24

latimes-appengine-template

Bootstrap a Google App Engine project with Django and other goodies.
Python
33
star
25

nasa-wildfires

Download wildfire hotspots detected by NASA satellites and the Fire Information for Resource Management System (FIRMS)
Python
32
star
26

overpass-turbo-tutorial

Code examples and tutorial on getting data out of OpenStreetMap
32
star
27

census-map-consolidator

Combine Census blocks into new shapes
Python
31
star
28

slackdown

A simple Slack message text formatting to HTML code converter.
Python
27
star
29

latimes-statestyle

A Python library that standardizes the names of U.S. states
Python
25
star
30

timemap

A branch of the original, with a touch of Leaflet
JavaScript
25
star
31

django-bigbuild

The open-source engine that powers bigbuilder, the Los Angeles Times Data Desk's system for publishing standalone pages
JavaScript
25
star
32

baker

A build tool by and for the Los Angeles Times
JavaScript
22
star
33

geopandas-spatial-join-example

An example of how to join point to polygon data with geopandas and Python
Jupyter Notebook
21
star
34

street-racing-analysis

A Los Angeles Times analysis of street racing fatalities in L.A. County
Jupyter Notebook
21
star
35

california-electricity-capacity-analysis

A Los Angeles Times analysis of California's costly power glut
Jupyter Notebook
17
star
36

latimes-mappingla-geopy

A fork of the geocoding library geopy. Returns accuracy scores. Allows viewport biasing.
Python
17
star
37

baker-example-page-template

A demonstration of how to build and publish pages with the baker build tool
SCSS
17
star
38

census-error-analyzer

Analyze the margin of error in U.S. census data
Python
16
star
39

california-topojson-atlas

Simple maps of California's 58 counties
HTML
15
star
40

california-business-entities

Corporations and limited-liability companies registered with the California Secretary of State.
Shell
14
star
41

los-angeles-police-killings-data

The Los Angeles Times' database of people killed by local police in Los Angeles County.
14
star
42

california-fire-zone-analysis

A Los Angeles Times analysis of California buildings within fire hazard zones
Jupyter Notebook
14
star
43

altair-area-examples

How to use the Altair data visualization library to create an array of area charts.
Jupyter Notebook
14
star
44

latimes-mappingla-api

A Python wrapper for accessing the Mapping L.A. Boundaries API.
Python
13
star
45

copyboy

Copyboy is our fork of GitHub's Campfire bot, hubot. He is aware of all Internet traditions.
CoffeeScript
13
star
46

star-wars-analysis

A Los Angeles Times analysis of the dialogue spoken in Star Wars episodes 1-8
12
star
47

django-project-template

A custom template for initializing a new Django project the Data Desk way.
Python
12
star
48

latimes-pluggablemaps-uscounties

A pluggable GeoDjango app with the boundaries of United States counties.
Python
12
star
49

nifc-wildfires

Download wildfires data from the National Interagency Fire Center
Python
12
star
50

california-ccscore-analysis

A Los Angeles Times analysis of water usage after the state eased drought restrictions
Jupyter Notebook
12
star
51

inciweb-wildfires

Download wildfire incidents data from InciWeb
Python
12
star
52

noaa-wildfires

Download wildfires data from NOAA satellites
Python
11
star
53

altair-latimes

A Los Angeles Times theme for Python's Altair statistical visualization library
Jupyter Notebook
11
star
54

ripa-analysis

A Los Angeles Times analysis found that LAPD officers search blacks and Latinos far more often than whites during traffic stops even though whites are more likely to be found with illegal items.
HTML
11
star
55

nws-wwa

Download watch, warning and advisory data from the National Weather Service
Python
11
star
56

lametro-maps

Geospatial data from L.A. Metro's public transportation system
10
star
57

slack-buttons-example

A Flask app to document and test Slack's interactive messages.
Python
10
star
58

deadspin-scraper

Scrape posts from Deadspin
Jupyter Notebook
10
star
59

homeless-sleeping-restrictions

Analyzing a Los Angeles city council proposal aiming to limit where homeless people can sleep
Jupyter Notebook
10
star
60

california-h2a-visas-analysis

The Los Angeles Times analysis of temporary visas granted to foreign agricultural workers
Jupyter Notebook
10
star
61

latimes-document-stacker

Use DocumentCloud to publish PDFs for humans.
Python
10
star
62

helicopter-accident-analysis

A Los Angeles Times analysis of helicopter accident rates
Jupyter Notebook
10
star
63

jquery-scroll-slideshow

jQuery Waypoints extension that rotates through an array of images as you scroll down the page
JavaScript
9
star
64

osm-silent-la

A template for a black base layer about Southern California
CartoCSS
9
star
65

congress-headshots

Almost all active congress members' photos
Python
9
star
66

calfire-wildfires

Download wildfires data from CalFire
Python
8
star
67

wine-country-fires

Analysis of every fire in California's wine country since 1950
Jupyter Notebook
7
star
68

la-election-night

Parses election results data published online by the Los Angeles County Registrar-Recorder/County Clerk
Python
7
star
69

houston-flood-zone-analysis

A Los Angeles Times geospatial analysis of Houston homes after Hurricane Harvey
Jupyter Notebook
7
star
70

lat-soundsystem

The voice of the Los Angeles Times Data Desk
HTML
6
star
71

la-county-trail-maps

Geospatial data of trails managed or planned by the Los Angeles County Department of Parks and Recreation
6
star
72

construction-jobs-analysis

A Los Angeles Times analysis of the demographics and pay of construction workers
HTML
6
star
73

jquery-shareify

Automagically shareificates divs to Facebook and Twitter buttons.
JavaScript
6
star
74

latimes-pluggablemaps-usstates

A pluggable GeoDjango app with the boundaries of all states in the United States of America. Geography, loosely coupled.
Python
6
star
75

geopandas-intersection-area-example

How to use geopandas' overlay method to find the area of intersections between two datasets.
Jupyter Notebook
6
star
76

altair-election-maps-example

An experiment in creating precinct-level election results maps using Python's Altair library
Jupyter Notebook
6
star
77

checkbook-la-watchdog

A periodically updated archive of financial data published by the city of Los Angeles' Checkbook LA data portal
Python
6
star
78

extreme-heat-excess-deaths-analysis

A statistical analysis of excess deaths attributable to extreme heat in California's most populous counties
Jupyter Notebook
6
star
79

groundwater-analysis

A Los Angeles Times analysis of well completion, groundwater levels, and water shortages in the San Joaquin Valley
Jupyter Notebook
5
star
80

tilemill-staticmaps

Generate static maps from an existing TileMill project.
Python
5
star
81

python-lametro-api

A simple Python wrapper for the L.A. Metro’s API for bus stops, routes and vehicles.
JavaScript
5
star
82

python-road-clipping-example

An example of how to use Python's geospatial tools to clip a shapefile of roads into segments using a polygon layer.
Jupyter Notebook
5
star
83

la-vacant-building-complaints-analysis

A Los Angeles Times analysis of vacant building complaints filed with L.A. city
Jupyter Notebook
5
star
84

highschool-homicide-analysis

The Los Angeles Times analysis of homicides near public high schools.
Jupyter Notebook
5
star
85

la-weedmaps-analysis

An analysis conducted for the May 29, 2019, Los Angeles Times story "Black market cannabis shops thrive in L.A. even as city cracks down."
Jupyter Notebook
5
star
86

la-settlements-analysis

A Los Angeles Times analysis of legal payouts by L.A. city
Jupyter Notebook
5
star
87

kamala-harris-schedules

An open-source archive of Vice President Kamala Harris' public calendar
4
star
88

pandas-squarify-example

How to use the squarify extension to matplotlib to visualize a pandas DataFrame as a treemap.
Jupyter Notebook
4
star
89

air-quality-index

Download air quality index data from AirNow
Python
4
star
90

latimes-qiklog

A simplified wrapper for Python's logging module
Python
4
star
91

lausd-school-campus-polygons

The areas of school campuses at the Los Angeles Unified School District
4
star
92

ferc-enforcement-analysis

A Los Angeles Times analysis of civil penalties issued by FERC
Jupyter Notebook
4
star
93

responsive-fullscreen-header-example

An example of a header image that changes based on the device's orientation and always fits to fill the full screen
HTML
4
star
94

nyrb-covers-analysis

An analysis of the colors and content of NYRB Classics book covers
HTML
4
star
95

california-k12-notebooks

Scripts to download and process California K12 data
Jupyter Notebook
4
star
96

python-censusbatchgeocoder-examples

Examples of how to use the python-censusbatchgeocoder library
Jupyter Notebook
3
star
97

california-kindergarten-vaccination-analysis

A Los Angeles Times analysis of California kindergartens with the lowest vaccination rates
Jupyter Notebook
3
star
98

mapping-la-data

Data files from the Los Angeles Times' Mapping L.A. project
3
star
99

native-american-census-analysis

An analysis conducted for the June 13, 2019, Los Angeles Times story "The 2020 census is coming. Will Native Americans be counted?"
Jupyter Notebook
3
star
100

osm-la-streets

A street-centric base layer for overlaying point data about Southern California
Python
3
star