Every time a user clones a new environment, a link to that environment will appear on their profile.
In order to make the links more visually appealing, we wanted to add an image along with the link. We could have used the Static Maps API satellite images however that wouldn’t be an accurate representation of what the user would expect to see once they had clicked through. A screenshot of the actual rendered 3D environment is what we needed to display, and luckily canvas makes it relatively easy to generate these captures.
I slotted the ‘capture’ process in between generating the height maps and redirecting to the environment page.
Hiding underneath the same fullscreen preloader as on the Clone A New Environment page, another ThreeJS environment is loaded into the page with a slightly different camera and controls setup to ensure the environment is in the correct position for the capture.
We can use ThreeJS’ WebGLRenderer’s build in capture function to encode the displayed content into a PNG base64 string:
This string is then passed into a PHP script to convert and save that string as an image to the server.
The PHP also converts the PNG to a JPEG and compresses it. The user’s profile page loads in 12 of these images on each page (see post on Pagination) so it is important to keep the file sizes low.
By combining Google Elevation API, Google Static Maps API and ThreeJS, we can automatically generate and display 3D topographical representations of real-world areas using only code.
The following lists out the step-by-step process we went through in order to produce the outcome.
Getting the elevation data:
- Split the world into tiles
- Work out which tile the user has selected
- Get the latlng (latitude and longitude) boundaries of that tile
- Divide the tile into another grid (30×30)
- Loop through the grid, row by row, and get the latlng of each of those divisions
- Request the elevation of each latlng from Google Elevation API and store in an array
Generating the height map:
A height map is a grey scale image where white means high and black means low.
In RGB, white = 255 and black = 0.
- Take the elevation array and find the maximum and minimum value
- Normalise all the data so that the minimum value = 0
- Calculate the ratio of all the values so that they lie between 0 and 255
- Convert the array into a string, with each column delimited by a comma and each new row signified by a dash, and pass the string into a PHP script
- The PHP parses and loops through the string to draw out the grey scale image pixel by pixel, which is then saved to disk
Getting the satellite image:
- From the selected tile boundaries, calculate the centre latlng of that tile
- Request the satellite image using Google Static Maps (proxying through a PHP script to bypass the Cross-Origin Policy)
Rendering the model:
- ThreeJS is used to generate a mesh and downloads the height map and the satellite image
- The height map is applied as a displacement map
- The satellite image is applied as a texture
Considering the code required to simply load a 3D OBJ file into the browser amounts to a whopping 450KB, we are having to be very careful about the rest of the data we are loading into the page. Specifically, the size of the 3D models and how that affects the performance of WebGL.
I start off with this 100 poly “mountain scene”.
The OBJ file for this model is 14KB. A pretty decent size for the web but not a very smooth looking model. The WebGL performance is of course pretty good at 63fps.
I then decided to push the upper limits, turn on the Turbosmooth modifier and crank up the iterations to 6. This produced me a baby smooth 820,000 poly model.
The exported OBJ is 64MB! Not something we imagine having to download on a wifi connection, let alone a limited 3G one. It took ~9 seconds on my sandbox environment!
Considering the massive file size increase, I was pleasantly surprised to see that the WebGL performance only dropped by 3fps to 60.
Dropping the Turbosmooth iterations down 2 produces a relatively smooth model (undistinguishable without a comparison) with 3,200 polys at 218KB. Still quite a large file but not quite as much as 64MB.
The fps also has increased back up to 63fps.
Some things we may need to consider is providing visual feedback in the form of a spinner or preloader while the OBJ is loading and potentially showing a warning message declaring that this page is going to be bandwidth heavy.