diff --git a/rviz_default_plugins/src/rviz_default_plugins/view_controllers/orbit/orbit_view_controller.cpp b/rviz_default_plugins/src/rviz_default_plugins/view_controllers/orbit/orbit_view_controller.cpp
index e7521e90d..c60e39887 100644
--- a/rviz_default_plugins/src/rviz_default_plugins/view_controllers/orbit/orbit_view_controller.cpp
+++ b/rviz_default_plugins/src/rviz_default_plugins/view_controllers/orbit/orbit_view_controller.cpp
@@ -193,6 +193,14 @@ bool OrbitViewController::setMouseMovementFromEvent(
   } else if (dragging_ && event.type == QEvent::MouseMove) {
     diff_x = event.x - event.last_x;
     diff_y = event.y - event.last_y;
+  } else if (dragging_ && event.type == QEvent::Wheel) {
+    diff_x = (event.x - event.last_x) / 1.01;
+    diff_y = (event.y - event.last_y) / 1.01;
+    if (std::abs(diff_x) > 50 || std::abs(diff_y) > 50) {
+      diff_x = 0;
+      diff_y = 0;
+      return false;
+    }
     return true;
   }
   return false;
diff --git a/rviz_rendering/include/rviz_rendering/render_window.hpp b/rviz_rendering/include/rviz_rendering/render_window.hpp
index 34600835b..6a186e4b4 100644
--- a/rviz_rendering/include/rviz_rendering/render_window.hpp
+++ b/rviz_rendering/include/rviz_rendering/render_window.hpp
@@ -36,6 +36,7 @@
 
 #include <QObject>  // NOLINT
 #include <QWindow>  // NOLINT
+#include <QMap>  // NOLINT
 
 #include "OgreSceneNode.h"
 #include "OgreRenderTargetListener.h"
@@ -102,6 +103,12 @@ class RVIZ_RENDERING_PUBLIC RenderWindow : public QWindow
   void
   windowMovedOrResized();
 
+  bool translateTouchToMouseEvent(QTouchEvent* touchEvent);
+
+  QMap<int, QPointF> previousTouchPoints;
+  bool singleTouch = false;
+  bool doubleTouch = false;
+
 public slots:
   virtual
   void
diff --git a/rviz_rendering/src/rviz_rendering/render_window.cpp b/rviz_rendering/src/rviz_rendering/render_window.cpp
index 9d6bd2fcd..58f9b3926 100644
--- a/rviz_rendering/src/rviz_rendering/render_window.cpp
+++ b/rviz_rendering/src/rviz_rendering/render_window.cpp
@@ -153,6 +153,46 @@ ToString(const EnumType & enumValue)
   return QString("%1::%2").arg(enumName).arg(static_cast<int>(enumValue));
 }
 
+bool RenderWindow::translateTouchToMouseEvent(QTouchEvent* touchEvent) {
+    auto touchPoints = touchEvent->touchPoints();
+
+    if (touchPoints.size() == 1) {
+        auto position = touchPoints[0].pos().toPoint();
+        if (touchPoints[0].state() == Qt::TouchPointReleased) {
+            on_mouse_events_callback_(new QMouseEvent(QEvent::MouseButtonRelease, position, Qt::LeftButton, Qt::NoButton, Qt::NoModifier));
+            singleTouch = doubleTouch = false;
+        } else if (!doubleTouch) { // Ignore single touch if we are already dragging or zooming
+            on_mouse_events_callback_(new QMouseEvent(
+                singleTouch ? QEvent::MouseMove : QEvent::MouseButtonPress,
+                position, Qt::LeftButton, Qt::LeftButton, singleTouch ? Qt::ShiftModifier : Qt::NoModifier
+            ));
+            // Left button down first, then mouse move
+            singleTouch = true;
+        }
+        previousTouchPoints.clear();
+    } else if (touchPoints.size() == 2) { // We are in multitouch
+        auto pos1 = touchPoints[0].pos();
+        auto pos2 = touchPoints[1].pos();
+        if (touchPoints[0].state() == Qt::TouchPointReleased || touchPoints[1].state() == Qt::TouchPointReleased) {
+            previousTouchPoints.clear();
+        } else if (previousTouchPoints.contains(touchPoints[0].id()) && previousTouchPoints.contains(touchPoints[1].id())) {
+            qreal delta = QLineF(pos1, pos2).length() - QLineF(previousTouchPoints[touchPoints[0].id()], previousTouchPoints[touchPoints[1].id()]).length();
+            QPointF midpoint = (pos1 + pos2) / 2.0;
+            int deltaInt = std::abs(delta) > 0.5 ? static_cast<int>(delta * 6) : 0;
+            on_wheel_events_callback_(new QWheelEvent(midpoint, midpoint, QPoint(0, deltaInt), QPoint(0, deltaInt), Qt::LeftButton, Qt::NoModifier, Qt::ScrollUpdate, false));
+            doubleTouch = true;
+
+            previousTouchPoints[touchPoints[0].id()] = pos1;
+            previousTouchPoints[touchPoints[1].id()] = pos2;
+            return true;
+        } else {
+          previousTouchPoints[touchPoints[0].id()] = pos1;
+          previousTouchPoints[touchPoints[1].id()] = pos2;
+        }
+    }
+    return false;
+}
+
 bool
 RenderWindow::event(QEvent * event)
 {
@@ -165,6 +205,16 @@ RenderWindow::event(QEvent * event)
     case QEvent::UpdateRequest:
       this->renderNow();
       return true;
+    case QEvent::TouchBegin:
+    case QEvent::TouchUpdate:
+    case QEvent::TouchEnd:
+    {
+      QTouchEvent* touchEvent = static_cast<QTouchEvent*>(event);
+      if (translateTouchToMouseEvent(touchEvent)) {
+        return QWindow::event(event);
+      }
+      return false;
+    }
     case QEvent::Type::MouseMove:
     case QEvent::Type::MouseButtonPress:
     case QEvent::Type::MouseButtonRelease: