The purpose of this package is to provide a 3D rendering backend for a very particular visual aesthetic.
That is, {isocubes}
is an isometric rendering canvas with cubes as the
only graphics primitive.
Some tools are included for creating particular scenes, but in general, if you can provide a list of (x,y,z) integer coorindates of what to render, then isocumes will create a 3d render.
isocubesGrob()
to convert 3d integer coordinates into a grob for
plottingcoord_heightmap()
to create coordinates for a heightmap from a
matrix and (optional) colour informationsdf_sphere()
, sdf_cyl()
, sdf_box()
,
sdf_torus()
, sdf_plane()
sdf_translate()
, sdf_onion()
, sdf_scale()
,
sdf_round()
, sdf_rotatex()
, sdf_repeat_infinite()
,
sdf_repeat()
sdf_union()
, sdf_interpolate()
,
sdf_intersect()
, sdf_subtract()
, sdf_union_smooth()
,
sdf_intersect_smooth()
, sdf_subtract_smooth()
isocubesGrob(coords, ysize, xo, yo)
xo
and yo
give the positition of the origin of the isometric
view within the graphics window. These are fractional values which
will be interpreted as snpc
units i.e. fractional width and height
of the graphics devices.ysize
is the main control for cube sizing. This value is the
height of the cube expressed as a fraction of the window height. y
vertical.(x, y, z)
coordinates given to position the cubes will be
rounded to the nearest integer. There is no fractional positioning
of cubes.Isometric cubes have advantages over other axonometric and perspective coordinate systems:
You can install from GitHub with:
# install.package('remotes')
remotes::install_github('coolbutuseless/isocubes')
library(grid)
library(purrr)
library(isocubes)
x <- c(9, 8, 7, 6, 5, 4, 3, 2, 10, 9, 3, 2, 11, 10, 3, 2, 11, 10,
3, 2, 11, 10, 3, 2, 11, 10, 3, 2, 10, 9, 3, 2, 9, 8, 7, 6, 5,
4, 3, 2, 10, 9, 3, 2, 11, 10, 3, 2, 11, 10, 3, 2, 11, 10, 3,
2, 11, 10, 3, 2, 11, 10, 3, 2, 11, 10, 3, 2)
y <- c(15, 15, 15, 15, 15, 15, 15, 15, 14, 14, 14, 14, 13, 13, 13,
13, 12, 12, 12, 12, 11, 11, 11, 11, 10, 10, 10, 10, 9, 9, 9,
9, 8, 8, 8, 8, 8, 8, 8, 8, 7, 7, 7, 7, 6, 6, 6, 6, 5, 5, 5, 5,
4, 4, 4, 4, 3, 3, 3, 3, 2, 2, 2, 2, 1, 1, 1, 1)
coords <- data.frame(x = x, y = y, z = 0)
cubes <- isocubesGrob(coords, ysize = 1/25)
grid.newpage(); grid.draw(cubes)
# Colour the cubes with rainbow
cubes <- isocubesGrob(coords, fill = rainbow(nrow(coords)), ysize = 1/25)
grid.newpage(); grid.draw(cubes)
# VaporWave palette
cubes <- isocubesGrob(coords, fill = '#ff71ce', fill2 = '#01cdfe',
fill3 = '#05ffa1', ysize = 1/25)
grid.newpage(); grid.draw(cubes)
# Nightmare palette
cubes <- isocubesGrob(coords,
fill = rainbow(nrow(coords)),
fill2 = 'hotpink',
fill3 = viridisLite::inferno(nrow(coords)),
ysize = 1/25, col = NA)
grid.newpage(); grid.draw(cubes)
library(grid)
library(isocubes)
N <- 13
coords <- expand.grid(x=seq(-N, N), y = seq(-N, N), z = seq(-N, N))
keep <- with(coords, sqrt(x * x + y * y + z * z)) < N
coords <- coords[keep,]
cubes <- isocubesGrob(coords, ysize = 1/35, xo = 0.5, yo = 0.5)
grid.newpage()
grid.draw(cubes)
library(isocubes)
N <- 15
coords <- expand.grid(x=0:N, y=0:N, z=0:N)
coords <- coords[sample(nrow(coords), 0.66 * nrow(coords)),]
fill <- rgb(red = 1 - coords$x / N, coords$y /N, 1 - coords$z/N, maxColorValue = 1)
cubes <- isocubesGrob(coords, fill, ysize = 1/40)
grid.newpage()
grid.draw(cubes)
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Prepare a matrix of values
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
mat <- volcano
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# An optional matrix of colours
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
val <- as.vector(mat)
val <- round(255 * (val - min(val)) / diff(range(val)))
col <- viridisLite::viridis(256)[val + 1L]
dim(col) <- dim(mat)
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Find the (integer) coordiinates of the cubes in the heightmap
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
coords <- coords_heightmap(mat - min(mat), col = col, scale = 0.3)
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Convert the coordinates into a grob
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
cubes <- isocubesGrob(coords, ysize = 1/100, fill = coords$col, xo = 0.8)
grid.newpage(); grid.draw(cubes)
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Load image and convert to a matrix of heights
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
img <- png::readPNG("man/figures/Rlogo-small-blur.png")
ht <- round( 10 * (1 - img[,,2]) ) # Use Green channel intensity as height
ht[,1] <- 0 # image editing to remove some artefacts
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# A matrix of colours extracted from the image
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
col <- rgb(img[,,1], img[,,2], img[,,3])
dim(col) <- dim(ht)
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# convert to cubes and draw
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
coords <- coords_heightmap(ht, col = col, ground = 'xy')
cubes <- isocubesGrob(coords, ysize = 1/130, fill = coords$col, col = NA, light = 'right-top')
grid.newpage(); grid.draw(cubes)
ambient
library(grid)
library(ggplot2)
library(dplyr)
library(ambient)
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Create some perline noise on an NxN grid
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
set.seed(3)
N <- 60
dat <- long_grid(x = seq(0, 10, length.out = N), y = seq(0, 10, length.out = N)) %>%
mutate(
noise =
gen_perlin(x, y, frequency = 0.3) +
gen_perlin(x, y, frequency = 2) / 10
)
hm <- dat %>%
mutate(
x = x * 4,
z = y * 4,
y = noise * 4
)
pal <- topo.colors(11)
sy <- as.integer(10 * (hm$y - min(hm$y)) / diff(range(hm$y))) + 1
cols <- pal[sy]
cubes <- isocubesGrob(hm, ysize = 1/45, xo = 0.7, fill = cols, col = NA)
grid.newpage(); grid.draw(cubes)
library(grid)
library(isocubes)
library(bdftools)
bdf <- bdftools::read_bdf_builtin("spleen-32x64.bdf")
single_word <- bdftools::bdf_create_df(bdf, "#RStats!")
N <- 10
cols <- rainbow(N)
multiple_words <- purrr::map_dfr(seq(N), function(i) {
single_word$z <- i
single_word$col <- cols[i]
single_word
})
cubes <- isocubesGrob(multiple_words, ysize = 1/170, xo = 0.1, fill = multiple_words$col, light = 'right-top', col = NA)
grid.newpage(); grid.draw(cubes)
library(grid)
library(dplyr)
library(isocubes)
# Create a scene that consists of a scaled torus
scene <- sdf_torus(3, 1) %>%
sdf_scale(5)
# Render the scene into a list of coordinates of voxels inside objects
coords <- sdf_render(scene, N = 30)
# Create cubes, and draw
cubes <- isocubesGrob(coords, ysize = 1/50, xo = 0.5, yo = 0.5, fill = 'lightblue')
grid.newpage(); grid.draw(cubes)
library(dplyr)
library(grid)
library(isocubes)
sphere <- sdf_sphere() %>%
sdf_scale(40)
box <- sdf_box() %>%
sdf_scale(32)
cyl <- sdf_cyl() %>%
sdf_scale(16)
scene <- sdf_subtract_smooth(
sdf_intersect(box, sphere),
sdf_union(
cyl,
sdf_rotatey(cyl, pi/2),
sdf_rotatex(cyl, pi/2)
)
)
coords <- sdf_render(scene, 50)
cubes <- isocubesGrob(coords, ysize = 1/100, xo = 0.5, yo = 0.5, fill = 'lightseagreen')
grid.newpage(); grid.draw(cubes)
Unfortunately this isn’t fast enough to animate in realtime, so I’ve stitched together individuall saved frames to create an animation.
thetas <- seq(0, pi, length.out = 45)
for (i in seq_along(thetas)) {
cat('.')
theta <- thetas[i]
rot_scene <- scene %>%
sdf_rotatey(theta) %>%
sdf_rotatex(theta * 2) %>%
sdf_rotatez(theta / 2)
coords <- sdf_render(rot_scene, 50)
cubes <- isocubesGrob(coords, ysize = 1/110, xo = 0.5, yo = 0.5)
png_filename <- sprintf("working/anim/%03i.png", i)
png(png_filename, width = 800, height = 800)
grid.draw(cubes)
dev.off()
}
# ffmpeg -y -framerate 20 -pattern_type glob -i 'anim/*.png' -c:v libx264 -pix_fmt yuv420p -s 800x800 'anim.mp4'
Arrange cubes by -x
, -z
then y
to ensure cubes are drawn in the
correct ordering such that cubes in front are drawn over the top of
cubes which are behind.
All the faces of all the cubes are then calculated as polygons - each with 4 vertices.
The data for all polygons is then concatenated into a single
polygonGrob()
call with an appropiate vector for id.lengths
to split
the data.
Most of the prototyping for this package was done with
{ingrid}
- a package I
wrote which I find makes working iteratively/at-the-console with base
grid graphics a bit easier.