Skip to main content
OpenTrack has a modular plugin architecture that allows you to extend functionality by creating custom trackers, filters, and protocols. All plugins use the same base API defined in api/plugin-api.hpp.

Plugin Architecture Overview

Plugin Types

OpenTrack supports three types of plugins:

Trackers

Read input from hardware devices or algorithms to generate head pose data (XYZ position + Yaw, Pitch, Roll)

Filters

Process and smooth raw tracking data to reduce noise and improve responsiveness

Protocols

Send processed tracking data to games and simulators using various output methods

Pose Data Structure

All plugins work with a 6-DOF pose:
using Pose = Mat<double, 6, 1>;

enum Axis : int
{
    NonAxis = -1,
    TX = 0, TY = 1, TZ = 2,        // Translation X, Y, Z (position)
    Yaw = 3, Pitch = 4, Roll = 5,  // Rotation (orientation)
    Axis_MIN = TX, Axis_MAX = 5,
    Axis_COUNT = 6,
};

Setting Up Your Plugin

1

Create Plugin Directory

Create a directory for your plugin in the OpenTrack source tree:
cd opentrack
mkdir tracker-myplugin  # or filter-myplugin, proto-myplugin
cd tracker-myplugin
2

Create CMakeLists.txt

Create a minimal CMakeLists.txt:
otr_module(tracker-myplugin)
The otr_module() function automatically:
  • Finds all .cpp, .h, .hpp, .ui, and .qrc files
  • Links against Qt and OpenTrack APIs
  • Sets up installation rules
  • Configures translations
3

Create Plugin Files

Create header and implementation files:
  • myplugin.h - Class declarations
  • myplugin.cpp - Implementation
  • myplugin_dialog.cpp - UI dialog (optional)
  • myplugin.ui - Qt Designer UI file (optional)
4

Link Against OpenTrack API

Important: You must link against opentrack-api in CMakeLists.txt to avoid vtable link errors:
otr_module(tracker-myplugin)
target_link_libraries(opentrack-tracker-myplugin PRIVATE opentrack-api)

Developing a Tracker Plugin

Tracker Interface

Implement the ITracker interface:
struct OTR_API_EXPORT ITracker
{
    ITracker();
    virtual ~ITracker();
    
    // Start tracking, optionally use frame for video display
    virtual module_status start_tracker(QFrame* frame) = 0;
    
    // Return XYZ yaw pitch roll data
    // Don't block here - use a separate thread for computation
    virtual void data(double *data) = 0;
    
    // Called when user centers tracking
    // Return true to make identity the center pose
    virtual bool center();
    
    static module_status status_ok();
    static module_status error(const QString& error);
};

Complete Tracker Example

Here’s a complete working tracker that generates sinusoidal test data:
#pragma once
#include "api/plugin-api.hpp"
#include "compat/timer.hpp"
#include <cmath>

class my_tracker : public ITracker
{
public:
    my_tracker();
    ~my_tracker() override;
    module_status start_tracker(QFrame *) override;
    void data(double *data) override;

private:
    double last[6] {};
    Timer t;
};

class my_dialog : public ITrackerDialog
{
    Q_OBJECT
public:
    my_dialog();
    void register_tracker(ITracker *) override {}
    void unregister_tracker() override {}
};

class my_metadata : public Metadata
{
    Q_OBJECT
    
    QString name() override { return tr("My Tracker"); }
    QIcon icon() override { return QIcon(":/images/icon.png"); }
};

Tracker Best Practices

The data() method is called 250 times per second. Never perform blocking operations or heavy computation in this method.

Use Separate Threads

Perform heavy computation (image processing, sensor polling) in a separate thread and cache results

Initialize in start_tracker()

Open devices, allocate resources, and start threads in start_tracker(), not in the constructor

Return Status Properly

Return status_ok() on success or error("message") with a descriptive error message

Clean Up in Destructor

Stop threads, release resources, and close devices in the destructor

Developing a Filter Plugin

Filter Interface

struct OTR_API_EXPORT IFilter : module_status_mixin
{
    IFilter();
    ~IFilter() override;
    
    // Initialize the filter
    virtual module_status initialize() = 0;
    
    // Perform filtering step
    // You have to handle dt (delta time) yourself
    virtual void filter(const double *input, double *output) = 0;
    
    // Optionally reset the filter when centering
    virtual void center() {}
};

Filter Example

class my_filter : public IFilter
{
public:
    my_filter();
    
    module_status initialize() override
    {
        // Initialize filter state
        for (int i = 0; i < 6; i++)
            last_output[i] = 0;
        
        return status_ok();
    }
    
    void filter(const double *input, double *output) override
    {
        // Simple exponential smoothing
        const double alpha = 0.3;  // Smoothing factor
        
        for (int i = 0; i < 6; i++)
        {
            output[i] = alpha * input[i] + (1 - alpha) * last_output[i];
            last_output[i] = output[i];
        }
    }
    
    void center() override
    {
        // Reset filter state when user centers
        for (int i = 0; i < 6; i++)
            last_output[i] = 0;
    }
    
private:
    double last_output[6];
};

OPENTRACK_DECLARE_FILTER(my_filter, my_filter_dialog, my_metadata)

Developing a Protocol Plugin

Protocol Interface

