Nothing
#' Generate Camera Movement
#'
#' Takes a series of key frame camera positions and smoothly interpolates between them. Generates
#' a data.frame that can be passed to `render_animation()`.
#'
#' @param positions A list or 3-column XYZ matrix of camera positions.
#' These will serve as key frames for the camera position. Alternatively, this can also be the a
#' dataframe of the keyframe output from an interactive rayrender session (`ray_keyframes`).
#' @param lookats Default `NULL`, which sets the camera lookat to the origin `c(0,0,0)`
#' for the animation. A list or 3-column XYZ matrix
#' of `lookat` points. Must be the same number of points as `positions`.
#' @param apertures Default `0`. A numeric vector of aperture values.
#' @param fovs Default `40`. A numeric vector of field of view values.
#' @param focal_distances Default `NULL`, automatically the distance between positions and lookats.
#' Numeric vector of focal distances.
#' @param ortho_dims Default `NULL`, which results in `c(1,1)` orthographic dimensions. A list or 2-column matrix
#' of orthographic dimensions.
#' @param camera_ups Default `NULL`, which gives at up vector of `c(0,1,0)`. Camera up orientation.
#' @param type Default `spline`. Type of transition between keyframes.
#' Other options are `linear`, `quad`, `cubic`, `bezier`, `exp`, and `manual`. `spline` keeps the
#' path linear between keyframes while using a shape-preserving cubic Hermite spline to smoothly
#' vary speed. Closed paths use matching boundary speeds. Direction can still change abruptly at
#' a corner because the keyed path is preserved. `manual` just returns the values passed in,
#' properly formatted to be passed to `render_animation()`.
#' @param frames Default `30`. Total number of frames.
#' @param closed Default `FALSE`. Whether to close the camera curve so the first position matches the last. Set this to `TRUE` for perfect loops.
#' @param constant_step Default `TRUE`. Whether to make the camera travel at a constant speed
#' when `type = "bezier"`.
#' @param aperture_linear Default `TRUE`. This linearly interpolates focal distances, rather than using a smooth Bezier curve or easing function.
#' @param fov_linear Default `TRUE`. This linearly interpolates focal distances, rather than using a smooth Bezier curve or easing function.
#' @param focal_linear Default `TRUE`. This linearly interpolates focal distances, rather than using a smooth Bezier curve or easing function.
#' @param ortho_linear Default `TRUE`. This linearly interpolates orthographic dimensions, rather than using a smooth Bezier curve or easing function.
#' @param curvature_adjust Default `none`. Other options are `position`, `lookat`, and `both`. Whether to slow down the camera at areas of high curvature
#' to prevent fast swings. Only used for curve `type = bezier`. This does not preserve key frame positions.
#' Note: This feature will likely result in the `lookat` and `position` diverging if they do not
#' have similar curvatures at each point. This feature is best used when passing the same set of points to `positions` and `lookats`
#' and providing an `offset_lookat` value, which ensures the curvature will be the same.
#' @param curvature_scale Default `30`. Constant dividing factor for curvature. Higher values will subdivide the
#' path more, potentially finding a smoother path, but increasing the calculation time. Only used for curve `type = bezier`.
#' Increasing this value after a certain point will not increase the quality of the path, but it is scene-dependent.
#' @param offset_lookat Default `0`. Amount to offset the lookat position, either along the path (if `constant_step = TRUE`)
#' or towards the derivative of the Bezier curve.
#' @param damp_motion Default `FALSE`. Whether to damp the motion of the camera, so that quick movements are damped
#' and don't result in shakey motion. This function tracks the current position, and linearly interpolates between
#' that point and the next point using value `damp_magnitude`.
#' The equation for the position is `cam_current = cam_current * damp_magnitude + cam_next_point * (1 - damp_magnitude)`.
#' @param damp_magnitude Default `0.1`. Amount to damp the motion, a numeric value greater than `0` (no damping) and
#' less than `1`.
#' @param progress Default `TRUE`. Whether to display a progress bar.
#' @param smooth_orientation Default `TRUE`. Whether to use a quaternion orientation spline for
#' `spline`, `linear`, `quad`, `cubic`, and `exp` motion. This preserves the keyed camera positions
#' and orientations while making angular velocity continuous through orientation keyframes. Closed
#' paths use periodic orientation tangents. Set this to `FALSE` to interpolate lookat and up vectors
#' directly with the selected `type`.
#'
#' @export
#' @return Data frame of camera positions, orientations, apertures, focal distances, and field of views
#'
#'@examplesIf interactive() || identical(Sys.getenv("IN_PKGDOWN"), "true")
#' #Create and animate flying through a scene on a simulated roller coaster
#' set.seed(3)
#' elliplist = list()
#' ellip_colors = rainbow(8)
#' for(i in 1:1200) {
#' elliplist[[i]] = ellipsoid(x=10*runif(1)-5,y=10*runif(1)-5,z=10*runif(1)-5,
#' angle = 360*runif(3), a=0.1,b=0.05,c=0.1,
#' material=glossy(color=sample(ellip_colors,1)))
#' }
#' ellip_scene = do.call(rbind, elliplist)
#'
#' camera_pos = list(c(0,1,15),c(5,-5,5),c(-5,5,-5),c(0,1,-15))
#'
#' #Plot the camera path and render from above using the path object:
#' generate_ground(material=diffuse(checkercolor="grey20"),depth=-10) |>
#' add_object(ellip_scene) |>
#' add_object(sphere(y=50,radius=10,material=light(intensity=30))) |>
#' add_object(path(camera_pos, material=diffuse(color="red"))) |>
#' render_scene(lookfrom=c(0,20,0), width=800,height=800,samples=16,
#' camera_up = c(0,0,1),
#' fov=80)
#' #Side view
#' generate_ground(material=diffuse(checkercolor="grey20"),depth=-10) |>
#' add_object(ellip_scene) |>
#' add_object(sphere(y=50,radius=10,material=light(intensity=30))) |>
#' add_object(path(camera_pos, material=diffuse(color="red"))) |>
#' render_scene(lookfrom=c(20,0,0),width=800,height=800,samples=16,
#' fov=80)
#' #View from the start
#' generate_ground(material=diffuse(checkercolor="grey20"),depth=-10) |>
#' add_object(ellip_scene) |>
#' add_object(sphere(y=50,radius=10,material=light(intensity=30))) |>
#' add_object(path(camera_pos, material=diffuse(color="red"))) |>
#' render_scene(lookfrom=c(0,1.5,16),width=800,height=800,samples=16,
#' fov=80)
#' #Generate Camera movement, setting the lookat position to be same as camera position, but offset
#' #slightly in front. We'll render 12 frames, but you'd likely want more in a real animation.
#'
#' camera_motion = generate_camera_motion(positions = camera_pos, lookats = camera_pos,
#' offset_lookat = 1, fovs=80, frames=12,
#' type="bezier")
#'
#' #This returns a data frame of individual camera positions, interpolated by cubic bezier curves.
#' camera_motion
#'
#' #Pass NA filename to plot to the device. We'll keep the path and offset it slightly to see
#' #where we're going. This results in a "roller coaster" effect.
#' generate_ground(material=diffuse(checkercolor="grey20"),depth=-10) |>
#' add_object(ellip_scene) |>
#' add_object(sphere(y=50,radius=10,material=light(intensity=30))) |>
#' add_object(obj_model(r_obj(simple_r = TRUE),x=10,y=-10,scale_obj=3, angle=c(0,315,0),
#' material=dielectric(attenuation=c(1,1,0.3)))) |>
#' add_object(pig(x=-7,y=10,z=-5,scale=1,angle=c(0,-45,80),emotion="angry")) |>
#' add_object(pig(x=0,y=-0.25,z=-15,scale=1,angle=c(0,225,-22), order_rotation = c(3,2,1),
#' emotion="angry", spider=TRUE)) |>
#' add_object(path(camera_pos, y=-0.2,material=diffuse(color="red"))) |>
#' render_animation(filename = NA, camera_motion = camera_motion, samples=16,
#' sample_method="sobol_blue",
#' clamp_value=10, width=400, height=400)
#'
generate_camera_motion = function(
positions,
lookats = NULL,
apertures = 0,
fovs = 40,
focal_distances = NULL,
ortho_dims = NULL,
camera_ups = NULL,
type = "spline",
frames = 30,
closed = FALSE,
aperture_linear = TRUE,
fov_linear = TRUE,
focal_linear = TRUE,
ortho_linear = TRUE,
constant_step = TRUE,
curvature_adjust = "none",
curvature_scale = 30,
offset_lookat = 0,
damp_motion = FALSE,
damp_magnitude = 0.1,
progress = TRUE,
smooth_orientation = TRUE
) {
damp_magnitude = 1 - damp_magnitude
stopifnot(damp_magnitude >= 0 && damp_magnitude < 1)
if (damp_magnitude == 0) {
damp_motion = FALSE
}
curve_adj_type = unlist(lapply(
tolower(curvature_adjust),
switch,
"none" = 0,
"position" = 1,
"lookats" = 2,
"both" = 3,
0
))
curvature_adjust_pos = curve_adj_type %in% c(1, 3)
curvature_adjust_look = curve_adj_type %in% c(2, 3)
if (
!is.null(dim(positions)) &&
all(
c(
"x",
"y",
"z",
"dx",
"dy",
"dz",
"aperture",
"fov",
"focal",
"orthox",
"orthoy",
"upx",
"upy",
"upz"
) %in%
colnames(positions)
)
) {
temp = positions
positions = temp[, c("x", "y", "z")]
lookats = temp[, c("dx", "dy", "dz")]
apertures = temp[, "aperture"]
fovs = temp[, "fov"]
focal_distances = temp[, "focal"]
ortho_dims = temp[, c("orthox", "orthoy")]
camera_ups = temp[, c("upx", "upy", "upz")]
}
if (type == "bezier") {
position_control_points = process_point_series(
positions,
closed = closed,
straight = FALSE
)
points_dist = calculate_distance_along_bezier_curve(
position_control_points,
breaks = 50
)
points_final = calculate_final_path(
points_dist,
steps = frames,
constant_step = constant_step,
curvature_adjust = curvature_adjust_pos,
curvature_scale = curvature_scale,
progress = progress,
string = "Position "
)
if (!is.null(lookats)) {
position_control_lookats = process_point_series(
lookats,
closed = closed,
straight = FALSE
)
lookats_dist = calculate_distance_along_bezier_curve(
position_control_lookats,
breaks = 50
)
lookats_final = calculate_final_path(
lookats_dist,
steps = frames,
constant_step = constant_step,
offset = offset_lookat,
curvature_adjust = curvature_adjust_look,
curvature_scale = curvature_scale,
progress = progress,
string = "Orientation"
)
} else {
lookats_final = matrix(
c(0, 0, 0),
nrow = nrow(points_final),
ncol = 3,
byrow = TRUE
)
}
if (curvature_adjust_pos || curvature_adjust_look) {
temp_points = points_final[1:frames, ]
temp_lookats = lookats_final[1:frames, ]
rowvals = seq(1, nrow(points_final), length.out = frames)
for (i in 1:frames) {
rv = rowvals[i]
frv = floor(rv)
if (frv == rv) {
temp_points[i, ] = points_final[rv, ]
temp_lookats[i, ] = lookats_final[rv, ]
next
}
temp_points[i, ] = lerp(
rv - frv,
points_final[rv, ],
points_final[frv + 1, ]
)
temp_lookats[i, ] = lerp(
rv - frv,
lookats_final[rv, ],
lookats_final[frv + 1, ]
)
}
if (curvature_adjust_pos) {
points_final = temp_points
}
if (curvature_adjust_look) {
lookats_final = temp_lookats
}
}
if (length(apertures) != 1) {
apertures_control_lookats = process_point_series_1d(
apertures,
closed = closed,
straight = aperture_linear
)
apertures_dist = calculate_distance_along_bezier_curve(
apertures_control_lookats,
breaks = 50
)
apertures_final = calculate_final_path(
apertures_dist,
steps = frames,
constant_step = constant_step
)
apertures_final = apertures_final[, 1]
} else {
apertures_final = matrix(apertures, nrow = nrow(points_final), ncol = 1)
}
if (length(fovs) != 1) {
fovs_control_lookats = process_point_series_1d(
fovs,
closed = closed,
straight = fov_linear
)
fovs_dist = calculate_distance_along_bezier_curve(
fovs_control_lookats,
breaks = 50
)
fovs_final = calculate_final_path(
fovs_dist,
steps = frames,
constant_step = constant_step
)
fovs_final = fovs_final[, 1]
} else {
fovs_final = matrix(fovs, nrow = nrow(points_final), ncol = 1)
}
if (!is.null(focal_distances)) {
if (is.numeric(focal_distances)) {
if (length(focal_distances) == 1) {
focal_final = rep(focal_distances, nrow(points_final))
} else {
focal_final = tween(focal_distances, n = frames, ease = "linear")
}
} else {
focal_control_lookats = process_point_series_1d(
focal_distances,
closed = closed,
straight = focal_linear
)
focal_dist = calculate_distance_along_bezier_curve(
focal_control_lookats,
breaks = 50
)
focal_final = calculate_final_path(
focal_dist,
steps = frames,
constant_step = constant_step
)
focal_final = focal_final[, 1]
}
} else {
focal_final = sqrt(apply((points_final - lookats_final)^2, 1, sum))
}
if (!is.null(ortho_dims)) {
ortho_control_lookats = process_point_series_2d(
ortho_dims,
closed = closed,
straight = focal_linear
)
ortho_dist = calculate_distance_along_bezier_curve(
ortho_control_lookats,
breaks = 50
)
ortho_final = calculate_final_path(
ortho_dist,
steps = frames,
constant_step = constant_step
)
ortho_final = ortho_final[, 1:2]
} else {
ortho_final = matrix(c(1, 1), nrow = nrow(points_final), ncol = 2)
}
if (!is.null(camera_ups)) {
ups_control = process_point_series_2d(
camera_ups,
closed = closed,
straight = ortho_linear
)
up_dist = calculate_distance_along_bezier_curve(ups_control, breaks = 50)
up_final = calculate_final_path(
up_dist,
steps = frames,
constant_step = constant_step
)
} else {
up_final = matrix(
c(0, 1, 0),
nrow = nrow(points_final),
ncol = 3,
byrow = TRUE
)
}
final_motion = as.data.frame(cbind(
points_final,
lookats_final,
apertures_final,
fovs_final,
focal_final,
ortho_final,
up_final
))
rownames(final_motion) = NULL
colnames(final_motion) = c(
"x",
"y",
"z",
"dx",
"dy",
"dz",
"aperture",
"fov",
"focal",
"orthox",
"orthoy",
"upx",
"upy",
"upz"
)
if (damp_motion) {
final_motion = damp_camera_motion(final_motion, damp_magnitude, closed)
}
return(as_ray_camera_motion(final_motion))
} else if (type %in% c("exp", "quad", "cubic", "linear", "spline")) {
if (inherits(positions, "list")) {
positions = do.call(rbind, positions)
}
if (inherits(lookats, "list")) {
lookats = do.call(rbind, lookats)
}
if (inherits(camera_ups, "list")) {
camera_ups = do.call(rbind, camera_ups)
}
if (inherits(ortho_dims, "list")) {
ortho_dims = do.call(rbind, ortho_dims)
}
positions = grDevices::xyz.coords(positions)
if (is.null(lookats)) {
lookats = matrix(0, ncol = 3, nrow = length(positions$x))
}
lookats = grDevices::xyz.coords(lookats)
if (is.null(camera_ups)) {
camera_ups = matrix(
c(0, 1, 0),
ncol = 3,
nrow = length(positions$x),
byrow = TRUE
)
}
camera_ups = grDevices::xyz.coords(camera_ups)
if (is.null(focal_distances)) {
focal_distances = sqrt(
(positions$x - lookats$x)^2 +
(positions$y - lookats$y)^2 +
(positions$z - lookats$z)^2
)
} else if (length(focal_distances) == 1) {
focal_distances = rep(focal_distances, length(positions$x))
}
if (is.null(ortho_dims)) {
ortho_dims = matrix(
c(1, 1),
ncol = 2,
nrow = length(positions$x),
byrow = TRUE
)
}
ortho = grDevices::xy.coords(ortho_dims)
if (length(apertures) == 1) {
apertures = rep(apertures, length(positions$x))
}
if (length(apertures) == 1) {
fovs = rep(fovs, length(positions$x))
}
tween_df = data.frame(
x = positions$x,
y = positions$y,
z = positions$z,
dx = lookats$x,
dy = lookats$y,
dz = lookats$z,
aperture = apertures,
fov = fovs,
focal = focal_distances,
orthox = ortho$x,
orthoy = ortho$y,
upx = camera_ups$x,
upy = camera_ups$y,
upz = camera_ups$z
)
if (closed) {
tween_df = rbind(tween_df, tween_df[1, ])
apertures = c(apertures, apertures[1])
fovs = c(fovs, fovs[1])
focal_distances = c(focal_distances, focal_distances[1])
ortho$x = c(ortho$x, ortho$x[1])
ortho$y = c(ortho$y, ortho$y[1])
}
if (type == "spline") {
spline_groups = list(
c("x", "y", "z"),
c("dx", "dy", "dz"),
"aperture",
"fov",
"focal",
c("orthox", "orthoy"),
c("upx", "upy", "upz")
)
final_motion = do.call(
cbind,
lapply(
spline_groups,
function(columns) {
tween_spline_path(
tween_df[, columns, drop = FALSE],
n = frames,
closed = closed
)
}
)
)
final_motion = as.data.frame(final_motion)
colnames(final_motion) = colnames(tween_df)
} else {
final_motion = as.data.frame(apply(
tween_df,
2,
tween,
n = frames,
ease = type,
closed = closed
))
}
rownames(final_motion) = NULL
if (aperture_linear) {
final_motion$aperture = tween(apertures, n = frames, ease = "linear")
}
if (fov_linear) {
final_motion$fov = tween(fovs, n = frames, ease = "linear")
}
if (focal_linear) {
final_motion$focal = tween(focal_distances, n = frames, ease = "linear")
}
if (ortho_linear) {
final_motion$orthox = tween(ortho$x, n = frames, ease = "linear")
final_motion$orthoy = tween(ortho$y, n = frames, ease = "linear")
}
if (smooth_orientation) {
orientation_motion = tween_camera_orientation(
positions = as.matrix(tween_df[, c("x", "y", "z")]),
lookats = as.matrix(tween_df[, c("dx", "dy", "dz")]),
camera_ups = as.matrix(tween_df[, c("upx", "upy", "upz")]),
output_positions = as.matrix(final_motion[, c("x", "y", "z")]),
closed = closed
)
final_motion[, c("dx", "dy", "dz")] = orientation_motion$lookats
final_motion[, c("upx", "upy", "upz")] = orientation_motion$camera_ups
}
if (damp_motion) {
final_motion = damp_camera_motion(final_motion, damp_magnitude, closed)
}
return(as_ray_camera_motion(final_motion))
} else if (type == "manual") {
if (inherits(positions, "list")) {
positions = do.call(rbind, positions)
}
if (inherits(lookats, "list")) {
lookats = do.call(rbind, lookats)
}
if (inherits(camera_ups, "list")) {
camera_ups = do.call(rbind, camera_ups)
}
if (inherits(ortho_dims, "list")) {
ortho_dims = do.call(rbind, ortho_dims)
}
if (is.numeric(ortho_dims) && length(ortho_dims) == 2) {
ortho_dims = matrix(
ortho_dims,
ncol = 2,
nrow = length(positions$x),
byrow = TRUE
)
}
positions = grDevices::xyz.coords(positions)
if (is.null(lookats)) {
lookats = matrix(0, ncol = 3, nrow = length(positions$x))
} else if (is.numeric(lookats) && length(lookats) == 3) {
lookats = matrix(
lookats,
ncol = 3,
nrow = length(positions$x),
byrow = TRUE
)
}
lookats = grDevices::xyz.coords(lookats)
if (is.null(camera_ups)) {
camera_ups = matrix(
c(0, 1, 0),
ncol = 3,
nrow = length(positions$x),
byrow = TRUE
)
}
camera_ups = grDevices::xyz.coords(camera_ups)
if (is.null(focal_distances)) {
focal_distances = sqrt(
(positions$x - lookats$x)^2 +
(positions$y - lookats$y)^2 +
(positions$z - lookats$z)^2
)
} else if (length(focal_distances) == 1) {
focal_distances = rep(focal_distances, length(positions$x))
}
if (is.null(ortho_dims)) {
ortho_dims = matrix(
c(1, 1),
ncol = 2,
nrow = length(positions$x),
byrow = TRUE
)
}
ortho = grDevices::xy.coords(ortho_dims)
final_motion = data.frame(
x = positions$x,
y = positions$y,
z = positions$z,
dx = lookats$x,
dy = lookats$y,
dz = lookats$z,
aperture = rep(apertures, length(positions$x)),
fov = rep(fovs, length(positions$x)),
focal = focal_distances,
orthox = ortho$x,
orthoy = ortho$y,
upx = camera_ups$x,
upy = camera_ups$y,
upz = camera_ups$z
)
if (damp_motion) {
final_motion = damp_camera_motion(final_motion, damp_magnitude, closed)
}
return(as_ray_camera_motion(final_motion))
} else {
stop("type '", type, "' not recognized")
}
}
#' Damp Camera Motion
#'
#' @param motion Motion data frame.
#' @param damp_magnitude Damping multiplier.
#' @param closed Whether to treat the motion as a closed loop.
#' @return Damped motion data frame.
#'
#' @keywords internal
damp_camera_motion = function(motion, damp_magnitude, closed = FALSE) {
motion_names = colnames(motion)
motion_matrix = as.matrix(motion)
if (nrow(motion_matrix) < 2) {
return(motion)
}
if (!closed) {
damped_motion = damp_camera_motion_open(motion_matrix, damp_magnitude)
} else {
closed_cols = seq_len(ncol(motion_matrix))
if (all(c("x", "y", "z") %in% motion_names)) {
closed_cols = match(c("x", "y", "z"), motion_names)
}
duplicate_endpoint = all(
abs(
motion_matrix[1, closed_cols] -
motion_matrix[nrow(motion_matrix), closed_cols]
) <
sqrt(.Machine$double.eps)
)
if (duplicate_endpoint) {
damped_motion = damp_camera_motion_closed(
motion_matrix[-nrow(motion_matrix), , drop = FALSE],
damp_magnitude
)
damped_motion = rbind(damped_motion, damped_motion[1, , drop = FALSE])
} else {
damped_motion = damp_camera_motion_closed(motion_matrix, damp_magnitude)
damped_motion = close_damped_camera_motion(damped_motion)
}
}
damped_motion = as.data.frame(damped_motion)
colnames(damped_motion) = motion_names
rownames(damped_motion) = NULL
return(damped_motion)
}
#' Damp Open Camera Motion
#'
#' @param motion_matrix Motion matrix.
#' @param damp_magnitude Damping multiplier.
#' @return Damped motion matrix.
#'
#' @keywords internal
damp_camera_motion_open = function(motion_matrix, damp_magnitude) {
damped_motion = motion_matrix
current_pos = motion_matrix[1, ]
for (i in seq_len(nrow(motion_matrix))[-1]) {
current_pos = current_pos *
damp_magnitude +
motion_matrix[i, ] * (1 - damp_magnitude)
damped_motion[i, ] = current_pos
}
return(damped_motion)
}
#' Damp Closed Camera Motion
#'
#' @param motion_matrix Motion matrix.
#' @param damp_magnitude Damping multiplier.
#' @return Damped motion matrix.
#'
#' @keywords internal
damp_camera_motion_closed = function(motion_matrix, damp_magnitude) {
damped_motion = motion_matrix
target_weight = 1 - damp_magnitude
frame_count = nrow(motion_matrix)
powers = damp_magnitude^(rev(seq_len(frame_count)) - 1)
current_pos = as.numeric(
target_weight *
(powers %*% motion_matrix) /
(1 - damp_magnitude^frame_count)
)
for (i in seq_len(frame_count)) {
current_pos = current_pos *
damp_magnitude +
motion_matrix[i, ] * target_weight
damped_motion[i, ] = current_pos
}
return(damped_motion)
}
#' Close Damped Camera Motion
#'
#' @param motion_matrix Motion matrix.
#' @return Closed motion matrix.
#'
#' @keywords internal
close_damped_camera_motion = function(motion_matrix) {
endpoint_delta = motion_matrix[nrow(motion_matrix), ] - motion_matrix[1, ]
correction = seq(0, 1, length.out = nrow(motion_matrix)) %o%
endpoint_delta
return(motion_matrix - correction)
}
#' Process Points to Control Points
#'
#' @param points Points
#' @param closed Whether to be closed
#' @param straight Whether to be straight
#' @return Matrix of control points
#'
#' @keywords internal
process_point_series = function(points, closed = FALSE, straight = FALSE) {
if (inherits(points, "numeric")) {
stop("Input must either be list, matrix, or data.frame, not numeric.")
}
if (inherits(points, "list")) {
if (any(unlist(lapply(points, (function(x) length(x) != 3))))) {
stop("If `points` is a list, each entry must be a length-3 vector")
}
points = do.call(rbind, points)
}
if (inherits(points, "data.frame")) {
points = as.matrix(points)
}
if (is.array(points)) {
if (nrow(points) == 1) {
stop("Only one point passed, no path specified.")
}
if (nrow(points) == 2 && closed) {
closed = FALSE
}
}
if (inherits(points, "matrix")) {
if (ncol(points) == 3) {
if (!straight) {
full_control_points = calculate_control_points(points)
} else {
full_control_points = calculate_control_points_straight(points)
}
} else {
stop("If points a matrix or data.frame, must have 3 columns")
}
} else {
stop(
"points not of supported type (function expects matrix/data.frame/list, got ",
class(points),
")"
)
}
if (closed) {
first_point = full_control_points[[1]]
last_point = full_control_points[[length(full_control_points)]]
full_control_points[[length(full_control_points) + 1]] = last_point
full_control_points[[length(full_control_points)]][4, ] = first_point[1, ]
full_control_points[[length(full_control_points)]][3, ] = 2 *
first_point[1, ] -
first_point[2, ]
full_control_points[[length(full_control_points)]][2, ] = 2 *
last_point[4, ] -
last_point[3, ]
full_control_points[[length(full_control_points)]][1, ] = last_point[4, ]
}
return(full_control_points)
}
#' Process Points to Control Points
#'
#' @param values Points
#' @param closed Whether to be closed
#' @param straight Whether to be straight
#' @return Matrix of control points
#'
#' @keywords internal
process_point_series_1d = function(values, closed = FALSE, straight = FALSE) {
mat_values = matrix(0, ncol = 3, nrow = length(values))
mat_values[, 1] = values
if (!straight) {
full_control_points = calculate_control_points(mat_values)
} else {
full_control_points = calculate_control_points_straight(mat_values)
}
return(full_control_points)
}
#' Process Points to Control Points
#'
#' @param values Points
#' @param closed Whether to be closed
#' @param straight Whether to be straight
#' @return Matrix of control points
#'
#' @keywords internal
process_point_series_2d = function(values, closed = FALSE, straight = FALSE) {
mat_values = matrix(0, ncol = 3, nrow = nrow(values))
mat_values[, 1] = values[, 1]
mat_values[, 2] = values[, 2]
if (!straight) {
full_control_points = calculate_control_points(mat_values)
} else {
full_control_points = calculate_control_points_straight(mat_values)
}
return(full_control_points)
}
#' Evaluate Bezier
#'
#' @param cp Control point matrix (4x3)
#' @param t Interpolation distance
#' @return 3D Numeric value for point in space
#'
#' @keywords internal
eval_bezier = function(cp, t) {
return(
cp[1, ] *
(1 - t)^3 +
cp[2, ] * 3 * t * (1 - t)^2 +
cp[3, ] * 3 * t^2 * (1 - t) +
cp[4, ] * t^3
)
}
#' Evaluate Deriv Bezier
#'
#' @param cp Control point matrix (4x3)
#' @param t Interpolation distance
#' @return 3D Numeric value for point in space
#'
#' @keywords internal
eval_bezier_deriv = function(cp, t) {
return(
-3 *
cp[1, ] *
(1 - t)^2 +
cp[2, ] * 3 * (1 - t)^2 -
cp[2, ] * 6 * t * (1 - t) +
cp[3, ] * 6 * t * (1 - t) -
cp[3, ] * 3 * t^2 +
3 * cp[4, ] * t^2
)
}
#' Evaluate Deriv Bezier
#'
#' @param cp Control point matrix (4x3)
#' @param t Interpolation distance
#' @return 3D Numeric value for point in space
#'
#' @keywords internal
eval_bezier_2nd_deriv = function(cp, t) {
return(
6 *
cp[1, ] *
(1 - t) -
cp[2, ] * 6 * (1 - t) -
cp[2, ] * 6 * (1 - t) +
cp[2, ] * 6 * t +
cp[3, ] * 6 * (1 - t) -
cp[3, ] * 6 * t -
cp[3, ] * 6 * t +
6 * cp[4, ] * t
)
}
#' Cross product (vec)
#'
#' @param x vec1
#' @param y vec2
#' @return 3D Numeric value for vector in space
#'
#' @keywords internal
cross_prod = function(x, y) {
return(c(
x[2] * y[3] - x[3] * y[2],
-(x[1] * y[3] - x[3] * y[1]),
x[1] * y[2] - x[2] * y[1]
))
}
#' Get Distance Along Bezier Curve
#'
#' @param cps Control points
#' @param breaks Number of interpolation breaks
#' @return Data frame of points along curve, along with distances
#'
#' @keywords internal
calculate_distance_along_bezier_curve = function(cps, breaks = 20) {
distance_list = list()
prev_pos = eval_bezier(cps[[1]], 0)
temp_deriv = eval_bezier_deriv(cps[[1]], 0)
temp_second_deriv = eval_bezier_2nd_deriv(cps[[1]], 0)
if (any(temp_deriv != 0)) {
curve = sqrt(sum(cross_prod(temp_deriv, temp_second_deriv)^2)) /
sum(temp_deriv^2)^(3 / 2)
} else {
curve = Inf
}
distance_list[[1]] = data.frame(
dist = 0,
segment = 1,
t = 0,
x = prev_pos[1],
y = prev_pos[2],
z = prev_pos[3],
curvature = curve,
dx = temp_deriv[1],
dy = temp_deriv[2],
dz = temp_deriv[3]
)
counter = 2
for (seg in seq_len(length(cps))) {
for (t in seq(0, 1, length.out = breaks + 1)[-1]) {
temp_pos = eval_bezier(cps[[seg]], t)
temp_deriv = eval_bezier_deriv(cps[[seg]], t)
temp_second_deriv = eval_bezier_2nd_deriv(cps[[seg]], t)
if (any(temp_deriv != 0)) {
curve = sqrt(sum(cross_prod(temp_deriv, temp_second_deriv)^2)) /
sum(temp_deriv^2)^(3 / 2)
distance_list[[counter]] = data.frame(
dist = sqrt(sum((temp_pos - prev_pos)^2)),
segment = seg,
t = (seg + t - 1) / length(cps),
x = temp_pos[1],
y = temp_pos[2],
z = temp_pos[3],
curvature = curve,
dx = temp_deriv[1],
dy = temp_deriv[2],
dz = temp_deriv[3]
)
} else {
curve = Inf
distance_list[[counter]] = data.frame(
dist = sqrt(sum((temp_pos - prev_pos)^2)),
segment = seg,
t = (seg + t - 1) / length(cps),
x = temp_pos[1],
y = temp_pos[2],
z = temp_pos[3],
curvature = curve,
dx = temp_deriv[1],
dy = temp_deriv[2],
dz = temp_deriv[3]
)
}
prev_pos = temp_pos
counter = counter + 1
}
}
final_values = do.call(rbind, distance_list)
final_values$total_dist = cumsum(final_values$dist)
return(final_values)
}
#' Remove Stalled Path Samples
#'
#' @param linearized_cp Linearized path data frame.
#' @return Linearized path data frame with repeated cumulative-distance samples removed.
#'
#' @keywords internal
remove_stalled_path_samples = function(linearized_cp) {
if (!"total_dist" %in% colnames(linearized_cp) || nrow(linearized_cp) < 2) {
return(linearized_cp)
}
distance_tolerance = .Machine$double.eps *
max(1, max(abs(linearized_cp$total_dist), na.rm = TRUE)) *
100
keep_rows = c(TRUE, diff(linearized_cp$total_dist) > distance_tolerance)
return(linearized_cp[keep_rows, , drop = FALSE])
}
#' Calculate Path Interval
#'
#' @param linearized_cp Linearized path data frame.
#' @param current_dist Current cumulative distance.
#' @return List containing the row and interpolation value.
#'
#' @keywords internal
calculate_path_interval = function(linearized_cp, current_dist) {
distance_tolerance = .Machine$double.eps *
max(1, max(abs(linearized_cp$total_dist), na.rm = TRUE)) *
100
row = max(which(linearized_cp$total_dist <= current_dist), 1)
if (row + 1 > nrow(linearized_cp)) {
row = nrow(linearized_cp) - 1
}
dist_delta = linearized_cp$total_dist[row + 1] -
linearized_cp$total_dist[row]
if (dist_delta <= distance_tolerance) {
tval = 0
} else {
tval = (current_dist - linearized_cp$total_dist[row]) / dist_delta
}
return(list(row = row, tval = tval))
}
#' Linearize and Calculate Final Points (with constant stepsize)
#'
#' @param linearized_cp Matrix (4x3)
#' @param steps Number of steps
#' @param constant_step Whether to step at constant steps or not
#' @param offset Offset along curve
#' @return Matrix of points along curve
#'
#' @keywords internal
calculate_final_path = function(
linearized_cp,
steps,
constant_step = TRUE,
curvature_adjust = FALSE,
curvature_scale = 1,
offset = 0,
progress = FALSE,
string = ""
) {
if (max(linearized_cp$total_dist) < 1e-7) {
single_row = linearized_cp[1, c("x", "y", "z")]
single_df = as.data.frame(matrix(
as.numeric(single_row),
nrow = steps,
ncol = 3,
byrow = TRUE
))
colnames(single_df) = c("x", "y", "z")
return(single_df)
}
if (constant_step || curvature_adjust) {
linearized_cp = remove_stalled_path_samples(linearized_cp)
}
if (nrow(linearized_cp) < 2) {
single_row = linearized_cp[1, c("x", "y", "z")]
single_df = as.data.frame(matrix(
as.numeric(single_row),
nrow = steps,
ncol = 3,
byrow = TRUE
))
colnames(single_df) = c("x", "y", "z")
return(single_df)
}
if (constant_step) {
stepsize = max(linearized_cp$total_dist) / steps
final_points = list()
current_dist = offset
for (i in 1:steps) {
path_interval = calculate_path_interval(linearized_cp, current_dist)
row = path_interval$row
tval = path_interval$tval
final_points[[i]] = lerp(
tval,
linearized_cp[row, c("x", "y", "z")],
linearized_cp[row + 1, c("x", "y", "z")]
)
current_dist = current_dist + stepsize
}
} else {
if (!curvature_adjust) {
rowvals = seq(1, nrow(linearized_cp), length.out = steps)
final_points = list()
for (i in 1:(length(rowvals) - 1)) {
final_points[[i]] = lerp(
rowvals[i] - floor(rowvals[i]),
linearized_cp[floor(rowvals[i]), c("x", "y", "z")],
linearized_cp[floor(rowvals[i]) + 1, c("x", "y", "z")]
)
if (offset != 0) {
direction = lerp(
rowvals[i] - floor(rowvals[i]),
linearized_cp[floor(rowvals[i]), c("dx", "dy", "dz")],
linearized_cp[floor(rowvals[i]) + 1, c("dx", "dy", "dz")]
)
if (all(direction != 0)) {
direction = direction / sqrt(sum(direction^2))
} else {
direction = c(0, 0, 0)
}
final_points[[i]] = final_points[[i]] + direction * offset
}
}
if (offset != 0) {
direction = linearized_cp[nrow(linearized_cp), c("dx", "dy", "dz")]
direction = direction / sqrt(sum(direction^2))
final_points[[length(rowvals)]] = linearized_cp[
nrow(linearized_cp),
c("x", "y", "z")
] +
direction * offset
} else {
final_points[[length(rowvals)]] = linearized_cp[
nrow(linearized_cp),
c("x", "y", "z")
]
}
} else {
maxdist = max(linearized_cp$total_dist)
default_stepsize = maxdist / steps
final_points = list()
current_dist = 0
i = 1
if (progress) {
pb = progress::progress_bar$new(
format = sprintf("Generating Camera %s [:bar] :percent", string),
total = maxdist,
width = 70
)
}
while (current_dist < maxdist) {
if (progress) {
pb$update(current_dist / maxdist)
}
path_interval = calculate_path_interval(linearized_cp, current_dist)
row = path_interval$row
tval = path_interval$tval
final_points[[i]] = lerp(
tval,
linearized_cp[row, c("x", "y", "z")],
linearized_cp[row + 1, c("x", "y", "z")]
)
direction = lerp(
tval,
linearized_cp[row, c("dx", "dy", "dz")],
linearized_cp[row + 1, c("dx", "dy", "dz")]
)
direction_length = sqrt(sum(direction^2))
if (is.finite(direction_length) && direction_length > 0) {
direction = direction / direction_length
} else {
direction = c(0, 0, 0)
}
final_points[[i]] = final_points[[i]] + direction * offset
temp_curve = lerp(
tval,
linearized_cp[row, c("curvature")],
linearized_cp[row + 1, c("curvature")]
)
if (!is.finite(temp_curve) || temp_curve <= 0) {
step = default_stepsize
} else {
step = min(c(1 / temp_curve / curvature_scale, default_stepsize))
}
if (!is.finite(step) || step <= 0) {
step = default_stepsize
}
current_dist = current_dist + step
i = i + 1
}
}
}
return(do.call(rbind, final_points))
}
#' Remove Sequential Duplicate Keyframes
#'
#' @param keyframes Keyframe data frame.
#' @return Keyframe data frame with adjacent duplicate rows removed.
#'
#' @keywords internal
remove_sequential_duplicate_keyframes = function(keyframes) {
if (nrow(keyframes) < 2) {
return(keyframes)
}
duplicate_rows = rep(TRUE, nrow(keyframes) - 1)
for (col in seq_along(keyframes)) {
previous_values = keyframes[-nrow(keyframes), col]
current_values = keyframes[-1, col]
values_equal = previous_values == current_values
values_equal[is.na(previous_values) & is.na(current_values)] = TRUE
values_equal[is.na(values_equal)] = FALSE
duplicate_rows = duplicate_rows & values_equal
}
deduped_keyframes = keyframes[c(TRUE, !duplicate_rows), , drop = FALSE]
rownames(deduped_keyframes) = NULL
return(deduped_keyframes)
}
#' Get Saved Keyframes
#'
#' @description Get a dataframe of the saved keyframes (using the interactive renderer) to pass to `generate_camera_motion()`
#' @return Data frame of keyframes
#'
#' @export
#'@examplesIf interactive() || identical(Sys.getenv("IN_PKGDOWN"), "true")
#' #This will return an empty data frame if no keyframes have been set.
#' get_saved_keyframes()
get_saved_keyframes = function() {
keyframes = get("keyframes", envir = ray_environment)
if (nrow(keyframes) == 0) {
message(
"No keyframes saved: press K when using interactive preview to save keyframe"
)
return(keyframes)
}
return(remove_sequential_duplicate_keyframes(keyframes))
}
Any scripts or data that you put into this service are public.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.