::install_github("ryantimpe/brickr") remotes
tl;dr
You can create 3D Lego models with {brickr}. I made some models of soccer players.
Virtual Lego
{brickr} is a fun package by Ryan Timpe that lets you build 2D mosaics and 3D models with Lego-like virtual bricks,1 with a little help from Tyler Morgan Wall’s {rayshader} package.
You can get started with the brickr toybox, which lets you arrange bricks in a spreadsheet that {brickr} can turn into a 3D model.
Kick-off
First, we install {brickr} from GitHub.2
And attach it along with some tidyverse packages.
suppressPackageStartupMessages({
library(brickr)
library(dplyr)
library(tibble)
})
I’ve written a function called create_brickr_player()
that lets you build a soccer player and select the brick colours for the shirt, socks, and much more. It lets you create the same model but change the brick colours with minimum fuss.
The function is simple. It helps you create a data frame that specifies the location and colour of individual bricks on successive 2D planes to build up a 3D model.
This data frame is a plan that can be interpreted by {brickr} and transformed into a special list that can be rendered in 3D space.
Click for the function definition.
Note that the arguments must all be numeric codes as per brickr::lego_colors()
.
<- function(
create_brickr_player hair_col = 34,
skin_col = 5,
boot_col = 6,
shirt_body_col = 3,
shirt_sleeve_col = 1,
shorts_col = 1,
sock_col = 3,
sock_trim_col = 1
){
::tribble(
tibble
~"Level", ~`1`, ~`2`, ~`3`, ~`4`, ~`5`,
"A", 0, 0, 0, 0, 0,
"A", 0, boot_col, 0, boot_col, 0,
"A", 0, boot_col, 0, boot_col,0,
"B", 0, 0, 0, 0, 0,
"B", 0, sock_col, 0, sock_col, 0,
"B", 0, 0, 0, 0, 0,
"C", 0, 0, 0, 0, 0,
"C", 0, sock_trim_col, 0, sock_trim_col, 0,
"C", 0, 0, 0, 0, 0,
"D", 0, 0, 0, 0, 0,
"D", 0, skin_col, 0, skin_col, 0,
"D", 0, 0, 0, 0, 0,
"E", 0, shorts_col, 0, shorts_col, 0,
"E", 0, shorts_col, 0, shorts_col, 0,
"E", 0, shorts_col, 0, shorts_col, 0,
"F", 0, shorts_col, shorts_col, shorts_col, 0,
"F", 0, shorts_col, shorts_col, shorts_col, 0,
"F", 0, shorts_col, shorts_col, shorts_col, 0,
"G", 0, shirt_body_col, shirt_body_col, shirt_body_col, 0,
"G", skin_col, shirt_body_col, shirt_body_col, shirt_body_col, skin_col,
"G", 0, shirt_body_col, shirt_body_col, shirt_body_col, 0,
"H", 0, shirt_body_col, shirt_body_col, shirt_body_col, 0,
"H", skin_col, shirt_body_col, shirt_body_col, shirt_body_col, skin_col,
"H", 0, shirt_body_col, shirt_body_col, shirt_body_col, 0,
"I", 0, shirt_body_col, shirt_body_col, shirt_body_col, 0,
"I", skin_col, shirt_body_col, shirt_body_col, shirt_body_col, skin_col,
"I", 0, shirt_body_col, shirt_body_col, shirt_body_col, 0,
"J", 0, shirt_body_col, shirt_body_col, shirt_body_col, 0,
"J", shirt_sleeve_col, shirt_body_col, shirt_body_col, shirt_body_col, shirt_sleeve_col,
"J", 0, shirt_body_col, shirt_body_col, shirt_body_col, 0,
"K", 0, shirt_body_col, shirt_body_col, shirt_body_col, 0,
"K", shirt_sleeve_col, shirt_sleeve_col, shirt_body_col, shirt_sleeve_col, shirt_sleeve_col,
"K", 0, shirt_body_col, shirt_body_col, shirt_body_col, 0,
"L", 0, 0, 0, 0, 0,
"L", 0, 0, skin_col, 0, 0,
"L", 0, 0, 0, 0, 0,
"M", 0, skin_col, skin_col, skin_col, 0,
"M", 0, skin_col, skin_col, skin_col, 0,
"M", 0, skin_col, skin_col, skin_col, 0,
"N", 0, hair_col, hair_col, hair_col, 0,
"N", 0, hair_col, skin_col, hair_col, 0,
"N", 0, skin_col, skin_col, skin_col, 0,
"O", 0, hair_col, hair_col, hair_col, 0,
"O", 0, hair_col, hair_col, hair_col, 0,
"O", 0, hair_col, hair_col, hair_col, 0
)
}
Here’s what happens when you use the function with default arguments.
<- create_brickr_player()
player_plan # preview the object player_plan
# A tibble: 45 × 6
Level `1` `2` `3` `4` `5`
<chr> <dbl> <dbl> <dbl> <dbl> <dbl>
1 A 0 0 0 0 0
2 A 0 6 0 6 0
3 A 0 6 0 6 0
4 B 0 0 0 0 0
5 B 0 3 0 3 0
6 B 0 0 0 0 0
7 C 0 0 0 0 0
8 C 0 1 0 1 0
9 C 0 0 0 0 0
10 D 0 0 0 0 0
# ℹ 35 more rows
Each layer of bricks gets a separate value in the Level
column. The x-dimension is represented by the rows of the data frame and the y-dimension by the numbered columns.
Every non-zero number represents a brick and each value represents a different colour. For example, layer A has dimensions of 3 x 5 where 4 spots will be filled with a brick. Each of these has the value ‘2’, which encodes the colour black. Layer B, meanwhile, has a couple of bricks with value ‘7’, which is bright red.
How do you know which numbers encode which colours? You can access the codes from the lego_colors
data frame in the {brickr} package.
select(lego_colors, brickrID, Color, hex) # colour codes
# A tibble: 54 × 3
brickrID Color hex
<int> <chr> <chr>
1 1 White #F4F4F4
2 2 Brick yellow #CCB98D
3 3 Bright red #B40000
4 4 Bright blue #1E5AA8
5 5 Bright yellow #FAC80A
6 6 Black #1B2A34
7 7 Dark green #00852B
8 8 Reddish brown #5F3109
9 9 Medium stone grey #969696
10 10 Dark stone grey #646464
# ℹ 44 more rows
Click for full brick colour codes.
%>%
lego_colors mutate(
hex = cell_spec(
hex, "html",
background = factor(brickrID, lego_colors$brickrID, lego_colors$hex)
)%>%
) select(`Colour ID` = brickrID, Colour = Color, `Hex code` = hex) %>%
kable(format = "html", escape = FALSE) %>%
kable_styling("striped", full_width = TRUE)
Colour ID | Colour | Hex code |
---|---|---|
1 | White | #F4F4F4 |
2 | Brick yellow | #CCB98D |
3 | Bright red | #B40000 |
4 | Bright blue | #1E5AA8 |
5 | Bright yellow | #FAC80A |
6 | Black | #1B2A34 |
7 | Dark green | #00852B |
8 | Reddish brown | #5F3109 |
9 | Medium stone grey | #969696 |
10 | Dark stone grey | #646464 |
11 | Nougat | #BB805A |
12 | Bright green | #58AB41 |
13 | Medium blue | #7396C8 |
14 | Bright orange | #D67923 |
15 | Br. yellowish green | #A5CA18 |
16 | Earth blue | #19325A |
17 | Earth green | #00451A |
18 | Dark red | #720012 |
19 | Bright purple | #C8509B |
20 | Light purple | #FF9ECD |
21 | Medium azur | #68C3E2 |
22 | Medium lavender | #9A76AE |
23 | Dark orange | #91501C |
24 | Bright bluish green | #009894 |
25 | Bright reddish violet | #901F76 |
26 | Sand blue | #70819A |
27 | Sand yellow | #897D62 |
28 | Sand green | #708E7C |
29 | Flame yellowish orange | #FCAC00 |
30 | Light royal blue | #9DC3F7 |
31 | Cool yellow | #FFEC6C |
32 | Medium lilac | #441A91 |
33 | Light nougat | #E1BEA1 |
34 | Dark brown | #352100 |
35 | Medium nougat | #AA7D55 |
36 | Dark azur | #469BC3 |
37 | Aqua | #D3F2EA |
38 | Lavender | #CDA4DE |
39 | Spring yellowish green | #E2F99A |
40 | Olive green | #8B844F |
41 | Vibrant coral | #F06D78 |
42 | Transparent | #EEEEEE |
43 | Tr. red | #B80000 |
44 | Tr. light blue | #ADDDED |
45 | Tr. blue | #0085B8 |
46 | Tr. yellow | #FFE622 |
47 | Tr. green | #73B464 |
48 | Tr. fl green | #FAF15B |
49 | Tr. brown | #BBB29E |
50 | Tr. bright orange | #E18D0A |
51 | Tr. fl red orange | #CB4E29 |
52 | Tr. medium violet | #FD8ECF |
53 | Tr. bright violet | #6F7AB8 |
54 | Tr. bright green | #AFD246 |
So ‘1’ is white, ‘2’ is black and so on. I think Timpe selected this set of colours to match the colours available from Lego sets.
Boring, boring Arsenal
To actually build the model, pass the data frame to a couple of {brickr} functions.
The first is bricks_from_table()
that converts the data frame to a list containing several elements that define the required bricks and colours.
# Convert plan to list with brick types and colours
<- player_plan %>%
player_bricks bricks_from_table()
names(player_bricks) # see the element names
[1] "Img_lego" "brickr_object" "Img_bricks" "ID_bricks"
[5] "pieces" "use_bricks"
As a side note, you can use display_pieces()
to find out the set of pieces you’ll need to recreate the model in real life!
build_pieces(player_bricks)
Pass the list object to the display_bricks()
function to get the plan rendered into 3D. This opens a new device window and the model will be built up layer by layer. When complete, you can use your mouse to click and drag the object to look at at from all directions.
build_bricks(player_bricks) # opens separate window
So the default set builds up to make a player that has red socks with white trim, white shorts, and a red shirt with white sleeves. An Arsenal player, of course.3
Show your support
To change the colour of the player’s shirt you just need to change all the bricks associated with the shirt. This could be tedious by hand, so create_brickr_player()
has an argument to do exactly this. Set shirt_body_col
to ‘6’ to make it bright blue, for example.
You can change more than the shirt colour. Here’s the current set of arguments:
shirt_body_col
andshirt_sleeve_col
shorts_col
sock_col
andsock_trim_col
boot_col
hair_col
andskin_col
So you could create a Manchester City player with the following:
# Build player plan with certain colours
<- create_brickr_player(
man_city hair_col = 6, # Black
skin_col = 34, # Dark brown
boot_col = 3, # Bright red
shirt_body_col = 30, # Light royal blue
shirt_sleeve_col = 30, # Light royal blue
shorts_col = 1, # White
sock_col = 16, # Earth blue
sock_trim_col = 16 # Earth blue
)
# Convert plan to list and render it
%>%
man_city bricks_from_table() %>%
display_bricks()
In fact, this is a faithful rendering of the 2019 Premier League winner, FA Cup winner, League Cup winner, PFA Team of the Year inductee, PFA Young Player of the Year and FWA Footballer of the Year Raheem Sterling. Obviously.
I’ve added a couple more to a GitHub Gist. Feel free to add more.
Extra-time
Hopefully this is useful for anyone who wants to create the same {brickr} model in multiple colours. I realise that might be a niche audience.
The obvious next step would be to allow for features of the plan to change. For example, you could set an argument for player_height
and add or remove layers from the plan to make the final model taller or shorter. Or maybe different shirt types could be specified, like horizontal_stripe = TRUE
.
Pull requests always welcome!
Environment
Session info
Last rendered: 2023-08-01 18:53:57 BST
R version 4.3.1 (2023-06-16)
Platform: aarch64-apple-darwin20 (64-bit)
Running under: macOS Ventura 13.2.1
Matrix products: default
BLAS: /Library/Frameworks/R.framework/Versions/4.3-arm64/Resources/lib/libRblas.0.dylib
LAPACK: /Library/Frameworks/R.framework/Versions/4.3-arm64/Resources/lib/libRlapack.dylib; LAPACK version 3.11.0
locale:
[1] en_US.UTF-8/en_US.UTF-8/en_US.UTF-8/C/en_US.UTF-8/en_US.UTF-8
time zone: Europe/London
tzcode source: internal
attached base packages:
[1] stats graphics grDevices utils datasets methods base
other attached packages:
[1] tibble_3.2.1 dplyr_1.1.2 brickr_0.3.5 kableExtra_1.3.4
[5] knitr_1.43.1
loaded via a namespace (and not attached):
[1] gtable_0.3.3 jsonlite_1.8.7 compiler_4.3.1 webshot_0.5.5
[5] tidyselect_1.2.0 xml2_1.3.5 stringr_1.5.0 tidyr_1.3.0
[9] systemfonts_1.0.4 scales_1.2.1 yaml_2.3.7 fastmap_1.1.1
[13] ggplot2_3.4.2 R6_2.5.1 labeling_0.4.2 generics_0.1.3
[17] htmlwidgets_1.6.2 munsell_0.5.0 svglite_2.1.1 pillar_1.9.0
[21] rlang_1.1.1 utf8_1.2.3 stringi_1.7.12 xfun_0.39
[25] viridisLite_0.4.2 cli_3.6.1 withr_2.5.0 magrittr_2.0.3
[29] grid_4.3.1 digest_0.6.33 rvest_1.0.3 rstudioapi_0.15.0
[33] lifecycle_1.0.3 vctrs_0.6.3 evaluate_0.21 glue_1.6.2
[37] farver_2.1.1 fansi_1.0.4 colorspace_2.1-0 rmarkdown_2.23
[41] purrr_1.0.1 httr_1.4.6 tools_4.3.1 pkgconfig_2.0.3
[45] htmltools_0.5.5
Footnotes
Not an official product (yet).↩︎
The package was on CRAN when this post was originally written, but it was later removed.↩︎
Why Arsenal? Mostly to demonstrate that sleeves can be a different colour to the shirt body, but also because they just got binned 4-1 by Chelsea in the Europa League final and I feel sorry for them.↩︎