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
- → ROS 2 pluginlib guide — the plugin registration system that powers RViz2 extension points
- → ROS 2 PointCloud2 & PCL guide — process clouds before feeding them to your Display plugin
- → ros2_control hardware interface guide — add Interactive Marker goal poses to drive real hardware