This topic focuses on exposing what has been developed using Open Reality SDK into Python using a third party library called Boost.Python. Boost.Python is a C++ library which enables seamless interoperability between C++ and Python. “pyfbsdk” includes the Boost.Python header files and lib, Python header files and lib. It also has initial utilities to ease your exposing work, and one exposed class hierarchy corresponding exactly to the ORSDK. Custom Python classes can be constructed on this class hierarchy.
In general, it’s fairly intuitive to use the convenient and concise interface of Boost.Python to binding C++ classes and functions to Python. A template project called ‘orpyfbsdk_template’ is located in the samples directory of OpenRealitySDK for reference. Follow the below steps to build custom development project using OR SDK & Python.
Add the pyfbsdk, Boost, and Python include directories to your project's Additional Include Directories. These directories are located in the include directory under the OpenRealitySDK directory.
Both Python 2 and Python 3 versions of the include files are provided. Keep in mind that a Python 3 plug-in will only work if MotionBuilder is running in Python 3 mode. Similarly with Python 2. Loading a Python 2 plug-in when MotionBuilder is running in Python 3 mode can lead to a crash. The same will happen if you attempt to load a Python 3 plug-in when MotionBuilder is running in Python 2 mode.
Add the boost.python libraries and the pyfbsdk libraries to the your project's Additional Dependencies. These libraries are located in the lib directory under the OpenRealitySDK directory. Both Python 2 and Python 3 versions of the libraries are provided.
To use pyfbsdk, you need to include the header files in your code:
#include <fbsdk/fbsdk.h>
#include <pyfbsdk/pyfbsdk.h>
To use Boost.Python, you need to include the header file in your code:
#include <boost/python.hpp>
using namespace boost::python;
In the source file of project, add the following to define the Python module initialization function:
BOOST_PYTHON_MODULE(modulename)
{
ORModelUser_Init();
…
…
}
The ‘modulename’ must exactly match the name of the module, usually it's the same name as $(TargetName). Use a post-build event to rename the module by file extension “.pyd” into python directory to make sure the module could be found and loaded, meanwhile also keep the corresponding dll to use in MotionBuilder.
"copy $(TargetPath) ..\..\..\..\bin\$(PlatformName)\python\lib\plat-win\$(TargetName).pyd"
Beside the ‘ORModelUser_Init’, put the exposing code that uses Boost.Python. So it will be executed when the module initialization function is invoked during module loading.
The classes currently in the ORSDK, the classes implemented using the ORSDK, and in theory all classes implemented in C++ can be exposed into Python. In order to clarify the classes intend to be exposed to Python, the internal C++ classes are first wrapped with a wrapper class. This wrapper class has the same name as the internal class name concatenated by ‘_Wrapper’, and has the same position in class hierarchy corresponding to the class hierarchy of ORSDK. This wrapper class is implemented by an instance of the internal class usually. This wrapper class also allows us to restrict, modify and clarify what members of internal class are exposed.
For example, a class ‘ORModelUser’ inherited from ‘FBModel’ will be wrapped by a class ‘ORModelUser_Wrapper’ inherited from ‘FBModel_Wrapper’. The ‘ORModelUser_Wrapper’ is implemented by an instance of ‘ORModelUser’, as follows:
class ORModelUser : public FBModel
…
typedef ORModelUser *HORModelUser;
class ORModelUser_Wrapper : public FBModel_Wrapper {
public:
HORModelUser mORModelUser;
…
The exposing code looks like this:
class_<ORModelUser_Wrapper,bases<FBModel_Wrapper>("ORModelUser",init<char*>())
;
…
// class_<”class_name”,bases<”base_class_name”>("exposed_class_name", init<” initialization constructor parameters”)>())
This needs to be included in the module initialization function.
The code that is exposed will be used like this:
>>> import modulename
>>> lModelUser = modulename.ORModelUser()
Here we will look into specific functionality to expose:
An optional bases<...> argument to the class_<...> template parameter list is used to represent the inheritance relationship, such as FBModel_Wrapper above.
class_<Derived, bases<Base1,Base2> >("Derived")
...
Derived automatically inherits all of Base's Python methods (wrapped C++ member functions), if Base is polymorphic, Derived objects which have been passed to Python via a pointer or reference to Base can be passed where a pointer or reference to Derived is expected. It's also possible to derive new Python classes from exposed C++ class instances.
If one class has only an implicit default constructor, Boost.Python exposes the default constructor by default, in this case the initialization constructor is not necessary.
If the constructor is intended not to be exposed, the “no_init” can be specified.
A class may have additional constructors, these can be exposed as well by passing more instances of init<...> to def():
class_<ORModelUser_Wrapper,bases<FBModel_Wrapper>("ORModelUser",init<char*>())
.def(init< char*, double>())
;
...
To expose Properties, you can expose them as attributes in Python as follows:
class_<ORShaderScreen_Wrapper,bases<FBShader_Wrapper>, ("ORShaderScreen",init<char*>())
.add_property( "Intensity", &ORShaderScreen_Wrapper::GetIntensity, &ORShaderScreen_Wrapper::SetIntensity)
;
//.add_property( "attribute_name", &”getter member function”, //&”setter member function”)
// ;
...
If the data member is read only, the setter function pointer may not be provided.
If “Intensity’ is a publicly-accessible data member in the class ‘ORShaderScreen_Wrapper’, it also can be directly exposed as either read-only or read/write attributes:
class_<ORShaderScreen_Wrapper,bases<FBShader_Wrapper>, ("ORShaderScreen",init<char*>())
. def_readonly("Intensity ", & ORShaderScreen_Wrapper::Intensity)
;
...
There is a set of Macro that simplifies the attribute exposing: DECLARE_ORSDK_PROPERTY_PYTHON_ACCESS, DEFINE_ORSDK_PROPERTY_PYTHON_ACCESS, and ADD_ORSDK_PROPERTY_PYTHON_ACCESS.
These macros can be used as follows:
DECLARE_ORSDK_PROPERTY_PYTHON_ACCESS(IsDeformable, bool)
…
DEFINE_ORSDK_PROPERTY_PYTHON_ACCESS(FBModel, IsDeformable, bool)
class_<FBModel_Wrapper,bases<FBBox_Wrapper>, boost::noncopyable >("FBModel",no_init)
ADD_ORSDK_PROPERTY_PYTHON_ACCESS(FBModel, IsDeformable)
…
Boost.Python provides a concise mechanism for wrapping operator overloads. For example, when these operators are defined:
FBVector2d_Wrapper* operator+ (const FBVector2d_Wrapper &pVector2d);
FBVector2d_Wrapper& operator+= (const FBVector2d_Wrapper &pVector2d);
FBVector2d_Wrapper* operator+ (double pD);
FBVector2d_Wrapper& operator+= (double pD);
FBVector2d_Wrapper* operator- ();
…
They will be exposed as follows:
class_<FBVector2d_Wrapper>("FBVector2d")
.def(self + self)
.def(self += self)
.def(self + double())
.def(self += double())
.def(-self)
…
When new Python classes are derived from exposed C++ class instances, and the C++ virtual functions in the class are also exposed. Then the functions can be used polymorphically, if the Python method implementation overrides the implementation of C++ virtual functions. We must build a special derived class to dispatch a polymorphic class' virtual functions. Refer to Boost.Python online documentation for details. Both the pure virtual function and non-pure function can be exposed by a special derived class.
Python has a few special methods. These special methods have fix meaning for Python class. A similar set of intuitive interfaces can also be used to wrap C++ functions that correspond to these Python special functions. For example, the predefined special method ‘__repr__’,’ __len__’,’ __getitem__’,’ __setitem__’ below:
std::ostream& operator <<(std::ostream& pStream,const FBColor_Wrapper& pRight);
boost::python::str FBColor_Repr(const FBColor_Wrapper& pFBColor_Wrapper);
class_<FBColor_Wrapper>("FBColor")
.def("__repr__",FBColor_Repr)
.def("__len__",&FBColor_Wrapper::GetCount)
.def("__getitem__",&FBColor_Wrapper::Get)
.def("__setitem__",&FBColor_Wrapper::Set)
…
Exposing functions is fairly intuitive. Both the member function and the non-member function have the same schema. Given a C++ function:
double FBDot_Wrapper(FBVector4d_Wrapper& pV1,FBVector4d_Wrapper& pV2)
The exposing code will be:
def("FBDot", FBDot_Wrapper);
When overloading functions, we have other things to consider. Here is how to manually wrap overloaded functions:
double FBLength_Wrapper( FBVertex_Wrapper& pQ );
double FBLength_Wrapper( FBVector4d_Wrapper& pV );
Exposing code goes here:
double (*FBLength_Wrapper_1)( FBVector4d_Wrapper& pV) = FBLength_Wrapper;
double (*FBLength_Wrapper_2)( FBVertex_Wrapper& pQ ) = FBLength_Wrapper;
def("FBLength", FBLength_Wrapper_1);
def("FBLength", FBLength_Wrapper_2);
The same technique can be applied to wrapping overloaded member functions:
void ApplyUniqueShader(FBShader_Wrapper& pShader);
void ApplyUniqueShader(ORShaderType pShaderType);
Exposing code goes here:
void (ORModelUser_Wrapper::*ApplyUniqueShader_1)(FBShader_Wrapper& pShader) = &ORModelUser_Wrapper::ApplyUniqueShader;
void (ORModelUser_Wrapper::*ApplyUniqueShader_2)(ORShaderType pShaderType) = &ORModelUser_Wrapper::ApplyUniqueShader;
class_<ORModelUser_Wrapper,bases<FBModel_Wrapper>, >("ORModelUser",init<char*>())
.def("ApplyUniqueShader",ApplyUniqueShader_1)
.def("ApplyUniqueShader",ApplyUniqueShader_2)
;
When encountering functions with default arguments, such as this:
object FBFindModelByName_Wrapper( char* pName, FBModel_Wrapper* pParent=NULL )
Use the Macro BOOST_PYTHON_FUNCTION_OVERLOADS to handle this situation:
BOOST_PYTHON_FUNCTION_OVERLOADS(global__FBFindModelByName_Wrapper_Overloads, FBFindModelByName_Wrapper, 1, 2)
def("FBFindModelByName",FBFindModelByName_Wrapper,global__FBFindModelByName_Wrapper_Overloads());
For member functions:
// in class FBTime_Wrapper
kLongLong GetFrame(bool pCummul = false, FBTimeMode pTimeMode = kFBTimeModeDefault, double pFramerate = 0.0)
Use the Macro BOOST_PYTHON_MEMBER_FUNCTION_OVERLOADS:
BOOST_PYTHON_MEMBER_FUNCTION_OVERLOADS(FBTime_Wrapper__GetFrame_Overloads, FBTime_Wrapper::GetFrame,0,3)
class_<FBTime_Wrapper >("FBTime",init<optional<kLongLong> >())
.def("GetFrame",&FBTime_Wrapper::GetFrame,FBTime_Wrapper__GetFrame_Overloads())
;
There are some predefined call policies specified when a function is defined. These policies manage the lifetimes and references among the parameters and the return values to make safe function calls. The most frequently used is the return_value_policy< manage_new_object> , which indicates that the function return a pointer to an Python instance that needs to be adopted by Python.
// in class FBTimeSpan_Wrapper
FBTime_Wrapper* GetStart() ;
…
class_<FBTimeSpan_Wrapper >("FBTimeSpan") .def("GetStart",&FBTimeSpan_Wrapper::GetStart,return_value_policy<manage_new_object>())
;
Boost.Python has a good facility to capture and wrap C++ enums. Given the C++ enum:
enum ORShaderType {
kORShaderCg,
kORShaderTemplate,
kORShaderScreen,
kORShaderTexMat,
};
The exposing code will be:
enum_<ORShaderType>("ORShaderType")
.value("kORShaderCg", kORShaderCg)
.value("kORShaderTemplate", kORShaderTemplate)
.value("kORShaderScreen", kORShaderScreen)
.value("kORShaderTexMat", kORShaderTexMat)
;
Boost.Python provides a class object to enable the conversion to Python from C++ objects of arbitrary type.
object s("hello, world"); // s manages a Python string
The extract
double x = extract<double>(o);
The object type is accompanied by a set of derived types that mirror the Python built-in types such as list, dict, tuple, str, long_, enum, etc. They look and work like regular Python code.
There is a Macro used when defining an exposed class.
REGISTER_FBWRAPPER( ORModelUser );
This registers the factory function to the Wrapper Factory. The factory function makes the conversion from internal class to the Wrapper class easy. It also registers the Wrapper class to the Boost shared_ptr mechanism and defines the type information for the exposed class.