piecepackr has some basic support for projecting 3D board games onto a 2D diagram. It can make diagrams using an orthographic or oblique parallel projection using R’s grid graphics library. It can also render images using the rayrender, rayvertex, and rgl 3D graphics packages. These latter packages usually use a perspective projection but can be configured to produce parallel projections.
Contents
Orthographic projections (grid)
By default piecepackr‘s grid.piece() function makes an approximate “top view” orthographic projection with pieces drawn later “placed on top” of (and potentially hiding) pieces drawn earlier:
library("piecepackr")
options(piecepackr.default.units = "in",
piecepackr.cfg = game_systems("dejavu")$piecepack)
grid.piece("tile_back", x=0.5+c(3,1,3,1), y=0.5+c(3,3,1,1))
grid.piece("tile_back", x=0.5+3, y=0.5+1)
grid.piece("tile_back", x=0.5+3, y=0.5+1)
grid.piece("die_face", suit=3, rank=5, x=1, y=1)
grid.piece("pawn_face", x=1, y=4, angle=0)
grid.piece("coin_back", x=3, y=4, angle=180)
grid.piece("coin_back", suit=4, x=3, y=4, angle=180)
grid.piece("coin_back", suit=2, x=3, y=1, angle=90)

‘Top view’ orthographic projection of a piecepack game
grid.piece() makes an exact “top view” orthographic projection under the following conditions:
- All pieces are placed “flat” parallel/perpendicular to the game table. Although most piecepack games do this there are exceptions like San Andreas where some tiles may shift from flat to “leaning” (and vice versa) during the course of a game.
- The game doesn’t use pyramids placed on their side [1]. However you in this case one can approximate an orthographic projection by doing an oblique projection with a very small scale factor i.e. op_scale = 0.001 [2].
[1] | Instead we just draw the visible pyramid face. This is ideal for print-and-play layouts and usually works okay for game diagrams as well. |
[2] | (1, 2) If using a version of piecepackr older than v1.11.0 use the minimum op_scale value of 0.01. |
Oblique projections (grid)
Note
The oblique projections of dice sides, two-sided tokens placed on their side, and pyramids were improved in piecepackr v1.11.1. These enhancements require R 4.2 and a graphics device that supports the “affine transformation” feature such as the “cairo” devices e.g. png(type = "cairo") or cairo_pdf().
With a little more effort by the piecepackr programmer one can also make oblique projections with grid.piece() which makes it much easier to tell when pieces have been placed on top of other pieces:
library("piecepackr")
options(piecepackr.default.units = "in",
piecepackr.cfg = game_systems("dejavu")$piecepack,
piecepackr.op_angle = 45, piecepackr.op_scale = 0.5)
grid.piece("tile_back", x=0.5+c(3,1,3,1), y=0.5+c(3,3,1,1))
grid.piece("tile_back", x=0.5+3, y=0.5+1, z=1/4+1/8)
grid.piece("tile_back", x=0.5+3, y=0.5+1, z=2/4+1/8)
grid.piece("die_face", suit=3, rank=5, x=1, y=1, z=1/4+1/4)
grid.piece("pawn_top", x=1, y=4, z=1/4+1/8, angle=180)
grid.piece("coin_back", x=3, y=4, z=1/4+1/8, angle=180)
grid.piece("coin_back", suit=4, x=3, y=4, z=2/4+1/8, angle=180)
grid.piece("coin_back", suit=2, x=3, y=1, z=3/4+1/8, angle=90)

