9  3D Visualisation

Published

June 18, 2026

Numbers are useful; actually seeing the mesh is better. This chapter covers two routes: rgl (Adler et al. 2024) for interactive exploration in RStudio (spin it with the mouse, zoom in, inspect details), and rayshader (Morgan-Wall 2024) for rendering publication-quality static images.

Both work on mesh_vcg from Reading OBJ and STL Meshes.

Note

Representative result. The rgl and rayshader snippets in this chapter need a translated mesh and an OpenGL device, so they aren’t executed during book rendering. Run them in your own R session to get the interactive WebGL widget or the rendered PNG described alongside each example.

9.1 Interactive 3D with rgl

shade3d() opens an OpenGL window where you can rotate and zoom with the mouse. Takes one line:

library(rgl)
open3d()
shade3d(mesh_vcg, col = "#C0C0C0", alpha = 0.9)
bg3d("white")
axes3d()
title3d("Aerial DWG — Translated Mesh")

9.1.1 Embedding in HTML Output

rgl windows are interactive in RStudio but don’t embed in HTML documents on their own. rglwidget() wraps the scene in a self-contained WebGL widget that works in any browser:

# Add this once at the top of your document (setup chunk)
knitr::knit_hooks$set(webgl = rgl::hook_webgl)

open3d()
shade3d(mesh_vcg, col = "#4A90D9", alpha = 0.85)
rglwidget()
Note

The WebGL widget is fully self-contained in the rendered HTML. Readers can rotate and zoom without a running R server. It does require a modern browser (Chrome, Firefox, Safari, Edge all work fine).

9.2 Colouring by Height

Map vertex Z-values to a colour ramp to reveal the model’s vertical structure instantly. Blues for low, reds for high:

z_vals <- mesh_vcg$vb[3, ]
z_norm <- (z_vals - min(z_vals)) / diff(range(z_vals))   # 0–1

cols      <- colorRampPalette(c("#2166ac", "#92c5de", "#f7f7f7",
                                 "#f4a582", "#d6604d"))(100)
vert_cols <- cols[ceiling(z_norm * 99) + 1]

open3d()
shade3d(mesh_vcg, col = vert_cols)
rglwidget()

9.3 Rendered Images with rayshader

rayshader renders the active rgl scene with ray-traced lighting and ambient occlusion, great for reports and presentations. The output is a PNG file.

library(rayshader)

vertices  <- t(mesh_vcg$vb[1:3, ])
triangles <- t(mesh_vcg$it)

open3d()
triangles3d(vertices[triangles[, 1], ],
            vertices[triangles[, 2], ],
            vertices[triangles[, 3], ],
            col = "steelblue")

render_snapshot(filename    = "aerial_render.png",
                width       = 1200,
                height      = 900,
                title_text  = "Aerial DWG — Rayshader Render",
                title_color = "white",
                title_size  = 24,
                clear       = TRUE)

Include the render in your Quarto document with a standard image link:

![Aerial DWG rendered with rayshader](aerial_render.png)

9.4 Depth of Field

Add a depth-of-field effect to emphasise the model’s 3D structure in the output image, works especially well for tall structures:

render_depth(focus       = 0.7,
             focallength = 200,
             filename    = "aerial_dof.png")

9.5 Combining Multiple Meshes

Overlay two meshes in one scene to compare them — useful when you have an original and a repaired or simplified version. Rvcg::vcgMerge() fuses tmesh3d objects into a single mesh; for separate colours, render them individually before merging:

library(Rvcg)

mesh_a <- vcgImport("aerial_v1.obj")   # original
mesh_b <- vcgImport("aerial_v2.obj")   # repaired

open3d()
shade3d(mesh_a, col = "#4A90D9", alpha = 0.8)   # blue — original
shade3d(mesh_b, col = "#E74C3C", alpha = 0.6)   # red  — changes
title3d("v1 (blue) vs v2 (red)")
rglwidget()

To merge them into one object (e.g., for a metrics pass on the combined geometry):

mesh_combined <- vcgMerge(mesh_a, mesh_b)
cat("Combined vertices:", ncol(mesh_combined$vb), "\n")

9.6 Exporting the Scene

Save the interactive rgl scene as a standalone HTML file — no R session needed to view it:

open3d()
shade3d(mesh_vcg, col = "#4A90D9")

# Self-contained HTML with embedded WebGL
writeWebGL(dir = "webgl_export", filename = "aerial_viewer.html",
           width = 900, height = 600)

For a quick PNG without launching rayshader:

snapshot3d("aerial_snapshot.png", width = 1200, height = 900)

9.7 Quick Comparison: rgl vs. rayshader

rgl rayshader
Output Interactive HTML widget Static PNG
Use for Exploration, dashboards Reports, presentations
Render time Instant Seconds–minutes
WebGL support Yes (via rglwidget()) No