Skip to main content
📺 RViz2

ROS 2 RViz2 Custom Plugin Guide 2026

RViz2 is extensible through pluginlib — add custom Display plugins that subscribe to any topic, Interactive Markers for 6-DOF pose handles, and Panel widgets with Qt controls. This guide walks each type with working C++ code.

Plugin Types Overview

Display Plugin

rviz_common::Display / MessageFilterDisplay<T>

Visualize a custom message type — draw markers, meshes, text in the 3D view

Interactive Marker

visualization_msgs/InteractiveMarker

Drag-and-drop 6-DOF handles in 3D space, get feedback callbacks when moved

Panel Plugin

rviz_common::Panel

Embed Qt widgets (buttons, sliders, text) in RViz2's side panel dock

Display Plugin: Package Setup

<!-- package.xml -->
<depend>rviz_common</depend>
<depend>rviz_rendering</depend>
<depend>pluginlib</depend>
<depend>std_msgs</depend>
<depend>sensor_msgs</depend>
# CMakeLists.txt
find_package(rviz_common REQUIRED)
find_package(rviz_rendering REQUIRED)
find_package(pluginlib REQUIRED)
find_package(Qt5 COMPONENTS Widgets REQUIRED)
set(CMAKE_AUTOMOC ON)

add_library(my_rviz_plugins SHARED
  src/my_display.cpp
  src/my_panel.cpp
)
target_include_directories(my_rviz_plugins PUBLIC include)
ament_target_dependencies(my_rviz_plugins rviz_common rviz_rendering pluginlib)
target_link_libraries(my_rviz_plugins Qt5::Widgets)

pluginlib_export_plugin_description_file(rviz_common plugins_description.xml)
install(TARGETS my_rviz_plugins DESTINATION lib/${PROJECT_NAME})

plugins_description.xml

<library path="my_rviz_plugins">

  <class name="my_rviz_plugins/MyDisplay"
         type="my_rviz_plugins::MyDisplay"
         base_class_type="rviz_common::Display">
    <description>Displays MyCustomMsg as colored spheres</description>
  </class>

  <class name="my_rviz_plugins/MyPanel"
         type="my_rviz_plugins::MyPanel"
         base_class_type="rviz_common::Panel">
    <description>Control panel with speed slider</description>
  </class>

</library>

Display Plugin: MessageFilterDisplay

MessageFilterDisplay<T> adds a topic property and TF-aware message queue automatically:

// include/my_rviz_plugins/my_display.hpp
#pragma once
#include "rviz_common/message_filter_display.hpp"
#include "sensor_msgs/msg/point_cloud2.hpp"

namespace my_rviz_plugins {

class MyDisplay
  : public rviz_common::MessageFilterDisplay<sensor_msgs::msg::PointCloud2>
{
  Q_OBJECT
public:
  MyDisplay();
  ~MyDisplay() override;

protected:
  void onInitialize() override;
  void reset() override;
  void processMessage(sensor_msgs::msg::PointCloud2::ConstSharedPtr msg) override;

private:
  // Ogre3D scene node for your visual objects
  Ogre::ManualObject * manual_object_{nullptr};
};

}  // namespace my_rviz_plugins
// src/my_display.cpp
#include "my_rviz_plugins/my_display.hpp"
#include "rviz_rendering/objects/shape.hpp"
#include <pluginlib/class_list_macros.hpp>

