library(rayshader) # For 3D visualizations
library(tidyverse) # For data manipulation and visualization
library(viridis) # For color scales
library(terra) # For spatial data manipulation
library(sf) # For working with simple features
library(av) # For handling audio and video files
# devtools::install_github("mccarthy-m-g/embedr")
library(embedr) # For embedding content
library(rgl) # For interactive 3D visualizations
Lesson 8: 3D mapping
Learning outcomes
In this tutorial, we will learn how to create 3D maps in R using the rayshader package. We will use high resolution elevation and Orthophoto datasets specific to the Rummu Quarry area. The tutorial materials have been adapted from the works of Anto Aasa and the code has been adjusted using resources available on the official rayshader site.
At the end of this lesson, the students should be able to:
- Comprehend the features and functionalities of the rayshader package.
- Generate 3D visualizations for spatial data utilizing the rayshader package.
- Transform ggplot visualizations into 3D representations.
Let’s start with installing and loading the packages required for the lesson:
8.1. Introduction
3D mapping is the process of creating three-dimensional representations of objects, surfaces, landscapes, or environments in the digital or physical world. It involves capturing and modeling spatial data to depict objects and terrain in three dimensions, allowing for a more accurate and realistic portrayal compared to traditional 2D mapping methods.It allows for in-depth data analysis and effective communication of complex information, with options for interactivity and customization to enhance the visual appeal and clarity of the plots.
To generate 3D maps in R, various libraries and packages can be used. In this tutorial, we will use the rayshader
package, an open-source tool designed for creating both 2D and 3D data visualizations in R. The rayshader
uses elevation data in a base R matrix and a combination of raytracing, hillshading algorithms, and overlays to generate stunning 2D and 3D maps. In addition to maps, rayshader also allows the user to translate ggplot2
objects into beautiful 3D data visualizations.
The rayshader package encompasses various essential functions for generating customized 3D visualizations, including:
raster_to_matrix()
: converts a raster objects into a matrix.sphere_shade()
: maps an RGB texture to a hillshade by spherical mapping.render_highquality()
: combines the ability to render a scene with camera positioning, title adding and other scene management controls.plot_map()
: accepts either a matrix or an array and plots the current map.plot_3d()
creates a 3D map, given a texture and an elevation matrix.render_snapshot()
: plots the 3D view to the current device or saves an image of the current 3D view to disk (if given a filename).plot_gg()
: takes a ggplot2 object and uses the fill or color aesthetic to transform the plot into a 3D surface.render_movie()
: creates and saves a mp4/gif file of the camera rotating around the 3D scene.
8.2. 3D mapping with rayshader
In this tutorial, we’ll illustrate 3D visualization using a 1-meter DEM and a 20-centimeter aerial image of Rummu Quarry (coordinates 59.227398, 24.193968). This quarry, formed from the mining of Vasalemma limestone and marble, is a notable attraction detailed here. Its popularity peaks during the hot season, offering a unique experience of swimming and diving in its pristine, transparent waters. The code snippets employed in this tutorial have been adapted from the examples available at https://www.rayshader.com/.
Let’s download the DEM data from Landboard Estonia. Choose the 1-meter resolution GeoTIFF data and use map sheet number 63613. Once the download is complete, make sure to move the file to your working directory.
Import the downloaded DEM data:
#Import data
<- rast( "63613_DTM.tif")
dem
#Plot
plot(dem, col = terrain.colors(12))
The downloaded data covers a big area while we need a small part of it. Rummu quarry is the small area in the bottom left corner of the figure. Let’s create a bounding box from minimum and maximum x and y coordinates to clip DEM to the area of interest (Rummu Quarry):
#Define the bounding box coordinates
<- c(510800, 511300)
lon <- c(6565300, 6565750)
lat
#Create a bounding box using st_bbox
<- data.frame(lon, lat) %>%
bbox st_as_sf(coords = c("lon", "lat"), crs = 3301) %>%
st_bbox() %>%
st_as_sfc()
#Plot the bounding box on top of the DEM
plot(dem, col = terrain.colors(12))
plot(bbox, border = "red", lwd = 2, add = T)
Let’s extract a portion of the DEM based on the bounding box’s extent.
#Crop
<- terra::crop(dem, bbox)
dem_crop
#Plot
plot(dem_crop, col = terrain.colors(12))
The rayshader package requires elevation data to be in a matrix
form for creating 3D terrain maps. So, you need to convert your DEM data from a raster object to a matrix format.
#Convert raster to matrix
<- raster_to_matrix(dem_crop) dem_matrix
Let’s try the first 3D visualization:
%>%
dem_matrix sphere_shade(texture = "desert") %>% # applies shading effects to the DEM
plot_map()
#Try with different textures (`imhof1`,`imhof2`,`imhof3`,`imhof4`,`desert`, `bw`)
We can customize the appearance of a 3D visualization by adjusting some parameters. Here’s an example of how you can change the sunangle and texture parameters. For more information and customization choices, visit this link:
#Adjust sunangle and texture
%>%
dem_matrix sphere_shade(sunangle = 200, texture = "unicorn") %>%
plot_map()
We can also create our own texture
and increase the colorintensity
(default is 1).
#Apply custom texture and increase color intensity
%>%
dem_matrix sphere_shade(
texture = create_texture("#FFA900", "#FF7600", "#A2CDCD", "#CD113B", "#000000"),
colorintensity = 6) %>%
plot_map()
Now, let’s proceed with the 3D visualization using the plot_3d
function from rayshader package.
#Use sphere_shade with a custom texture and sun angle
%>%
dem_matrix sphere_shade(texture = "unicorn", sunangle = 10) %>%
plot_3d(dem_matrix,
baseshape = "hex", # Shape of the base (options: "rectangle", "circle", "hex")
solidcolor = 'black', # Base color
solidlinecolor = '#191A19', # Base edge line color
background = '#1F1D36', # Color of the background
zscale = 0.4, # Ratio between the x and y spacing
fov = 0, # Field-of-view angle
theta = 60, # Rotation around the z-axis
zoom = 0.7, # Zoom factor
phi = 45, # Azimuth angle
windowsize = c(500, 500)) # Size of the plotting window
A 3D visualization will appear in a new window
, allowing you to customize the perspective of the DEM. Find the optimal viewpoint, and then run the following script. Make sure to keep the newly opened window active during the execution of this code.
Sys.sleep(0.2) # Pause for 0.2 seconds
render_snapshot() # Capture a snapshot of the 3D visualization
Once we are happy with our shot, we might render it to a high quality image by using the render_highquality() function. When running render_highquality()
, there’s no need to pre-compute the shadows with any of the _shade()
functions, so we remove those. After rendering, the close3d()
function from rgl
package is used to close the window displaying the image. Please be patient as it may take a few minutes to complete.
Sys.sleep(0.2)
render_highquality(
lightdirection = 100, # Direction of the light source
lightaltitude = 45, # Altitude angle of the light source
lightintensity = 400, # Intensity of the light source
clamp_value = 10, # Clamping value for rendering
title_text = "Rummu quarry",# Text for the rendering title
title_color = "white", # Color of the rendering title text
title_bar_color = "black", # Color of the rendering title bar
parallel = TRUE, # Flag for parallel rendering
width = 1000, # Width of the rendering
height = 1000, # Height of the rendering
samples = 1000) # Number of rendering samples
# Close the window when you're done
::clear3d() rgl
To export high quality resolution images using Rayshader, you need to add a parameter stating the file path and name of the output image in render_snapshot().
#Export a map
render_snapshot("Rummu.png", clear = TRUE)
The Rummu heap is located nearby the water body. We can represent this water feature, assuming that any terrain below 23 meters in elevation is submerged or flooded.
%>%
dem_matrix sphere_shade(texture = "imhof1", sunangle = 100) %>%
plot_3d(
dem_matrix,baseshape = "circle", # Shape of the base
solidcolor = 'grey20', # Base color
solidlinecolor = 'grey20',# Base edge line color
zscale = 0.4, # Ratio between x and y spacing
fov = 0, # Field-of-view angle
theta = 60, # Rotation around the z-axis
zoom = 0.7, # Zoom factor
phi = 45, # Azimuth angle
windowsize = c(500, 500), # Window size
water = TRUE, # Display water surface
waterdepth = 23, # Elevation below which terrain is considered flooded
wateralpha = 0.5, # Transparency of the water surface
watercolor = "#233aa1", # Color of the water
waterlinecolor = "white", # Color of the waterline
waterlinealpha = 0.5) # Transparency of the waterline
Sys.sleep(0.2)
render_snapshot()
To enhance our 3D visualization and its information content, let’s incorporate details from a 20-cm aerial image. Download the aerial image from Landboard Estonia using this link. Ensure that you select the image corresponding to the same sheet number that you used for downloading the DEM (63613) and select 63613_OF_RGB_GeoTIFF_2022_06_15.zip
.
Import the downloaded aerial image and crop the image to the extent of previously created bbox
object.
#Import aerial image
<- rast("63613_RGB.tif")
rgb
#Crop
<- terra::crop(rgb, bbox)
rgb_cropped
#Plot the RGB image
plotRGB(rgb_cropped)
Similar to what we did for the DEM data, we also need to transform the RGB image into a matrix. It’s important to note that each band (Red, Green, Blue) should be converted individually.
#Assign names to the bands in the cropped image
names(rgb_cropped) <- c("r", "g", "b")
#Convert each band (Red, Green, Blue) to a matrix
<- rayshader::raster_to_matrix(rgb_cropped$r) # Red band
image_R <- rayshader::raster_to_matrix(rgb_cropped$g) # Green band
image_G <- rayshader::raster_to_matrix(rgb_cropped$b) # Blue band
image_B
#Create an RGB array and assign the color channels
<- array(0, dim = c(nrow(image_R), ncol(image_R), 3))
image_RGB 1] <- image_R # assign the values of the image_R matrix to the red channel
image_RGB[,,2] <- image_G # assign the values of the image_G matrix to the green channel
image_RGB[,,3] <- image_B # assign the values of the image_B matrix to the blue channel
image_RGB[,,
#Rescale the image to the [0, 1] range
<- scales::rescale(image_RGB, to = c(0, 1))
image_RGB
#Plot
plot_map(image_RGB)
Next up, let’s add the RGB image onto the DEM. Ensure that the subsequent processes depend on the persistence of the newly opened window initiated by the next code.
plot_3d(
# RGB image to be plotted
image_RGB, # DEM matrix
dem_matrix, windowsize = c(700, 600), # Size of the plotting window in pixels
zscale = 0.6, # Ratio between the x and y spacing
shadowdepth = -50, # Depth of shadows
zoom = 0.7, # Zoom factor
phi = 45, # Azimuth angle
theta = -45, # Rotation around the z-axis
fov = 70, # Field-of-view angle
background = "#D5D5D5", # Background color
shadowcolor = "#5e5e5e") # Color of shadows
Sys.sleep(0.2)
render_snapshot()
You can add labels to the plot (find more information on how to do this here. For example, add one viewing point
and diving place
:
# Add a label for the viewing point
render_label(
# Demographic matrix or data
dem_matrix, x = 180, # X-coordinate for label placement
y = 240, # Y-coordinate for label placement
altitude = 30, # Altitude of the label in 3D space
zscale = 0.6, # Scaling factor for the label in the z-direction
text = "Viewing point", # Text content of the label
textcolor = "black", # Color of the label text
linecolor = "black", # Color of the label outline
dashed = TRUE, # Flag indicating whether the label should have a dashed outline
alpha = 0.9, # Transparency level of the label
textsize = 1.5) # Size of the label text
# Add a label for the diving place
render_label(
dem_matrix, x = 390,
y = 370,
altitude = 60,
zscale = 0.6,
text = "Diving place",
textcolor = "black",
linecolor = "black",
dashed = TRUE,
alpha = 0.9,
textsize = 1.5)
# Capture a snapshot of the visualization with a title
render_snapshot(
title_text = "Rummu quarry \nData: Land Board", # Text for the snapshot title
title_color = "white", # Color of the snapshot title text
title_bar_alpha = 1) # Transparency level of the title bar
8.3. 3D animation with rayshader
The fundamental capability of the rayshader package lies in its ability to generate 3D animations. To produce animations, we can employ the render_snapshot
function to save individual frames. These frames can then be assembled into a cohesive animation using the av_encode_video
function, which is part of the av
package.
Following the selection of an aesthetically pleasing viewpoint for the above visualization, the subsequent two code chunks can be executed to generate a video centered around the heap. The first chunk generates 360 images, each representing a degree of view (for a total of 360 degrees), and the second chunk compiles these images into a video.
#Let's create a new folder to store the images:
dir.create("./image")
# Generate a sequence of angles from 0 to 360 with one image for each degree
<- seq(0,360, length.out = 361)[-1]
angles
# Loop through each angle to render and save the images
for(i in 1:360) {
# Adjust the camera angle based on the current iteration
render_camera(theta = -45 + angles[i])
# Render the scene and save the snapshot image
render_snapshot(filename = sprintf("./image/Rummu_quarry%i.png", i),
title_text = "./image/Rummu quarry \nData: Land Board",
title_color = "white",
title_bar_alpha = 1)}
::clear3d() rgl
Let’s create a video with those 360 images:
# Generate a vector of file paths for a series of images (in PNG format)
::av_encode_video(
avsprintf("./image/Rummu_quarry%d.png",seq(1,360, by = 1)),
framerate = 30, # frame rate for the video (30 frames per second)
output = "./image/Rummu_quarry.mp4") # Specify the output path and name
8.4. 3D mapping with rayshader and ggplot2
One of the pretty features of the Rayshader package is its ability to transform ggplot2
objects to visually 3D. The plot_gg()
function from Rayshader package facilitates the transformation of ggplot objects into a 3D format, projecting the fill or color aesthetic onto a third dimension. To demonstrate the creation of 3D visualizations using ggplot2, we will employ population
data introduced in lesson 1
.
In the next few lines of code, we use the ggplot()
function from the ggplot2 package to produce the gg_map
. The geom_sf()
function loads the polygon data from the Population field, and we also define the color coding through scale_fill_viridis()
function that accepts a column (Population) to define the color coding. Then, the final lines of code use the plot_gg()
function from the rayshader package to display the data in 3D. Notice map produced by the ggplot() function is passed to the plot_gg()
function.
#Import population data from lesson 1 folder
<- st_read("../R_01/population.gpkg")
est_pop
#Create ggplot
<- ggplot(data = est_pop) +
gg_map geom_sf(aes(fill = Population)) +
scale_fill_viridis() +
ggtitle('Population Distribution in Estonia') +
theme_dark()
#Plot in 3D using the plot_gg function
plot_gg(
# Input ggplot object to be rendered in 3D
gg_map, multicore = TRUE, # Flag to use multiple processor cores for faster rendering
width = 5, # Set the width of the resulting 3D plot
height = 5, # Set the height of the resulting 3D plot
scale = 250, # Vertical exaggeration of the 3D plot
zoom = 0.55, # Zoom level of the plot
theta = 45, # Azimuth angle (angle of the light source around the x-axis)
phi = 30, # Altitude angle (angle of the light source above the x-y plane)
sunangle = 225, # Angle of the light source for shading
windowsize = c(1400,866)) # Size of the plotting window in pixels
Sys.sleep(0.2)
render_snapshot()
Upon completing the 3D visualization for the ggplot object, the rendered result can be saved using the render_movie
function. It is crucial to ensure that the new window generated through the preceding code remains open when executing this section of code.
# Set the output file path and name for the rendered movie
<- file.path("estonia_population.mp4")
outputFile
# Call the render_movie function to generate the movie
render_movie(filename = outputFile)
The created video can be visualized using the embed_video()
function from the embedr
package.