struct OTR_API_EXPORT IProtocol : module_status_mixin
{
    IProtocol();
    ~IProtocol() override;
    
    // Initialize the protocol
    virtual module_status initialize() = 0;
    
    // Called 250 times a second with XYZ yaw pitch roll pose
    // Try not to perform intense computation here - use a thread
    virtual void pose(const double* pose, const double* raw) = 0;
    
    // Return game name or placeholder text
    virtual QString game_name() = 0;
};

Protocol Example

class my_protocol : public IProtocol
{
public:
    my_protocol() : sock(this) {}
    
    module_status initialize() override
    {
        if (!sock.bind(QHostAddress::Any, 4242))
            return error(tr("Can't bind to port 4242"));
        
        return status_ok();
    }
    
    void pose(const double* pose, const double* raw) override
    {
        // Send pose data over UDP
        QByteArray data;
        QDataStream stream(&data, QIODevice::WriteOnly);
        
        for (int i = 0; i < 6; i++)
            stream << pose[i];
        
        sock.writeDatagram(data, QHostAddress::LocalHost, 4242);
    }
    
    QString game_name() override
    {
        return tr("My Game Protocol");
    }
    
private:
    QUdpSocket sock;
};

OPENTRACK_DECLARE_PROTOCOL(my_protocol, my_protocol_dialog, my_metadata)

Plugin Metadata

Metadata Class

Every plugin must provide metadata:
class OTR_API_EXPORT Metadata_
{
public:
    Metadata_();
    virtual ~Metadata_();
    
    // Plugin name displayed in the interface
    virtual QString name() = 0;
    
    // Plugin icon (can return empty QIcon())
    virtual QIcon icon() = 0;
};

// Use the Metadata base class for Qt support
class OTR_API_EXPORT Metadata : public TR, public Metadata_
{
    Q_OBJECT
public:
    Metadata();
    ~Metadata() override;
};

Plugin Dialogs

Plugins can provide configuration dialogs:

Base Dialog

class OTR_API_EXPORT BaseDialog : public QDialog
{
    Q_OBJECT
protected:
    BaseDialog();
public:
    void closeEvent(QCloseEvent *) override;
    virtual bool embeddable() noexcept;
    virtual void set_buttons_visible(bool x);
    virtual void save();     // Save settings
    virtual void reload();   // Reload settings
signals:
    void closing();
};

Dialog Example

class my_dialog : public ITrackerDialog
{
    Q_OBJECT
    
    Ui::my_ui ui;  // From .ui file
    
public:
    my_dialog()
    {
        ui.setupUi(this);
        connect(ui.buttonBox, &QDialogButtonBox::accepted,
                this, &my_dialog::doOK);
        connect(ui.buttonBox, &QDialogButtonBox::rejected,
                this, &my_dialog::doCancel);
    }
    
    void register_tracker(ITracker *tracker) override
    {
        // Receive pointer to tracker instance
        m_tracker = tracker;
    }
    
    void unregister_tracker() override
    {
        // Tracker is about to be deleted
        m_tracker = nullptr;
    }
    
private slots:
    void doOK() { save(); close(); }
    void doCancel() { close(); }
    
private:
    ITracker* m_tracker = nullptr;
};

Plugin Declaration Macros

Use these macros to export your plugin:
// For trackers
OPENTRACK_DECLARE_TRACKER(tracker_class, dialog_class, metadata_class)

// For filters  
OPENTRACK_DECLARE_FILTER(filter_class, dialog_class, metadata_class)

// For protocols
OPENTRACK_DECLARE_PROTOCOL(protocol_class, dialog_class, metadata_class)
These macros expand to export the required functions:
extern "C"
{
    OTR_PLUGIN_EXPORT ConstructorClass* GetConstructor(void);
    OTR_PLUGIN_EXPORT Metadata_* GetMetadata(void);
    OTR_PLUGIN_EXPORT DialogClass* GetDialog(void);
}

Module Status Handling

Returning Status

// Return success
return status_ok();

// Return error with message
return error(tr("Failed to open device"));

// Check if status is OK
module_status s = initialize();
if (!s.is_ok())
{
    qDebug() << "Error:" << s.error;
}

Building and Testing

1

Build Your Plugin

cd build
cmake ..
make opentrack-tracker-myplugin
2

Install Plugin

make install
Plugins are installed to ${CMAKE_INSTALL_PREFIX}/lib/opentrack/ or similar.
3

Test in OpenTrack

Run OpenTrack and select your plugin from the appropriate dropdown (Input, Filter, or Output).

Additional Resources

Plugin API Header

Complete API reference with documentation

Test Tracker Example

Simple working example plugin

Core Hacking Guide

Guide for working with OpenTrack core code

Building from Source

Set up your build environment

Tips and Best Practices

The UI thread and tracking thread are separate. Use proper synchronization (mutexes, atomics) when sharing data between threads.
  • Avoid memory allocations in hot paths (data(), filter(), pose())
  • Use double-buffering for thread communication
  • Profile your code to find bottlenecks
  • Always return descriptive error messages
  • Use qDebug() for logging during development
  • Handle device disconnection gracefully
  • Use Qt’s signal/slot mechanism for UI updates
  • Store settings using opentrack-options API
  • Support translations with tr() strings