Introduction
This post explores how to plot a cube in ggplot2
using the threed library.
ggplot2 doesn’t include any notion of a 3rd spatial axis, so instead, after
manipulating a 3d object, we use perspective projection to “flatten” its faces and
vertices onto a 2d plane. These projected faces/vertices are what ggplot2 will plot.
Prepare an object for plotting
- Create an object (here the standard 2x2x2 cube is being used)
- Define where camera is located, and where it is looking
- Transform the object into camera space
- Perspective transform the data
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# The `threed` package has some builtin objects in `threed::mesh3dobj`
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
obj <- threed::mesh3dobj$cube
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Define camera 'lookat' matrix i.e. camera-to-world transform
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
camera_to_world <- threed::look_at_matrix(eye = c(1.5, 1.75, 4), at = c(0, 0, 0))
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Transform the object into camera space and do perspective projection
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
obj <- obj %>%
transform_by(invert_matrix(camera_to_world)) %>%
perspective_projection()
as.data.frame(obj) %>% knitr::kable()
| element_id | element_type | vorder | x | y | z | vertex | vnx | vny | vnz | fnx | fny | fnz | fcx | fcy | fcz | zorder | zorder_var | hidden |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 1 | 4 | 1 | -0.0945866 | -0.0706857 | 1.581399 | 1 | -0.1855445 | -0.1356142 | -0.9732328 | 0.1523493 | 0.1540033 | -0.9762544 | 0.0748818 | 0.0756647 | 1.630932 | 1 | 1.630932 | TRUE |
| 1 | 4 | 2 | -0.1077957 | 0.2603509 | 1.631559 | 3 | -0.2154400 | -0.3088482 | -0.9263900 | 0.1523493 | 0.1540033 | -0.9762544 | 0.0748818 | 0.0756647 | 1.630932 | 1 | 1.630932 | TRUE |
| 1 | 4 | 3 | 0.2693976 | 0.2400504 | 1.687219 | 4 | -0.4345424 | -0.3475777 | -0.8308806 | 0.1523493 | 0.1540033 | -0.9762544 | 0.0748818 | 0.0756647 | 1.630932 | 1 | 1.630932 | TRUE |
| 1 | 4 | 4 | 0.2325119 | -0.1270569 | 1.623552 | 2 | -0.3706042 | -0.1407513 | -0.9180640 | 0.1523493 | 0.1540033 | -0.9762544 | 0.0748818 | 0.0756647 | 1.630932 | 1 | 1.630932 | TRUE |
| 2 | 4 | 1 | -0.1077957 | 0.2603509 | 1.631559 | 3 | -0.2154400 | -0.3088482 | -0.9263900 | 0.0000000 | 0.9394660 | 0.3426420 | 0.0013440 | 0.2085811 | 1.773502 | 5 | 1.773502 | FALSE |
| 2 | 4 | 2 | -0.3483420 | 0.1903526 | 1.823482 | 7 | 0.1812743 | 0.2777220 | 0.9434035 | 0.0000000 | 0.9394660 | 0.3426420 | 0.0013440 | 0.2085811 | 1.773502 | 5 | 1.773502 | FALSE |
| 2 | 4 | 3 | 0.1921159 | 0.1435705 | 1.951750 | 8 | -0.6699657 | 0.4605475 | 0.5822731 | 0.0000000 | 0.9394660 | 0.3426420 | 0.0013440 | 0.2085811 | 1.773502 | 5 | 1.773502 | FALSE |
| 2 | 4 | 4 | 0.2693976 | 0.2400504 | 1.687219 | 4 | -0.4345424 | -0.3475777 | -0.8308806 | 0.0000000 | 0.9394660 | 0.3426420 | 0.0013440 | 0.2085811 | 1.773502 | 5 | 1.773502 | FALSE |
| 3 | 4 | 1 | 0.2325119 | -0.1270569 | 1.623552 | 2 | -0.3706042 | -0.1407513 | -0.9180640 | 0.9631644 | -0.1369155 | 0.2314485 | 0.2119637 | -0.0287422 | 1.767221 | 4 | 1.767221 | FALSE |
| 3 | 4 | 2 | 0.2693976 | 0.2400504 | 1.687219 | 4 | -0.4345424 | -0.3475777 | -0.8308806 | 0.9631644 | -0.1369155 | 0.2314485 | 0.2119637 | -0.0287422 | 1.767221 | 4 | 1.767221 | FALSE |
| 3 | 4 | 3 | 0.1921159 | 0.1435705 | 1.951750 | 8 | -0.6699657 | 0.4605475 | 0.5822731 | 0.9631644 | -0.1369155 | 0.2314485 | 0.2119637 | -0.0287422 | 1.767221 | 4 | 1.767221 | FALSE |
| 3 | 4 | 4 | 0.1538294 | -0.3715328 | 1.806364 | 6 | 0.3411191 | 0.1021382 | 0.9344547 | 0.9631644 | -0.1369155 | 0.2314485 | 0.2119637 | -0.0287422 | 1.767221 | 4 | 1.767221 | FALSE |
| 4 | 4 | 1 | -0.0945866 | -0.0706857 | 1.581399 | 1 | -0.1855445 | -0.1356142 | -0.9732328 | -0.6370835 | 0.0905625 | -0.7654561 | -0.2099435 | 0.0306140 | 1.689395 | 3 | 1.689395 | TRUE |
| 4 | 4 | 2 | -0.2890497 | -0.2575617 | 1.721140 | 5 | -0.1542710 | -0.1026941 | -0.9826771 | -0.6370835 | 0.0905625 | -0.7654561 | -0.2099435 | 0.0306140 | 1.689395 | 3 | 1.689395 | TRUE |
| 4 | 4 | 3 | -0.3483420 | 0.1903526 | 1.823482 | 7 | 0.1812743 | 0.2777220 | 0.9434035 | -0.6370835 | 0.0905625 | -0.7654561 | -0.2099435 | 0.0306140 | 1.689395 | 3 | 1.689395 | TRUE |
| 4 | 4 | 4 | -0.1077957 | 0.2603509 | 1.631559 | 3 | -0.2154400 | -0.3088482 | -0.9263900 | -0.6370835 | 0.0905625 | -0.7654561 | -0.2099435 | 0.0306140 | 1.689395 | 3 | 1.689395 | TRUE |
| 5 | 4 | 1 | -0.0945866 | -0.0706857 | 1.581399 | 1 | -0.1855445 | -0.1356142 | -0.9732328 | 0.0000000 | -0.5988573 | -0.8008558 | 0.0006763 | -0.2067093 | 1.683114 | 2 | 1.683114 | TRUE |
| 5 | 4 | 2 | 0.2325119 | -0.1270569 | 1.623552 | 2 | -0.3706042 | -0.1407513 | -0.9180640 | 0.0000000 | -0.5988573 | -0.8008558 | 0.0006763 | -0.2067093 | 1.683114 | 2 | 1.683114 | TRUE |
| 5 | 4 | 3 | 0.1538294 | -0.3715328 | 1.806364 | 6 | 0.3411191 | 0.1021382 | 0.9344547 | 0.0000000 | -0.5988573 | -0.8008558 | 0.0006763 | -0.2067093 | 1.683114 | 2 | 1.683114 | TRUE |
| 5 | 4 | 4 | -0.2890497 | -0.2575617 | 1.721140 | 5 | -0.1542710 | -0.1026941 | -0.9826771 | 0.0000000 | -0.5988573 | -0.8008558 | 0.0006763 | -0.2067093 | 1.683114 | 2 | 1.683114 | TRUE |
| 6 | 4 | 1 | -0.2890497 | -0.2575617 | 1.721140 | 5 | -0.1542710 | -0.1026941 | -0.9826771 | -0.2439436 | -0.2465920 | 0.9379147 | -0.0728616 | -0.0737928 | 1.825684 | 6 | 1.825684 | FALSE |
| 6 | 4 | 2 | 0.1538294 | -0.3715328 | 1.806364 | 6 | 0.3411191 | 0.1021382 | 0.9344547 | -0.2439436 | -0.2465920 | 0.9379147 | -0.0728616 | -0.0737928 | 1.825684 | 6 | 1.825684 | FALSE |
| 6 | 4 | 3 | 0.1921159 | 0.1435705 | 1.951750 | 8 | -0.6699657 | 0.4605475 | 0.5822731 | -0.2439436 | -0.2465920 | 0.9379147 | -0.0728616 | -0.0737928 | 1.825684 | 6 | 1.825684 | FALSE |
| 6 | 4 | 4 | -0.3483420 | 0.1903526 | 1.823482 | 7 | 0.1812743 | 0.2777220 | 0.9434035 | -0.2439436 | -0.2465920 | 0.9379147 | -0.0728616 | -0.0737928 | 1.825684 | 6 | 1.825684 | FALSE |
Plot the points for the vertices of the object
threeddefines afortify.mesh3d()function.- If a
mesh3dobject is given as the data for aggplot2call,ggplot2will automatically usefortify()to convert into a data.frame. - i.e. because
threeddefinesfortify.mesh3d(), we can callggplot2directly with amesh3dobject.
ggplot(obj, aes(x, y)) +
geom_point() +
theme_void() +
theme(legend.position = 'none') +
coord_equal()

