diff --git a/applications/reconstruct/src/main.cpp b/applications/reconstruct/src/main.cpp
index c8cc100b7e64d5a59f0a24a6d6bd808d60d01586..382f492935084369ef2440d9a198ffb4878f9818 100644
--- a/applications/reconstruct/src/main.cpp
+++ b/applications/reconstruct/src/main.cpp
@@ -37,6 +37,7 @@
 #include <ftl/operators/segmentation.hpp>
 #include <ftl/operators/mask.hpp>
 #include <ftl/operators/antialiasing.hpp>
+#include <ftl/operators/clipping.hpp>
 
 #include <ftl/cuda/normals.hpp>
 #include <ftl/registration.hpp>
@@ -320,6 +321,7 @@ static void run(ftl::Configurable *root) {
 
 	// Create the source depth map pipeline
 	auto *pipeline1 = ftl::config::create<ftl::operators::Graph>(root, "pre_filters");
+	pipeline1->append<ftl::operators::ClipScene>("clipping");
 	pipeline1->append<ftl::operators::ColourChannels>("colour");  // Convert BGR to BGRA
 	//pipeline1->append<ftl::operators::HFSmoother>("hfnoise");  // Remove high-frequency noise
 	pipeline1->append<ftl::operators::Normals>("normals");  // Estimate surface normals
diff --git a/components/operators/CMakeLists.txt b/components/operators/CMakeLists.txt
index d7db869cbe25bd5fa0d9860bc0b1219471b5c410..20dbced5e10557c438a414fbc12d64940c892d5d 100644
--- a/components/operators/CMakeLists.txt
+++ b/components/operators/CMakeLists.txt
@@ -17,6 +17,7 @@ set(OPERSRC
 	src/mask.cpp
 	src/antialiasing.cpp
 	src/antialiasing.cu
+	src/clipping.cpp
 )
 
 
diff --git a/components/operators/include/ftl/operators/clipping.hpp b/components/operators/include/ftl/operators/clipping.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..590e714c9eeca9713e56139aa874f7c0db0a472d
--- /dev/null
+++ b/components/operators/include/ftl/operators/clipping.hpp
@@ -0,0 +1,27 @@
+#ifndef _FTL_OPERATORS_CLIPPING_HPP_
+#define _FTL_OPERATORS_CLIPPING_HPP_
+
+#include <ftl/operators/operator.hpp>
+#include <ftl/cuda_common.hpp>
+
+namespace ftl {
+namespace operators {
+
+/**
+ * Calculate rough normals from local depth gradients.
+ */
+class ClipScene : public ftl::operators::Operator {
+	public:
+    explicit ClipScene(ftl::Configurable*);
+    ~ClipScene();
+
+	inline Operator::Type type() const override { return Operator::Type::ManyToMany; }
+
+    bool apply(ftl::rgbd::FrameSet &in, ftl::rgbd::FrameSet &out, cudaStream_t stream) override;
+
+};
+
+}
+}
+
+#endif  // _FTL_OPERATORS_CLIPPING_HPP_
diff --git a/components/operators/src/clipping.cpp b/components/operators/src/clipping.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..b37abe2c33b55b86b697565d1565b523c073d80f
--- /dev/null
+++ b/components/operators/src/clipping.cpp
@@ -0,0 +1,60 @@
+#include <ftl/operators/clipping.hpp>
+#include <ftl/cuda/points.hpp>
+#include <ftl/utility/matrix_conversion.hpp>
+
+using ftl::operators::ClipScene;
+using ftl::codecs::Channel;
+using ftl::rgbd::Format;
+
+ClipScene::ClipScene(ftl::Configurable *cfg) : ftl::operators::Operator(cfg) {
+
+}
+
+ClipScene::~ClipScene() {
+
+}
+
+// TODO: Put in common place
+static Eigen::Affine3d create_rotation_matrix(float ax, float ay, float az) {
+  Eigen::Affine3d rx =
+      Eigen::Affine3d(Eigen::AngleAxisd(ax, Eigen::Vector3d(1, 0, 0)));
+  Eigen::Affine3d ry =
+      Eigen::Affine3d(Eigen::AngleAxisd(ay, Eigen::Vector3d(0, 1, 0)));
+  Eigen::Affine3d rz =
+      Eigen::Affine3d(Eigen::AngleAxisd(az, Eigen::Vector3d(0, 0, 1)));
+  return rz * rx * ry;
+}
+
+bool ClipScene::apply(ftl::rgbd::FrameSet &in, ftl::rgbd::FrameSet &out, cudaStream_t stream) {
+	auto &c = *config();
+	float rx = c.value("pitch", 0.0f);
+	float ry = c.value("yaw", 0.0f);
+	float rz = c.value("roll", 0.0f);
+	float x = c.value("x", 0.0f);
+	float y = c.value("y", 0.0f);
+	float z = c.value("z", 0.0f);
+	float width = c.value("width", 1.0f);
+	float height = c.value("height", 1.0f);
+	float depth = c.value("depth", 1.0f);
+
+	Eigen::Affine3f r = create_rotation_matrix(rx, ry, rz).cast<float>();
+	Eigen::Translation3f trans(Eigen::Vector3f(x,y,z));
+	Eigen::Affine3f t(trans);
+
+	ftl::cuda::ClipSpace clip;
+	clip.origin = MatrixConversion::toCUDA(r.matrix() * t.matrix());
+	clip.size = make_float3(width, height, depth);
+		
+	for (size_t i=0; i<in.frames.size(); ++i) {	
+		auto &f = in.frames[i];
+		auto *s = in.sources[i];
+
+		auto pose = MatrixConversion::toCUDA(s->getPose().cast<float>());
+
+		auto sclip = clip;
+		sclip.origin = sclip.origin * pose;
+		ftl::cuda::clipping(f.createTexture<float>(Channel::Depth), s->parameters(), sclip, stream);
+	}
+
+	return true;
+}
diff --git a/components/operators/src/operator.cpp b/components/operators/src/operator.cpp
index e55c181c4604a53c2b1090bdee8233242bb6f177..dd821ee6fdd2b967e5ac46f3a4f77c57d59aed6b 100644
--- a/components/operators/src/operator.cpp
+++ b/components/operators/src/operator.cpp
@@ -46,16 +46,32 @@ bool Graph::apply(FrameSet &in, FrameSet &out, cudaStream_t stream) {
 	if (in.frames.size() != out.frames.size()) return false;
 
 	for (auto &i : operators_) {
-		// Make sure there are enough instances
-		while (i.instances.size() < in.frames.size()) {
+		if (i.instances.size() < 1) {
 			i.instances.push_back(i.maker->make());
 		}
 
-		for (int j=0; j<in.frames.size(); ++j) {
-			auto *instance = i.instances[j];
+		if (i.instances[0]->type() == Operator::Type::OneToOne) {
+			// Make sure there are enough instances
+			while (i.instances.size() < in.frames.size()) {
+				i.instances.push_back(i.maker->make());
+			}
+
+			for (int j=0; j<in.frames.size(); ++j) {
+				auto *instance = i.instances[j];
+
+				if (instance->enabled()) {
+					instance->apply(in.frames[j], out.frames[j], in.sources[j], stream_actual);
+				}
+			}
+		} else if (i.instances[0]->type() == Operator::Type::ManyToMany) {
+			auto *instance = i.instances[0];
 
 			if (instance->enabled()) {
-				instance->apply(in.frames[j], out.frames[j], in.sources[j], stream_actual);
+				try {
+					instance->apply(in, out, stream_actual);
+				} catch (const std::exception &e) {
+					LOG(ERROR) << "Operator exception: " << e.what();
+				}
 			}
 		}
 	}