piecepackr has some basic support to project 3D board games (like the hundreds of piecepack games) onto a 2D diagram. It can make diagrams with an orthographic or oblique projection using R's grid graphics library. Beginning with piecepackr 1.3 it can also make diagrams using the rgl or rayrender 3D graphics packages.

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")
g.p <- function(...) { grid.piece(..., default.units="in") }
g.p("tile_back", x=0.5+c(3,1,3,1), y=0.5+c(3,3,1,1))
g.p("tile_back", x=0.5+3, y=0.5+1)
g.p("tile_back", x=0.5+3, y=0.5+1)
g.p("die_face", suit=3, rank=5, x=1, y=1)
g.p("pawn_face", x=1, y=4, angle=90)
g.p("coin_back", x=3, y=4, angle=180)
g.p("coin_back", suit=4, x=3, y=4, angle=180)
g.p("coin_back", suit=2, x=3, y=1, angle=90)
```

piecepackr makes an exact "top view" orthographic projection under the following conditions:

- All pieces are placed "flat" parallel 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 (although the "top" view of pyramids is an okay if not exact orthographic projection).
- Any pawns used are (generalized) "meeple" pawns laid face down or face up.

## Oblique projections (grid)

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:

```
g.p <- function(...) { grid.piece(..., op_scale=0.5, op_angle=45, default.units="in") }
g.p("tile_back", x=0.5+c(3,1,3,1), y=0.5+c(3,3,1,1))
g.p("tile_back", x=0.5+3, y=0.5+1, z=1/4+1/8)
g.p("tile_back", x=0.5+3, y=0.5+1, z=2/4+1/8)
g.p("die_face", suit=3, rank=5, x=1, y=1, z=1/4+1/4)
g.p("pawn_face", x=1, y=4, z=1/4+1/8, angle=90)
g.p("coin_back", x=3, y=4, z=1/4+1/16, angle=180)
g.p("coin_back", suit=4, x=3, y=4, z=1/4+1/8+1/16, angle=180)
g.p("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.0` gives you the default "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:

```
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"))
g.p <- function(...) { grid.piece(..., op_scale=0.5, op_angle=45,
cfg=cfg, default.units="in") }
g.p("tile_back", x=0.5+c(3,1,3,1), y=0.5+c(3,3,1,1))
g.p("tile_back", x=0.5+3, y=0.5+1, z=1/4+1/8)
g.p("tile_back", x=0.5+3, y=0.5+1, z=2/4+1/8)
g.p("die_face", suit=3, rank=5, x=1, y=1, z=1/4+1/4)
g.p("pawn_face", x=1, y=4, z=1/4+1/2, angle=90)
g.p("coin_back", x=3, y=4, z=1/4+1/16, angle=180)
g.p("coin_back", suit=4, x=3, y=4, z=1/4+1/8+1/16, angle=180)
g.p("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. pieces all laid flat, no pyramids, pawns must be (genalized) meeple pawns laid face up or down) although it won't attempt to properly draw any visible "side" die faces.

### '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:

```
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, cfg=cfg, default.units="in",
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 [1].
- Piece "A"'s "z" value is estimated to be qual 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 heurist 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 [2].

[1] | For circles and convex polygons any overlap can inferred via an application of the Separating Axis Theorem. |

[2] | 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:

```
cfg <- pp_cfg(list(edge_color.tile="black", border_lex=3))
draw_3tiles <- function(x, y, op_angle) {
grid.piece("tile_back", x=x, y=y, z=(1:3)/4-1/8,
op_scale=0.5, op_angle=op_angle, cfg=cfg, default.units="in")
}
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: rgl and rayrender

piecepackr 1.3 introduced basic support for the rgl and rayrender 3D graphics libraries.

### rgl (piece3d)

`piece3d` draws pieces using the rgl graphic system.

```
library("ppgames", warn.conflicts = FALSE)
library("rgl")
invisible(rgl::open3d())
rgl::view3d(phi=-30, zoom = 0.8)
df <- ppgames::df_four_field_kono()
envir <- game_systems("dejavu3d")
pmap_piece(df, piece3d, trans=op_transform, envir = envir, scale = 0.98, res = 150)
```

### rayrender (piece)

`piece` creates rayrender objects.

```
library("rayrender")
df <- ppgames::df_four_field_kono()
envir <- piecepackr::game_systems("dejavu3d")
l <- pmap_piece(df, piece, trans=op_transform, envir = envir, scale = 0.98, res = 150)
scene <- Reduce(rayrender::add_object, l)
render_scene(scene, lookat = c(2.5, 2.5, 0), lookfrom = c(0, -2, 13))
```

## Pyramids

Currently piecepackr doesn't attempt to do a completely correct orthographic projection for laid down pyramids nor oblique projections of laid down and standing pyramids:

```
library("tibble")
df <- tibble(piece_side = c("tile_back", "pyramid_top", "pyramid_face", "pyramid_left", "pyramid_back", "pyramid_right"),
x = c(2, 2, 2, 1, 2, 3),
y = c(2, 2, 3, 2, 1, 2),
suit = 3, rank = 3, angle = c(0, 0, 0, 90, 180, -90))
pmap_piece(df, grid.piece, default.units="in")
```

```
pmap_piece(df, grid.piece, default.units="in", op_scale = 0.5, trans = op_transform)
```

For best results one should render with a 3D graphics system like rgl or rayrender:

```
library("rayrender")
l <- pmap_piece(df, piece, trans=op_transform, scale = 0.98, res = 150)
scene <- Reduce(rayrender::add_object, l)
render_scene(scene, lookat = c(2.5, 2.5, 0), lookfrom = c(0, -2, 10))
```