Oblique projection of a piecepack game
op_scale and op_angle are the arguments that control the appearance of the oblique projection. op_scale determines how much to scale the length of the piece’s edge along op_angle. An op_scale of 0.5 is commonly used in the cabinet projection, an op_scale of 1.0 is used in the cavalier projection, and an op_scale of 0.001 [2] gives you a “top view” orthographic projection. op_angle controls what angle the edges the pieces “go up” - it defaults to 45 degrees.
Depending on your preferences you may want to change up your pawns look and/or the color of your piece edges:
library("piecepackr")
cfg <- pp_cfg(list(width.pawn=0.75, height.pawn=0.75, depth.pawn=1,
dm_text.pawn="", shape.pawn="convex6", invert_colors.pawn=TRUE,
edge_color.coin="tan", edge_color.tile="tan",
border_lex=2, border_color="black"))
options(piecepackr.default.units = "in", piecepackr.cfg = cfg,
piecepackr.op_angle = 45, piecepackr.op_scale = 0.5)
grid.piece("tile_back", x=0.5+c(3,1,3,1), y=0.5+c(3,3,1,1))
grid.piece("tile_back", x=0.5+3, y=0.5+1, z=1/4+1/8)
grid.piece("tile_back", x=0.5+3, y=0.5+1, z=2/4+1/8)
grid.piece("die_face", suit=3, rank=5, x=1, y=1, z=1/4+1/4)
grid.piece("pawn_face", x=1, y=4, z=1/4+1/2, angle=0)
grid.piece("coin_back", x=3, y=4, z=1/4+1/16, angle=180)
grid.piece("coin_back", suit=4, x=3, y=4, z=1/4+1/8+1/16, angle=180)
grid.piece("coin_back", suit=2, x=3, y=1, z=3/4+1/8, angle=90)

One can configure the appearance of the pawns and piece edges
piecepackr can make an exact oblique projection under the same conditions it needs to do an exact orthographic projection (i.e. all pieces laid parallel/perpendicular to the board).
‘op_transform()’ helper function
To directly make oblique projection graphics with grid.piece() the programmer needs to figure out what height (the z argument) the various pieces are in relation to table and carefully arrange the grid.piece() calls so the pieces on top and/or in front are drawn later. An easier way to make oblique projection graphics is to use pmap_piece() with the helper function op_transform() as pmap_piece()‘s trans argument. op_transform() uses heuristics to make educated guesses about the height the game pieces are at and what order they should be drawn in:
library("piecepackr")
df <- tibble::tibble(piece_side="tile_back",
x=c(2,2,2,4,6,6,4,2,5),
y=c(4,4,4,4,4,2,2,2,3))
pmap_piece(df, op_angle=135, op_scale=0.5, trans=op_transform)

Using ‘op_transform()’ with ‘pmap_piece()’ to make educated oblique projection guesses
op_transform() currently uses the following heuristic to guess which game pieces overlap:
- We “bound” each piece’s shape with either a circle or a convex polygon (if the piece’s shape is exactly a circle or a convex polygon this bound is exact).
- If two pieces’ bounding circle/polygon overlap then we guess that the pieces overlap [3].
- Piece “A“‘s “z” value is estimated to be equal to the “z” value of the highest “overlapping” piece beneath “A” (let’s call it “B”) plus half of “B“‘s depth and half of “A“‘s depth. If there are no overlapping pieces beneath “A” its “z” value is just half of “A“‘s depth.
op_transform currently uses the following heuristic to sort the order of drawing game pieces:
- Generalize each “bounding box” from the previous section into a “bounding cube” whose top is equal to the top of the game piece.
- Piece’s whose “bounding cube” top is higher (on “z” axis) are drawn later, for any tie those whose “bounding cube” top (“y” axis) is higher or lower (depending on the “op_angle”) is drawn later, and finally for any remaining tie those whose “bounding cube” is more left or right (depending on the “op_angle”) is drawn later [4].
[3] | For circles and convex polygons any overlap can inferred via an application of the Separating Axis Theorem. |
[4] | In certain scenarios this may give the wrong ordering. An example may be a really tall pawn next to a stack of tiles that altogether are shorter than the pawn. For the common case of a single layer of tiles plus a single layer of pieces on top of the tiles this is very unlikely to have a false indentification. If there is a false identification one can sometimes change the dimensions of the pieces or the “op_angle” value to get a “correct” ordering. In an extreme case one can use tiles, coins, pawns, and dice all 1/2” tall which should always render the correct ordering (assuming their “z” values were correctly estimated). |
Mixed Projections
Technically since each grid.piece() function can have its own op_scale and op_angle arguments one can mix and match projections in a single diagram:
library("piecepackr")
draw_3tiles <- function(x, y, op_angle) {
grid.piece("tile_back", x=x, y=y, z=(1:3)/4-1/8, op_angle=op_angle)
}
draw_3tiles(2, 5, 180)
draw_3tiles(8, 5, 0)
draw_3tiles(5, 2, -90)
draw_3tiles(5, 8, 90)
draw_3tiles(8, 8, 45)
draw_3tiles(2, 8, 135)
draw_3tiles(2, 2, 225)
draw_3tiles(8, 2, -45)

