#' Display a brickr object as a 3D model #' #' @param brick_list A 3D brickr object from a bricks_from_*() function.. #' @param brick_type Either a 3-height "brick" (default) or 1-height "plate" #' @param lev z-level of 3D model #' @param brick_res Resolution, expressed at number of pixels on one side of a #' 1x1 brick. Defaults to 'sd' (15px). Use 'hd' for 30px per brick, and 'uhd' #' for 60px. Enter a value for a custom resolution. High resolutions take #' longer to render. #' @return A list with elements \code{threed_elevation} and #' \code{threed_hillshade} to created 3D mosiacs with the \code{rayshader} #' package. #' @keywords internal layer_from_bricks <- function(brick_list, brick_type = "brick", lev=1, brick_res = "sd"){ #Get previous data in_list <- brick_list BrickIDs <- in_list$ID_bricks%>% dplyr::filter(Level == lev) img_lego <- in_list$Img_lego %>% dplyr::filter(Level == lev) if(brick_type == 'plate'){ brick_depth = 1L } else { brick_depth = 3L } #Increment elevation - a brick is 3 plates tall up_el = (lev-1)*brick_depth #Number of 'pixels' on a side of a single-stud brick. Set by brick_res. if(is.numeric(brick_res)){ if(brick_res > 100) warning("brick_res capped at 100px per brick.") ex_size <- min(100, abs(round(brick_res))) } else { if(!(brick_res %in% c('sd', 'hd', 'uhd'))) stop("brick_res must be 'sd', 'hd', 'uhd', or a number.") ex_size <- switch(brick_res, sd = 18, hd = 30, uhd = 60) } #Use below is edge calculation # Optimized color only in HD bricks >20 pixels if(ex_size >= 20){ edge_offset <- 0:1 } else { edge_offset <- 0 } #Increase data frame into the correct resolution lego_expand <- img_lego %>% dplyr::select(Level, x, y, Lego_name, Lego_color) %>% dplyr::mutate(stud_id = dplyr::row_number()) lego_expand2 <- expand.grid(x = (min(lego_expand$x)*ex_size):(max(lego_expand$x+1)*ex_size), y = (min(lego_expand$y)*ex_size):(max(lego_expand$y+1)*ex_size)) %>% dplyr::mutate(x_comp = x %/% ex_size, y_comp = y %/% ex_size) %>% dplyr::left_join(lego_expand %>% dplyr::rename(x_comp = x, y_comp = y), by = c("x_comp", "y_comp")) %>% dplyr::left_join(BrickIDs %>% dplyr::select(brick_name, x_comp = x, y_comp = y), by = c("x_comp", "y_comp")) %>% dplyr::select(-x_comp, -y_comp) %>% dplyr::left_join(lego_colors %>% dplyr::select(Lego_name = Color, R_lego, G_lego, B_lego), by = "Lego_name") %>% #Round elevation to nearest 1/height dplyr::mutate(elevation = ifelse(is.na(brick_name),NA, brick_depth + up_el), elevation = ifelse(is.na(Lego_name),NA, elevation)) %>% #Create the edges of bricks... Brick base begins at 0.01 to avoid complete overlap with previous brick dplyr::group_by(brick_name) %>% dplyr::mutate(elevation = dplyr::case_when( x %in% (min(x) + edge_offset) ~ 0.01+up_el, x %in% (max(x) - edge_offset) ~ 0.01+up_el, y %in% (min(y) + edge_offset) ~ 0.01+up_el, y %in% (max(y) - edge_offset) ~ 0.01+up_el, TRUE ~ elevation )) %>% #Remove the bottom corners of brick for bricks with an offset # dplyr::do( # if(ex_size >= 20){ # dplyr::filter(., !((x %in% c(min(x), max(x))) & (y %in% c(min(y), max(y))))) # } else {.} # ) %>% dplyr::ungroup() %>% dplyr::mutate(y = max(y)-y) %>% #Calculate stud placement... radius of 5/8 * (1/2) and height of 0.5 plate dplyr::group_by(stud_id) %>% dplyr::mutate(x_mid = median(x), y_mid = median(y), stud = ((x-x_mid)^2 + (y-y_mid)^2)^(1/2) <= (ex_size * (5/8 * (1/2))), stud_color = dplyr::between(((x-x_mid)^2 + (y-y_mid)^2)^(1/2), (ex_size * (5/8 * (1/2))) - 1, (ex_size * (5/8 * (1/2))) + 1 )) %>% dplyr::ungroup() %>% dplyr::mutate(elevation = ifelse(stud, elevation+0.5, elevation)) %>% #Change color of the stude sics dplyr::mutate_at(dplyr::vars(R_lego, G_lego, B_lego), list(~ifelse(stud_color, .-0.1, .))) %>% dplyr::mutate_at(dplyr::vars(R_lego, G_lego, B_lego), list(~ifelse(. < 0, 0, .))) #Elevation Matrix lego_elmat <- lego_expand2 %>% dplyr::mutate(elevation = ifelse(is.na(Lego_name), NA, elevation)) %>% dplyr::select(x, y, elevation) %>% tidyr::spread(y, elevation) %>% dplyr::select(-x) %>% as.matrix() #Hillshade matrix lego_hillshade_m <- array(dim = c(length(unique(lego_expand2$y)), length(unique(lego_expand2$x)), 3)) lego_expand_color <- lego_expand2 %>% dplyr::group_by(brick_name) %>% #This darkens the edge of each brick, to look like they are separated # The higher the resolution, the dark this should be dplyr::mutate_at(dplyr::vars(R_lego, G_lego, B_lego), list(~ifelse((x == min(x) | y == min(y) | x == max(x) | y == max(y)), . - 0.1, .))) %>% #Darken the upper edge of the bricks. This is important for the HD and UHD dplyr::do( if(ex_size >= 20){ dplyr::mutate_at(., dplyr::vars(R_lego, G_lego, B_lego), list(~ifelse((x == min(x)+(edge_offset+1) | y == min(y)+(edge_offset+1) | x == max(x)-(edge_offset+1) | y == max(y)-(edge_offset+1)), . - 0.05, .))) }else{.}) %>% dplyr::mutate_at(dplyr::vars(R_lego, G_lego, B_lego), list(~ifelse(. < 0, 0, .))) %>% dplyr::ungroup() lego_hillshade_m[,,1] <- lego_expand_color %>% dplyr::select(x, y, R_lego) %>% tidyr::spread(x, R_lego) %>% dplyr::select(-y) %>% as.matrix() lego_hillshade_m[,,2] <- lego_expand_color %>% dplyr::select(x, y, G_lego) %>% tidyr::spread(x, G_lego) %>% dplyr::select(-y) %>% as.matrix() lego_hillshade_m[,,3] <- lego_expand_color %>% dplyr::select(x, y, B_lego) %>% tidyr::spread(x, B_lego) %>% dplyr::select(-y) %>% as.matrix() #Return in_list[["threed_elevation"]] <- lego_elmat in_list[["threed_hillshade"]] <- lego_hillshade_m in_list[["brick_resolution"]] <- ex_size return(in_list) } #' Build 3D brick model with rayshader. #' #' @param brick_list List output from collect_bricks(). Contains an element \code{Img_lego}. #' @param brick_type Type of brick to use. Default is 'brick'. Other option is 'plate', which is 1/3 the height of a brick. #' @param view_levels Numeric array of Levels/z values to display. Leave as \code{NULL} to include all. #' @param brick_res Resolution, expressed at number of pixels on one side of a 1x1 brick. Defaults to 'sd' (15px). Use 'hd' for 30px per brick, and 'uhd' for 60px. #' Enter a value for a custom resolution. High resolutions take longer to render. #' @param solidcolor Hex color of mosaic base. Only renders on bottom. #' @param water Default 'FALSE'. If 'TRUE', a water layer is rendered. #' @param waterdepth Default '0'. Water level. #' @param ... All other inputs from rayshader::plot_3d() EXCEPT \code{hillshade}, \code{soliddepth}, \code{zscale}, and \code{shadow}. #' @examples \dontrun{ #' #This is a brick #'brick <- data.frame( #' Level="A", #' X1 = rep(3,4), #The number 3 is the brickrID for 'bright red' #' X2 = rep(3,4) #') #' #'brick %>% #' bricks_from_table() %>% #' build_bricks() #' } #' @return 3D brick model rendered in the 'rayshader' package. #' @family 3D Models #' @export #' build_bricks_rayshader <- function(brick_list, brick_type = "brick", brick_res = "sd", view_levels = NULL, solidcolor = "#a3a2a4", water = FALSE, waterdepth = 0, ...){ #Requires Rayshader if (!requireNamespace("rayshader", quietly = TRUE)) { stop("Package \"rayshader\" needed for this function to work. Please install it.", call. = FALSE) } #Get previous data in_list <- brick_list BrickIDs <- in_list$ID_bricks img_lego <- in_list$Img_lego if(is.null(view_levels)){ view_levels <- unique(img_lego$Level) } for(ii in view_levels){ brick_layer <- brick_list %>% layer_from_bricks(ii, brick_type = brick_type, brick_res = brick_res) if(ii == min(view_levels) & water){ brick_layer$`threed_hillshade`%>% rayshader::plot_3d(brick_layer$`threed_elevation`, zscale=0.167*(15/brick_layer$`brick_resolution`), solid = FALSE, solidcolor=solidcolor, shadow = FALSE, water = TRUE, waterdepth = waterdepth * 3, ...) } else { brick_layer$`threed_hillshade`%>% rayshader::plot_3d(brick_layer$`threed_elevation`, zscale=0.167*(15/brick_layer$`brick_resolution`), solid = FALSE, solidcolor=solidcolor, shadow = FALSE, water = FALSE, waterdepth = 0, ...) } } }