R/generate_camera_motion.R

Defines functions get_saved_keyframes remove_sequential_duplicate_keyframes calculate_final_path calculate_path_interval remove_stalled_path_samples calculate_distance_along_bezier_curve cross_prod eval_bezier_2nd_deriv eval_bezier_deriv eval_bezier process_point_series_2d process_point_series_1d process_point_series close_damped_camera_motion damp_camera_motion_closed damp_camera_motion_open damp_camera_motion generate_camera_motion

Documented in calculate_distance_along_bezier_curve calculate_final_path calculate_path_interval close_damped_camera_motion cross_prod damp_camera_motion damp_camera_motion_closed damp_camera_motion_open eval_bezier eval_bezier_2nd_deriv eval_bezier_deriv generate_camera_motion get_saved_keyframes process_point_series process_point_series_1d process_point_series_2d remove_sequential_duplicate_keyframes remove_stalled_path_samples

#' 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))
}

Try the rayrender package in your browser

Any scripts or data that you put into this service are public.

rayrender documentation built on Sept. 16, 2026, 9:08 a.m.