namespace my_rviz_plugins {

MyDisplay::MyDisplay() = default;
MyDisplay::~MyDisplay() = default;

void MyDisplay::onInitialize()
{
  MessageFilterDisplay<sensor_msgs::msg::PointCloud2>::onInitialize();
  // Create Ogre objects here (attached to scene_node_)
  manual_object_ = scene_manager_->createManualObject();
  scene_node_->attachObject(manual_object_);
}

void MyDisplay::reset()
{
  MessageFilterDisplay<sensor_msgs::msg::PointCloud2>::reset();
  manual_object_->clear();
}

void MyDisplay::processMessage(
  sensor_msgs::msg::PointCloud2::ConstSharedPtr msg)
{
  // Called on RViz2's update thread — draw your custom visual here
  manual_object_->clear();
  manual_object_->begin(
    "BaseWhiteNoLighting", Ogre::RenderOperation::OT_POINT_LIST);

  // Iterate point cloud and add points
  for (size_t i = 0; i < msg->width * msg->height; ++i) {
    float x, y, z;
    // ... parse msg->data using PointCloud2Iterator
    manual_object_->position(x, y, z);
    manual_object_->colour(0.0f, 1.0f, 0.0f, 1.0f);  // green
  }

  manual_object_->end();
  context_->queueRender();  // request a redraw
}

}  // namespace my_rviz_plugins

PLUGINLIB_EXPORT_CLASS(my_rviz_plugins::MyDisplay, rviz_common::Display)

Interactive Markers: 6-DOF Pose Handle

InteractiveMarkerServer creates draggable/rotatable handles in RViz2 that fire callbacks when moved:

#include "interactive_markers/interactive_marker_server.hpp"
#include "visualization_msgs/msg/interactive_marker.hpp"
#include "visualization_msgs/msg/interactive_marker_control.hpp"
#include "visualization_msgs/msg/marker.hpp"

using visualization_msgs::msg::InteractiveMarker;
using visualization_msgs::msg::InteractiveMarkerControl;
using visualization_msgs::msg::Marker;

// Helper: add a 6-DOF control ring to an interactive marker
void add6DOFControls(InteractiveMarker & im) {
  InteractiveMarkerControl ctrl;
  ctrl.always_visible = true;

  // Rotate around X
  ctrl.name = "rotate_x";
  ctrl.interaction_mode = InteractiveMarkerControl::ROTATE_AXIS;
  ctrl.orientation.w = 1; ctrl.orientation.x = 1; ctrl.orientation.y = 0; ctrl.orientation.z = 0;
  im.controls.push_back(ctrl);

  // Translate along X
  ctrl.name = "move_x";
  ctrl.interaction_mode = InteractiveMarkerControl::MOVE_AXIS;
  im.controls.push_back(ctrl);

  // Rotate around Y
  ctrl.name = "rotate_y";
  ctrl.interaction_mode = InteractiveMarkerControl::ROTATE_AXIS;
  ctrl.orientation.x = 0; ctrl.orientation.y = 1; ctrl.orientation.z = 0;
  im.controls.push_back(ctrl);

  // Translate along Y
  ctrl.name = "move_y";
  ctrl.interaction_mode = InteractiveMarkerControl::MOVE_AXIS;
  im.controls.push_back(ctrl);

  // Rotate around Z
  ctrl.name = "rotate_z";
  ctrl.interaction_mode = InteractiveMarkerControl::ROTATE_AXIS;
  ctrl.orientation.x = 0; ctrl.orientation.y = 0; ctrl.orientation.z = 1;
  im.controls.push_back(ctrl);

  // Translate along Z
  ctrl.name = "move_z";
  ctrl.interaction_mode = InteractiveMarkerControl::MOVE_AXIS;
  im.controls.push_back(ctrl);
}

// In a ROS 2 node:
auto server = std::make_shared<interactive_markers::InteractiveMarkerServer>(
  "goal_marker",    // topic prefix: /goal_marker/update
  node);            // the owning node

// Create the interactive marker
InteractiveMarker im;
im.header.frame_id = "map";
im.name = "goal_pose";
im.description = "Drag to set goal pose";
im.scale = 0.5f;

// Add a sphere in the center so the user can see it
InteractiveMarkerControl box_ctrl;
box_ctrl.always_visible = true;
box_ctrl.interaction_mode = InteractiveMarkerControl::NONE;
Marker sphere;
sphere.type = Marker::SPHERE;
sphere.scale.x = sphere.scale.y = sphere.scale.z = 0.2;
sphere.color.r = 1.0; sphere.color.a = 1.0;
box_ctrl.markers.push_back(sphere);
im.controls.push_back(box_ctrl);