Plot the outline of each polygon
- Each element has a unique
element_id, and this is used as thegroupaesthetic to inform ggplot that it should draw one polygon for each element. - Set
fill = NA, colour = 'black'to draw only the borders of each polygon.
ggplot(obj, aes(x, y, group = element_id)) +
geom_polygon(fill = NA, colour = 'black', size = 0.2) +
theme_void() +
theme(legend.position = 'none') +
coord_equal()

Naive Filled Polygons
- When filling polygons,
ggplot2will draw them in the order of thegroupvariable. - If we group by
element_idthen the polygons are drawn in the order in which they were defined. This means that polygons which are further away will be draw over the top of ones which are actually close to the eye.
- The result will look weird - Almost Escher-esque!
ggplot(obj, aes(x, y, group = element_id)) +
geom_polygon(fill = 'lightblue', colour = 'black', size = 0.2) +
theme_void() +
theme(legend.position = 'none') +
coord_equal()

Filled Polygons - (3) Use the zorder variable to control draw order
- Third method for drawing filled polygons:
- Draw the elements from furtherest to nearest
- Exploit the fact that elements are drawn in the order of the
groupvariable. - When converting a
mesh3dto a data.frame, azordervariable is created starting at1for the furtherest element, up tonfor the closest element. - i.e. change the
groupvariable fromelement_idtozorder
ggplot(obj, aes(x, y, group = zorder)) +
geom_polygon(fill = 'lightblue', colour='black', size = 0.2) +
theme_void() +
theme(legend.position = 'none') +
coord_equal()

Fake-shaded polygon
- The normal to each face is included in the data.frame representation as
fnx, fny, fnz - By calculating dot products to a light source positioned in the scene, the fraction illumination could be calculated for each element.
- Here, the shading is being completely faked by using the sum
fny + fnzto shade the polygons.
ggplot(obj, aes(x, y, group = zorder)) +
geom_polygon(aes(fill = fny + fnz), colour = 'black', size = 0.2) +
theme_void() +
theme(legend.position = 'none') +
coord_equal()




