--- title: "Introduction to mvdf" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Introduction to mvdf} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r, include = FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "#>" ) ``` ## A Minimal Viable Data Format for Using R as a Frontend for Blender The history of 3D graphics in R is almost as long as the history of R itself, spanning from [lattice graphics](https://cran.r-project.org/package=lattice) (which gained 3D visualizations sometime around 2001-2002) to [rgl](https://cran.r-project.org/package=rgl) (whose oldest source on CRAN dates back to 2004) to newer implementations such as [anglr](https://github.com/hypertidy/anglr), [ggrgl](https://github.com/coolbutuseless/ggrgl), and [rayshader](https://www.rayshader.com/)/[rayrender](http://www.rayrender.net/). Each of these fantastic packages can be used to produce beautiful 3D visualizations from data, with the processing and rendering happening right inside your R session. `mvdf` is a little bit different. Rather than visualize your data itself, `mvdf` outsources the work, helping you use R as a frontend for the [Blender 3D rendering engine](https://www.blender.org/), translating data between analysis and visualization steps. As [the rayshader documentation says](https://www.tylermw.com/3d-ggplots-with-rayshader/), learning about programming, data science, and data visualization is hard enough: we don't need to add Blender to the list. `mvdf` aims to help you build all sorts of visualizations from almost any input data with the same set of generic tools, without needing to get into the weeds of 3D rendering technology. While `mvdf` is a relatively low-level package designed for developers (and check out the `mvdf for Developers` vignette for more information on that), this guide walks through the basic steps of getting from data to visualization, with pointers to documentation on how to make more complex renders. Note that all of our examples from here on out expect that you've already installed Blender ([version 2.80 or up](https://www.blender.org/download/)) and mvdf (via `remotes::install.github("mikemahoney218/mvdf")`). ## So What's an `mvdf` Anyway? A `mvdf` is a _minimum viable data format_ for 3D rendering -- that is, it's a data frame that contains _at least_ the bare minimum amount of information required to produce a render. For the simplest renders, that generally means having x, y, and z coordinates, as well as a unique identifier, for each object to be drawn. We'll call any object that contains at least that `mvdf` an "`mvdf` object" from here on out, and we can create one using `mvdf_obj`: ```{r setup, message=FALSE} library(mvdf) library(dplyr) # For demonstration purposes only -- not required by mvdf! iris %>% mvdf_obj( x = "Sepal.Length", y = "Sepal.Width", z = "Petal.Length") %>% head() ``` As you can see, our `mvdf_obj` only contains that minimal set of data needed to make a 3D visualization (plus an index column that's been calculated for us). If we don't want to lose the other fields in our data frame, we can include any tabular data we want in the special `metadata` field of `mvdf_obj`: ```{r} iris_mvdf <- iris %>% mvdf_obj( x = "Sepal.Length", y = "Sepal.Width", z = "Petal.Length", metadata = iris) head(iris_mvdf) ``` We can also store non-tabular data in the `appendix` slot of `mvdf_obj`, letting us preserve all our data in a single object. By working with the "getter" and "setter" methods provided by the package, we can manipulate these fields just like we would any other data frame: ```{r} iris_mvdf %>% mvdf() %>% # Retrieve the mvdf from iris_mvdf merge( metadata(iris_mvdf), # Getter methods also exist for metadata and appendix by = "idx" ) %>% mutate(x = ifelse(Species == "setosa", 0, x)) %>% set_mvdf(iris_mvdf) %>% # Setters set_metadata and set_appendix also exist head() ``` So why use `mvdf` objects? The simple answer is that `mvdf` objects are designed to be a "contract" defining how different rendering methods should interact with your data. For instance, if we run `?mvdf_obj` we can see that `x`, `y`, and `z` are all defined as follows: ``` Numeric: distance of the origin of the object from the origin of the grid system (the central point at 0, 0, 0) in meters in the given direction. Must have no `NA`, `NULL`, `NaN`, `Inf`, or `-Inf` values. [...] Coordinates are assumed to be on a right-handed coordinate system with Z oriented as the natural "vertical" direction. ``` All `mvdf` objects that have these fields (which, in this case, is all of them) agree to this same definition. When possible, definitions are checked automatically when creating an `mvdf`: ```{r} try(mvdf_obj(x = 1, y = 1, z = NA)) ``` And when updating `mvdf` values: ```{r} temp_mvdf <- iris_mvdf %>% mvdf() %>% mutate(x = NA) # This is the same as set_mvdf(temp_mvdf, iris_mvdf) try(mvdf(iris_mvdf) <- temp_mvdf) ``` Not all definitions can be automatically checked (for instance, there's no real way to tell if your data is oriented correctly or stored in meters). In those cases, `mvdf` objects (and rendering methods) expect the inputs to be correct without any validation. ## Making Simple Renders The advantage of this contract is that we can then make renders super easily. `mvdf` provides functions to let you interactively build Python scripts which can then be executed in Blender to produce renders. The absolute simplest rendering pipeline looks something like this: ```{r} render_script <- # Delete placeholder objects and import standard Python modules create_blender_frontmatter() %>% # Add cubes at each point in space in our mvdf add_mesh_primitive(object = iris_mvdf, primitive = "cube", size = 0.2) %>% # Save the output to save_render.blend add_blender_endmatter(filepath = "save_render.blend") ``` That pipeline produces a Python script that we could then run inside Blender to produce our render. If we don't want to boot up Blender, we can also execute the script in the background (which is a good bit more stable for larger renders) using the `execute_render` function: ```{r, eval=FALSE} execute_render(render_script) ``` This produces an output Blender file (named `save_render.blend`) that we can open in Blender to see something like this: ```{r, echo=FALSE, out.width='100%'} knitr::include_graphics("iris_cubes.jpg") ``` ## More Complicated Renders This is neat, but pretty basic. Luckily, `mvdf` is built to be a modular system, with more complicated data and rendering methods using almost exactly the same tools as the most basic renders. For instance, to color our cubes by species, we need to apply what Blender calls a "material" to each of our objects. To do that, we can create an `mvdf_simple_material` object instead of an `mvdf_obj` and pass a color column to `diffuse_color`: ```{r} iris_material <- iris %>% mutate(diffuse_color = case_when( Species == "setosa" ~ "blue", Species == "versicolor" ~ "red", TRUE ~ "green" )) %>% mvdf_simple_material( x = Sepal.Length, y = Sepal.Width, z = Petal.Length, # We can also leave this blank to use the "diffuse_color" column by default diffuse_color = diffuse_color, # Transform colors to an RGBA array translate_colors = TRUE) iris_material %>% head() ``` You'll notice that `mvdf_simple_material` did a few things for us automatically: * We now have `metallic` and `roughness` columns in our mvdf, even though we didn't specify anything to do with those fields! Because materials in Blender have metallic and roughness properties no matter what, `mvdf_simple_material` makes it explicit what default level any missing values will be set to. * Our color inputs of "blue", "red", and "green" were translated to RGBA arrays because we set `translate_colors` to `TRUE`. Now that we have an `mvdf_simple_material` object, we can use exactly the same code as above to construct our rendering script. Other than changing the object we provide `add_mesh_primitive`, this code is exactly the same as above -- our functions are able to infer what we want from the class of our provided object alone. ```{r} render_script <- create_blender_frontmatter() %>% add_mesh_primitive(object = iris_material, # Replace iris_mvdf here primitive = "cube", size = 0.2) %>% add_blender_endmatter(filepath = "save_render.blend") ``` We execute it the exact same way: ```{r, eval=FALSE} execute_render(render_script) ``` And our output looks the same, but now is colored by species! ```{r, echo=FALSE, out.width='100%'} knitr::include_graphics("iris_color.jpg") ``` By creating a specific class from our data, we provide our functions with all the information they need to operate. If we decide that we don't want to use those fancy colors, we can just cast our data back into `mvdf_obj` format: ```{r} set_values(iris_material, newclass = "mvdf_obj") %>% head() ``` And those are the basic features included in `mvdf`! If you're looking to extend these features to create more involved renders, check out the `mvdf for Developers` vignette, and happy rendering!