add6DOFControls(im);

// Callback when user moves the marker
auto processFeedback = [&node](
  const visualization_msgs::msg::InteractiveMarkerFeedback::ConstSharedPtr & fb)
{
  RCLCPP_INFO(node->get_logger(),
    "Marker '%s' moved to (%.2f, %.2f, %.2f)",
    fb->marker_name.c_str(),
    fb->pose.position.x,
    fb->pose.position.y,
    fb->pose.position.z);
};

server->insert(im, processFeedback);
server->applyChanges();

Panel Plugin: Qt Widget in RViz2

// include/my_rviz_plugins/my_panel.hpp
#pragma once
#include "rviz_common/panel.hpp"
#include <QSlider>
#include <QLabel>

namespace my_rviz_plugins {

class MyPanel : public rviz_common::Panel {
  Q_OBJECT
public:
  explicit MyPanel(QWidget * parent = nullptr);

  // Save/load panel state between sessions
  void load(const rviz_common::Config & config) override;
  void save(rviz_common::Config config) const override;

private Q_SLOTS:
  void onSpeedChanged(int value);

private:
  QSlider * speed_slider_{nullptr};
  QLabel * speed_label_{nullptr};
};

}  // namespace my_rviz_plugins
// src/my_panel.cpp
#include "my_rviz_plugins/my_panel.hpp"
#include <QVBoxLayout>
#include <pluginlib/class_list_macros.hpp>

namespace my_rviz_plugins {

MyPanel::MyPanel(QWidget * parent) : rviz_common::Panel(parent)
{
  auto layout = new QVBoxLayout(this);

  speed_label_ = new QLabel("Speed: 0.0 m/s", this);
  speed_slider_ = new QSlider(Qt::Horizontal, this);
  speed_slider_->setRange(0, 20);  // 0.0 – 2.0 m/s * 10
  speed_slider_->setValue(0);

  layout->addWidget(speed_label_);
  layout->addWidget(speed_slider_);

  connect(speed_slider_, &QSlider::valueChanged,
          this, &MyPanel::onSpeedChanged);
}

void MyPanel::onSpeedChanged(int value) {
  double speed = value / 10.0;
  speed_label_->setText(QString("Speed: %1 m/s").arg(speed, 0, 'f', 1));
  // Publish to /cmd_vel or a parameter server here
  Q_EMIT configChanged();
}

void MyPanel::load(const rviz_common::Config & config) {
  Panel::load(config);
  int v; config.mapGetInt("speed", &v);
  speed_slider_->setValue(v);
}

void MyPanel::save(rviz_common::Config config) const {
  Panel::save(config);
  config.mapSetValue("speed", speed_slider_->value());
}

}  // namespace my_rviz_plugins

PLUGINLIB_EXPORT_CLASS(my_rviz_plugins::MyPanel, rviz_common::Panel)

Quick: Publish Basic Markers (No Plugin Needed)

For simple visualization in C++ without a full plugin, publish visualization_msgs/Marker directly:

#include "visualization_msgs/msg/marker.hpp"
#include "rclcpp/rclcpp.hpp"

auto pub = node->create_publisher<visualization_msgs::msg::Marker>(
  "visualization_marker", 10);

visualization_msgs::msg::Marker marker;
marker.header.frame_id = "map";
marker.header.stamp = node->now();
marker.ns = "my_markers";
marker.id = 0;
marker.type = visualization_msgs::msg::Marker::ARROW;
marker.action = visualization_msgs::msg::Marker::ADD;
marker.pose.position.x = 1.0;
marker.pose.orientation.w = 1.0;
marker.scale.x = 0.5;  // arrow length
marker.scale.y = 0.05; // arrow width
marker.scale.z = 0.05; // arrow height
marker.color.r = 1.0; marker.color.a = 1.0;
marker.lifetime = rclcpp::Duration::from_seconds(1.0); // auto-expire

pub->publish(marker);

// In RViz2: Add → Marker, topic = /visualization_marker

Next Steps