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
threed
defines afortify.mesh3d()
function.- If a
mesh3d
object is given as the data for aggplot2
call,ggplot2
will automatically usefortify()
to convert into a data.frame. - i.e. because
threed
definesfortify.mesh3d()
, we can callggplot2
directly with amesh3d
object.
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 thegroup
aesthetic 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,
ggplot2
will draw them in the order of thegroup
variable. - If we group by
element_id
then 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
group
variable. - When converting a
mesh3d
to a data.frame, azorder
variable is created starting at1
for the furtherest element, up ton
for the closest element. - i.e. change the
group
variable fromelement_id
tozorder
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 + fnz
to 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()