Permalink
Cannot retrieve contributors at this time
Join GitHub today
GitHub is home to over 40 million developers working together to host and review code, manage projects, and build software together.
Sign up
Fetching contributors…

#' Render Scene | |
#' | |
#' Takes the scene description and renders an image, either to the device or to a filename. | |
#' | |
#' @param scene Tibble of object locations and properties. | |
#' @param width Default `400`. Width of the render, in pixels. | |
#' @param height Default `400`. Height of the render, in pixels. | |
#' @param fov Default `20`. Field of view, in degrees. If this is zero, the camera will use an orthographic projection. The size of the plane | |
#' used to create the orthographic projection is given in argument `ortho_dimensions`. | |
#' @param samples Default `100`. Number of samples for each pixel. | |
#' @param ambient_light Default `FALSE`, unless there are no emitting objects in the scene. | |
#' If `TRUE`, the background will be a gradient varying from `backgroundhigh` directly up (+y) to | |
#' `backgroundlow` directly down (-y). | |
#' @param lookfrom Default `c(0,1,10)`. Location of the camera. | |
#' @param lookat Default `c(0,0,0)`. Location where the camera is pointed. | |
#' @param camera_up Default `c(0,1,0)`. Vector indicating the "up" position of the camera. | |
#' @param aperture Default `0.1`. Aperture of the camera. Smaller numbers will increase depth of field, causing | |
#' less blurring in areas not in focus. | |
#' @param clamp_value Default `Inf`. If a bright light or a reflective material is in the scene, occasionally | |
#' there will be bright spots that will not go away even with a large number of samples. These | |
#' can be removed (at the cost of slightly darkening the image) by setting this to a small number greater than 1. | |
#' @param filename Default `NULL`. If present, the renderer will write to the filename instead | |
#' of the current device. | |
#' @param backgroundhigh Default `#80b4ff`. The "high" color in the background gradient. Can be either | |
#' a hexadecimal code, or a numeric rgb vector listing three intensities between `0` and `1`. | |
#' @param backgroundlow Default `#ffffff`. The "low" color in the background gradient. Can be either | |
#' a hexadecimal code, or a numeric rgb vector listing three intensities between `0` and `1`. | |
#' @param shutteropen Default `0`. Time at which the shutter is open. Only affects moving objects. | |
#' @param shutterclose Default `1`. Time at which the shutter is open. Only affects moving objects. | |
#' @param focal_distance Default `NULL`, automatically set to the `lookfrom-lookat` distance unless | |
#' otherwise specified. | |
#' @param ortho_dimensions Default `c(1,1)`. Width and height of the orthographic camera. Will only be used if `fov = 0`. | |
#' @param tonemap Default `gamma`. Choose the tone mapping function, | |
#' Default `gamma` solely adjusts for gamma and clamps values greater than 1 to 1. | |
#' `reinhold` scales values by their individual color channels `color/(1+color)` and then performs the | |
#' gamma adjustment. `uncharted` uses the mapping developed for Uncharted 2 by John Hable. `hbd` uses an | |
#' optimized formula by Jim Hejl and Richard Burgess-Dawson. | |
#' @param environment_light Default `NULL`. An image to be used for the background for rays that escape | |
#' the scene. Supports both HDR (`.hdr`) and low-dynamic range (`.png`, `.jpg`) images. | |
#' @param rotate_env Default `0`. The number of degrees to rotate the environment map around the scene. | |
#' @param parallel Default `FALSE`. If `TRUE`, it will use all available cores to render the image | |
#' (or the number specified in `options("cores")` if that option is not `NULL`). | |
#' @param progress Default `TRUE` if interactive session, `FALSE` otherwise. | |
#' @param debug Default `NULL`. If `bvh`, will return an image indicated the number of BVH lookups. | |
#' @export | |
#' @importFrom grDevices col2rgb | |
#' @return Raytraced plot to current device, or an image saved to a file. | |
#' | |
#' @examples | |
#' #Generate a large checkered sphere as the ground | |
#' scene = generate_ground(depth=-0.5, material = diffuse(color="white", checkercolor="darkgreen")) | |
#' \donttest{ | |
#' render_scene(scene,parallel=TRUE,samples=500) | |
#' } | |
#' | |
#' #Add a sphere to the center | |
#' scene = scene %>% | |
#' add_object(sphere(x=0,y=0,z=0,radius=0.5,material = diffuse(color=c(1,0,1)))) | |
#' \donttest{ | |
#' render_scene(scene,fov=20,parallel=TRUE,samples=500) | |
#' } | |
#' | |
#' #Add a marbled cube | |
#' scene = scene %>% | |
#' add_object(cube(x=1.1,y=0,z=0,material = diffuse(noise=3))) | |
#' \donttest{ | |
#' render_scene(scene,fov=20,parallel=TRUE,samples=500) | |
#' } | |
#' | |
#' #Add a metallic gold sphere | |
#' scene = scene %>% | |
#' add_object(sphere(x=-1.1,y=0,z=0,radius=0.5,material = metal(color="gold",fuzz=0.1))) | |
#' \donttest{ | |
#' render_scene(scene,fov=20,parallel=TRUE,samples=500) | |
#' } | |
#' | |
#' #Lower the number of samples to render more quickly (here, we also use only one core). | |
#' render_scene(scene, samples=4) | |
#' | |
#' #Add a floating R plot using the iris dataset as a png onto a floating 2D rectangle | |
#' | |
#' \donttest{ | |
#' tempfileplot = tempfile() | |
#' png(filename=tempfileplot,height=400,width=800) | |
#' plot(iris$Petal.Length,iris$Sepal.Width,col=iris$Species,pch=18,cex=4) | |
#' dev.off() | |
#' | |
#' image_array = aperm(png::readPNG(tempfileplot),c(2,1,3)) | |
#' scene = scene %>% | |
#' add_object(xy_rect(x=0,y=1.1,z=0,xwidth=2,angle = c(0,180,0), | |
#' material = diffuse(image = image_array))) | |
#' render_scene(scene,fov=20,parallel=TRUE,samples=500) | |
#' } | |
#' | |
#' #Move the camera | |
#' \donttest{ | |
#' render_scene(scene,lookfrom = c(7,1.5,10),lookat = c(0,0.5,0),fov=15,parallel=TRUE) | |
#' } | |
#' | |
#' #Change the background gradient to a night time ambiance | |
#' \donttest{ | |
#' render_scene(scene,lookfrom = c(7,1.5,10),lookat = c(0,0.5,0),fov=15, | |
#' backgroundhigh = "#282375", backgroundlow = "#7e77ea", parallel=TRUE, | |
#' samples=500) | |
#' } | |
#' | |
#'#Increase the aperture to blur objects that are further from the focal plane. | |
#' \donttest{ | |
#' render_scene(scene,lookfrom = c(7,1.5,10),lookat = c(0,0.5,0),fov=15, | |
#' aperture = 0.5,parallel=TRUE,samples=500) | |
#' } | |
#' | |
#'#Spin the camera around the scene, decreasing the number of samples to render faster. To make | |
#'#an animation, specify the a filename in `render_scene` for each frame and use the `av` package | |
#'#or ffmpeg to combine them all into a movie. | |
#' | |
#'t=1:30 | |
#'xpos = 10 * sin(t*12*pi/180+pi/2) | |
#'zpos = 10 * cos(t*12*pi/180+pi/2) | |
#'\donttest{ | |
#'#Save old par() settings | |
#'old.par = par(no.readonly = TRUE) | |
#'on.exit(par(old.par)) | |
#'par(mfrow=c(5,6)) | |
#'for(i in 1:30) { | |
#' render_scene(scene, samples=5, | |
#' lookfrom = c(xpos[i],1.5,zpos[i]),lookat = c(0,0.5,0), parallel=TRUE) | |
#'} | |
#'} | |
render_scene = function(scene, width = 400, height = 400, fov = 20, samples = 100, ambient_light = FALSE, | |
lookfrom = c(0,1,10), lookat = c(0,0,0), camera_up = c(0,1,0), | |
aperture = 0.1, clamp_value = Inf, | |
filename = NULL, backgroundhigh = "#80b4ff",backgroundlow = "#ffffff", | |
shutteropen = 0.0, shutterclose = 1.0, focal_distance=NULL, ortho_dimensions = c(1,1), | |
tonemap ="gamma", parallel=TRUE, | |
environment_light = NULL, rotate_env = 0, | |
progress = interactive(), debug = NULL) { | |
#Check if Cornell Box scene and set camera if user did not: | |
if(!is.null(attr(scene,"cornell"))) { | |
corn_message = "Setting default values for Cornell box: " | |
missing_corn = FALSE | |
if(missing(lookfrom)) { | |
lookfrom = c(278, 278, -800) | |
corn_message = paste0(corn_message, "lookfrom `c(278,278,-800)` ") | |
missing_corn = TRUE | |
} | |
if(missing(lookat)) { | |
lookat = c(278, 278, 0) | |
corn_message = paste0(corn_message, "lookat `c(278,278,0)` ") | |
missing_corn = TRUE | |
} | |
if(missing(fov)) { | |
fov=40 | |
corn_message = paste0(corn_message, "fov `40` ") | |
missing_corn = TRUE | |
} | |
if(fov == 0 && missing(ortho_dimensions)) { | |
ortho_dimensions = c(580,580) | |
corn_message = paste0(corn_message, "ortho_dimensions `c(580, 580)` ") | |
missing_corn = TRUE | |
} | |
corn_message = paste0(corn_message,".") | |
if(missing_corn) { | |
message(corn_message) | |
} | |
} | |
lookvec = (lookat - lookfrom) | |
i1 = c(2,3,1) | |
i2 = c(3,1,2) | |
if(all(lookvec[i1]*camera_up[i2] - lookvec[i2]*camera_up[i1] == 0)) { | |
stop("camera_up value c(", paste(camera_up, collapse=","), ") is aligned exactly with camera vector (lookat - lookfrom). Choose a different value for camera_up.") | |
} | |
backgroundhigh = convert_color(backgroundhigh) | |
backgroundlow = convert_color(backgroundlow) | |
xvec = scene$x | |
yvec = scene$y | |
zvec = scene$z | |
rvec = scene$radius | |
shapevec = unlist(lapply(tolower(scene$shape),switch, | |
"sphere" = 1,"xy_rect" = 2, "xz_rect" = 3,"yz_rect" = 4,"box" = 5, "triangle" = 6, | |
"obj" = 7, "objcolor" = 8, "disk" = 9, "cylinder" = 10, "ellipsoid" = 11)) | |
typevec = unlist(lapply(tolower(scene$type),switch, | |
"diffuse" = 1,"metal" = 2,"dielectric" = 3, "oren-nayar" = 4, "light" = 5)) | |
sigmavec = unlist(scene$sigma) | |
assertthat::assert_that(tonemap %in% c("gamma","reinhold","uncharted", "hbd")) | |
toneval = switch(tonemap, "gamma" = 1,"reinhold" = 2,"uncharted" = 3,"hbd" = 4) | |
movingvec = purrr::map_lgl(scene$velocity,.f = ~any(.x != 0)) | |
proplist = scene$properties | |
vel_list = scene$velocity | |
checkeredlist = scene$checkercolor | |
checkeredbool = purrr::map_lgl(checkeredlist,.f = ~all(!is.na(.x))) | |
#noise handler | |
noisebool = purrr::map_lgl(scene$noise, .f = ~.x > 0) | |
noisevec = scene$noise | |
noisephasevec = scene$noisephase * pi/180 | |
noiseintvec = scene$noiseintensity | |
noisecolorlist = scene$noisecolor | |
#rotation handler | |
rot_angle_list = scene$angle | |
#fog handler | |
fog_bool = scene$fog | |
fog_vec = scene$fogdensity | |
#flip handler | |
flip_vec = scene$flipped | |
#light handler | |
light_prop_vec = scene$lightintensity | |
if(!any(typevec == 5) && missing(ambient_light) && missing(environment_light)) { | |
ambient_light = TRUE | |
} | |
#texture handler | |
image_array_list = scene$image | |
image_tex_bool = purrr::map_lgl(image_array_list,.f = ~is.array(.x)) | |
temp_file_names = purrr::map_chr(image_tex_bool,.f = ~ifelse(.x, tempfile(),"")) | |
for(i in 1:length(image_array_list)) { | |
if(image_tex_bool[i]) { | |
if(dim(image_array_list[[i]])[3] == 4) { | |
png::writePNG(fliplr(aperm(image_array_list[[i]][,,1:3],c(2,1,3))),temp_file_names[i]) | |
} else if(dim(image_array_list[[i]])[3] == 3){ | |
png::writePNG(fliplr(aperm(image_array_list[[i]],c(2,1,3))),temp_file_names[i]) | |
} | |
} | |
} | |
#movement handler | |
if(shutteropen == shutterclose) { | |
movingvec = rep(FALSE,length(movingvec)) | |
} | |
#implicit sampling handler | |
implicit_vec = scene$implicit_sample | |
#order rotation handler | |
order_rotation_list = scene$order_rotation | |
#group handler | |
group_bool = purrr::map_lgl(scene$pivot_point,.f = ~all(!is.na(.x))) | |
group_pivot = scene$pivot_point | |
group_angle = scene$group_angle | |
group_order_rotation = scene$group_order_rotation | |
group_translate = scene$group_translate | |
group_scale = scene$group_scale | |
#triangle normal handler | |
tri_normal_bools = purrr::map2_lgl(shapevec,proplist,.f = ~.x == 6 && all(!is.na(.y))) | |
tri_color_vert = scene$tricolorinfo | |
is_tri_color = purrr::map_lgl(tri_color_vert,.f = ~all(!is.na(.x))) | |
#obj handler | |
fileinfovec = scene$fileinfo | |
fileinfovec[is.na(fileinfovec)] = "" | |
objfilenamevec = purrr::map_chr(fileinfovec, path.expand) | |
if(any(!file.exists(objfilenamevec) & nchar(objfilenamevec) > 0)) { | |
stop(paste0("Cannot find the following .obj files:\n", | |
paste(objfilenamevec[!file.exists(objfilenamevec) & nchar(objfilenamevec) > 0], | |
collapse="\n") | |
)) | |
} | |
objbasedirvec = purrr::map_chr(objfilenamevec, dirname) | |
#bg image handler | |
if(!is.null(environment_light)) { | |
hasbackground = TRUE | |
backgroundstring = path.expand(environment_light) | |
if(!file.exists(environment_light)) { | |
hasbackground = FALSE | |
warning("file '", environment_light, "' cannot be found, not using background image.") | |
} | |
} else { | |
hasbackground = FALSE | |
backgroundstring = "" | |
} | |
#scale handler | |
scale_factor = scene$scale_factor | |
assertthat::assert_that(all(c(length(xvec),length(yvec),length(zvec),length(rvec),length(typevec),length(proplist)) == length(xvec))) | |
assertthat::assert_that(all(!is.null(typevec))) | |
for(i in 1:length(xvec)) { | |
if(typevec[i] == 1) { | |
if(shapevec[i] == 1) { | |
# assertthat::assert_that(length(proplist[[i]]) == 3) | |
} | |
} else if (typevec[i] == 2) { | |
# assertthat::assert_that(length(proplist[[i]]) == 4) | |
} else if (typevec[i] == 3) { | |
# assertthat::assert_that(length(proplist[[i]]) == 7) | |
} | |
} | |
assertthat::assert_that(length(lookfrom) == 3) | |
assertthat::assert_that(length(lookat) == 3) | |
if(is.null(focal_distance)) { | |
focal_distance = sqrt(sum((lookfrom-lookat)^2)) | |
} | |
if(!is.null(options("cores")[[1]])) { | |
numbercores = options("cores")[[1]] | |
} else { | |
numbercores = parallel::detectCores() | |
} | |
if(is.null(debug)) { | |
debugval = 0 | |
} else if(debug == "bvh") { | |
debugval = 1 | |
} else { | |
debugval = 0 | |
} | |
if(fov == 0) { | |
assertthat::assert_that(length(ortho_dimensions) == 2) | |
} | |
rgb_mat = render_scene_rcpp(nx = width, ny = height, ns = samples, fov = fov, ambient_light = ambient_light, | |
lookfromvec = lookfrom, lookatvec = lookat, aperture=aperture,camera_up = camera_up, | |
type = typevec, shape = shapevec, radius = rvec, | |
x = xvec, y = yvec, z = zvec, | |
properties = proplist, velocity = vel_list, moving = movingvec, | |
n = length(typevec), | |
bghigh = backgroundhigh, bglow = backgroundlow, | |
shutteropen = shutteropen, shutterclose = shutterclose, | |
ischeckered = checkeredbool, checkercolors = checkeredlist, | |
noise=noisevec,isnoise=noisebool,noisephase=noisephasevec, | |
noiseintensity=noiseintvec, noisecolorlist = noisecolorlist, | |
angle = rot_angle_list, isimage = image_tex_bool, filelocation = temp_file_names, | |
lightintensity = light_prop_vec,isflipped = flip_vec, | |
focus_distance=focal_distance, | |
isvolume=fog_bool, voldensity = fog_vec , parallel=parallel, | |
implicit_sample = implicit_vec, order_rotation_list = order_rotation_list, clampval = clamp_value, | |
isgrouped = group_bool, group_pivot=group_pivot, group_translate = group_translate, | |
group_angle = group_angle, group_order_rotation = group_order_rotation, group_scale = group_scale, | |
tri_normal_bools = tri_normal_bools, is_tri_color = is_tri_color, tri_color_vert= tri_color_vert, | |
fileinfo = objfilenamevec, filebasedir = objbasedirvec, toneval = toneval, | |
progress_bar = progress, numbercores = numbercores, debugval = debugval, | |
hasbackground = hasbackground, background = backgroundstring, scale_list = scale_factor, | |
ortho_dimensions = ortho_dimensions, sigmavec = sigmavec, rotate_env = rotate_env) | |
full_array = array(0,c(ncol(rgb_mat$r),nrow(rgb_mat$r),3)) | |
full_array[,,1] = t(rgb_mat$r) | |
full_array[,,2] = t(rgb_mat$g) | |
full_array[,,3] = t(rgb_mat$b) | |
array_from_mat = array(full_array,dim=c(nrow(full_array),ncol(full_array),3)) | |
if(any(is.na(array_from_mat ))) { | |
array_from_mat[is.na(array_from_mat)] = 0 | |
} | |
if(any(array_from_mat > 1 | array_from_mat < 0,na.rm = TRUE)) { | |
array_from_mat[array_from_mat > 1] = 1 | |
array_from_mat[array_from_mat < 0] = 0 | |
} | |
if(is.null(filename)) { | |
plot_map(flipud(array_from_mat)) | |
} else { | |
save_png(flipud(array_from_mat),filename) | |
} | |
} |