One can technically mix oblique projections within one diagram
3D graphics packages: rayrender, rayvertex, and rgl
rayrender
Note
piece() added in piecepackr v1.3.1.
piece() creates rayrender objects.
library("piecepackr")
library("ppdf")
library("rayrender")
df <- ppdf::piecepack_four_field_kono()
options(piecepackr.cfg = game_systems("dejavu3d")$piecepack,
piecepackr.trans = op_transform)
l <- pmap_piece(df, piece, scale = 0.98, res = 150)
scene <- Reduce(rayrender::add_object, l)
png("rayrender.png", width=6, height=6, units = "in", res=100, type="cairo")
render_scene(scene, lookat = c(2.5, 2.5, 0), lookfrom = c(2.5, -2, 13), clamp_value=8)
invisible(dev.off())

3D render with the rayrender package
rayvertex
Note
piece_mesh() added in piecepackr v1.9.1.
piece_mesh() creates rayvertex objects.
library("piecepackr")
library("ppdf")
library("rayvertex", warn.conflicts = FALSE) # masks rayrender::r_obj
df <- ppdf::piecepack_international_chess()
options(piecepackr.cfg = game_systems("dejavu3d", round=TRUE, pawn="joystick")$piecepack,
piecepackr.trans = op_transform)
l <- pmap_piece(df, piece_mesh, trans=op_transform, scale = 0.98, res = 150, as_top="pawn_face")
table <- sphere_mesh(c(0, 0, -1e3), radius=1e3, material = material_list(diffuse="grey40"))
scene <- Reduce(rayvertex::add_shape, l, init=table)
png("rayvertex.png", width=6, height=6, units = "in", res=100, type="cairo")
rayvertex::rasterize_scene(scene, lookat = c(4.5, 4, 0), lookfrom=c(4.5, -16, 20),
light_info = directional_light(c(5, -7, 7), intensity = 2.5))
invisible(dev.off())

3D render with the rayvertex package
rgl
Note
piece3d() added in piecepackr v1.3.1.
piece3d() draws pieces using the rgl graphic system which leverages OpenGL and WebGL.
library("piecepackr")
library("ppdf")
library("rgl", warn.conflicts = FALSE) # masks rayrender::text3d
invisible(rgl::open3d())
rgl::view3d(phi=-30, zoom = 0.8)
df <- ppdf::piecepack_four_field_kono()
options(piecepackr.cfg = game_systems("dejavu3d")$piecepack,
piecepackr.trans = op_transform)
pmap_piece(df, piece3d, scale = 0.98, res = 150)

3D render with the rgl package
Parallel projections
It is also possible to do a parallel projection with the 3D graphics libraries rayrender, rayvertex, and rgl:
- rayrender
- Set fov = 0 in render_ao() / render_preview / render_scene(). Choose camera size/placement/direction carefully.
- rayvertex
- Set fov = 0 in rasterize_lines() / rasterize_scene(). Choose camera size/placement/direction carefully.
- rgl
- Set view3d(fov = 0). Choose polar coordinates carefully.