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
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
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
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)
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)
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
Build Your Plugin
cd build
cmake ..
make opentrack-tracker-myplugin
Install Plugin
Plugins are installed to ${CMAKE_INSTALL_PREFIX}/lib/opentrack/ or similar.
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.
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