From 25dec33c253f035485bb2e1f8563e12ef3134e8b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Thomas=20M=C3=BCller?= <thomas94@gmx.net>
Date: Wed, 31 Aug 2022 14:32:45 +0200
Subject: [PATCH] Add option to loop camera paths

Courtesy of Eric Haines
---
 .../neural-graphics-primitives/camera_path.h  | 19 ++++++++++++++++---
 include/neural-graphics-primitives/testbed.h  |  2 ++
 scripts/run.py                                |  2 ++
 src/camera_path.cu                            |  2 +-
 src/python_api.cu                             |  1 +
 src/testbed.cu                                |  8 ++++++++
 6 files changed, 30 insertions(+), 4 deletions(-)

diff --git a/include/neural-graphics-primitives/camera_path.h b/include/neural-graphics-primitives/camera_path.h
index b520b69..16a33e7 100644
--- a/include/neural-graphics-primitives/camera_path.h
+++ b/include/neural-graphics-primitives/camera_path.h
@@ -72,12 +72,25 @@ struct CameraPath {
 	bool m_update_cam_from_path = false;
 	float m_playtime = 0.f;
 	float m_autoplayspeed = 0.f;
-
-	const CameraKeyframe& get_keyframe(int i) { return m_keyframes[tcnn::clamp(i, 0, (int)m_keyframes.size()-1)]; }
+	// If m_loop is set true, the last frame set will be more like "next to last,"
+	// with animation then returning back to the first frame, making a continuous loop.
+	// Note that the user does not have to (and should not normally) duplicate the first frame to be the last frame.
+	bool m_loop = false;
+
+	const CameraKeyframe& get_keyframe(int i) {
+		if (m_loop) {
+			int size = (int)m_keyframes.size();
+			// add size to ensure no negative value is generated by modulo
+			return m_keyframes[(i + size) % size];
+		} else {
+			return m_keyframes[tcnn::clamp(i, 0, (int)m_keyframes.size()-1)];
+		}
+	}
 	CameraKeyframe eval_camera_path(float t) {
 		if (m_keyframes.empty())
 			return {};
-		t *= (float)(m_keyframes.size()-1);
+		// make room for last frame == first frame when looping
+		t *= (float)(m_loop ? m_keyframes.size() : m_keyframes.size()-1);
 		int t1 = (int)floorf(t);
 		return spline(t-floorf(t), get_keyframe(t1-1), get_keyframe(t1), get_keyframe(t1+1), get_keyframe(t1+2));
 	}
diff --git a/include/neural-graphics-primitives/testbed.h b/include/neural-graphics-primitives/testbed.h
index 521e8a3..227ffec 100644
--- a/include/neural-graphics-primitives/testbed.h
+++ b/include/neural-graphics-primitives/testbed.h
@@ -388,6 +388,8 @@ public:
 	void set_camera_from_time(float t);
 	void update_loss_graph();
 	void load_camera_path(const std::string& filepath_string);
+	bool loop_animation();
+	void set_loop_animation(bool value);
 
 	float compute_image_mse(bool quantize_to_byte);
 
diff --git a/scripts/run.py b/scripts/run.py
index aed15f8..151e924 100644
--- a/scripts/run.py
+++ b/scripts/run.py
@@ -46,6 +46,7 @@ def parse_args():
 
 	parser.add_argument("--video_camera_path", default="", help="The camera path to render, e.g., base_cam.json.")
 	parser.add_argument("--video_camera_smoothing", action="store_true", help="Applies additional smoothing to the camera trajectory with the caveat that the endpoint of the camera path may not be reached.")
+	parser.add_argument("--video_loop_animation", action="store_true", help="Connect the last and first keyframes in a continuous loop.")
 	parser.add_argument("--video_fps", type=int, default=60, help="Number of frames per second.")
 	parser.add_argument("--video_n_seconds", type=int, default=1, help="Number of seconds the rendered video should be long.")
 	parser.add_argument("--video_spp", type=int, default=8, help="Number of samples per pixel. A larger number means less noise, but slower rendering.")
@@ -336,6 +337,7 @@ if __name__ == "__main__":
 
 	if args.video_camera_path:
 		testbed.load_camera_path(args.video_camera_path)
+		testbed.loop_animation = args.video_loop_animation
 
 		resolution = [args.width or 1920, args.height or 1080]
 		n_frames = args.video_n_seconds * args.video_fps
diff --git a/src/camera_path.cu b/src/camera_path.cu
index 88d8be0..2815448 100644
--- a/src/camera_path.cu
+++ b/src/camera_path.cu
@@ -60,7 +60,7 @@ CameraKeyframe spline(float t, const CameraKeyframe& p0, const CameraKeyframe& p
 		CameraKeyframe r1 = lerp(q1, q2, t,  0.f, 2.f);
 		return lerp(r0, r1, t, 0.f, 1.f);
 	} else {
-		// cublic bspline
+		// cubic bspline
 		float tt=t*t;
 		float ttt=t*t*t;
 		float a = (1-t)*(1-t)*(1-t)*(1.f/6.f);
diff --git a/src/python_api.cu b/src/python_api.cu
index 6d4427d..ef41fa6 100644
--- a/src/python_api.cu
+++ b/src/python_api.cu
@@ -386,6 +386,7 @@ PYBIND11_MODULE(pyngp, m) {
 		.def("save_snapshot", &Testbed::save_snapshot, py::arg("path"), py::arg("include_optimizer_state")=false, "Save a snapshot of the currently trained model")
 		.def("load_snapshot", &Testbed::load_snapshot, py::arg("path"), "Load a previously saved snapshot")
 		.def("load_camera_path", &Testbed::load_camera_path, "Load a camera path", py::arg("path"))
+		.def_property("loop_animation", &Testbed::loop_animation, &Testbed::set_loop_animation)
 		.def("compute_and_save_png_slices", &Testbed::compute_and_save_png_slices,
 			py::arg("filename"),
 			py::arg("resolution") = Eigen::Vector3i::Constant(256),
diff --git a/src/testbed.cu b/src/testbed.cu
index 8052e50..78c1ac2 100644
--- a/src/testbed.cu
+++ b/src/testbed.cu
@@ -3008,5 +3008,13 @@ void Testbed::load_camera_path(const std::string& filepath_string) {
 	m_camera_path.load(filepath_string, Matrix<float, 3, 4>::Identity());
 }
 
+bool Testbed::loop_animation() {
+	return m_camera_path.m_loop;
+}
+
+void Testbed::set_loop_animation(bool value) {
+	m_camera_path.m_loop = value;
+}
+
 NGP_NAMESPACE_END
 
-- 
GitLab