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)
```

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)
```

`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)
```

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)
```

`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)
```

## 3D graphics packages: rayrender, rayvertex, and rgl

### rayrender

Note

piece() added in piecepackr v1.3.1.

`piece()` creates rayrender objects.

```
library("piecepackr")
library("ppgames")
library("rayrender")
df <- ppgames::df_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())
```

### rayvertex

Note

piece_mesh() added in piecepackr v1.9.1.

`piece_mesh()` creates rayvertex objects.

```
library("piecepackr")
library("ppgames")
library("rayvertex", warn.conflicts = FALSE) # masks rayrender::r_obj
df <- ppgames::df_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())
```

### 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("ppgames")
library("rgl", warn.conflicts = FALSE) # masks rayrender::text3d
invisible(rgl::open3d())
rgl::view3d(phi=-30, zoom = 0.8)
df <- ppgames::df_four_field_kono()
options(piecepackr.cfg = game_systems("dejavu3d")$piecepack,
piecepackr.trans = op_transform)
pmap_piece(df, piece3d, scale = 0.98, res = 150)
```

### 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.