Vortex Studio SDK Advanced - Fields And Extensions
Advance Field Types
Listed below are a list of special fields. The all derived from VxData::FieldBase.
Field of Enumeration Type - Field<eType>
Fields of enumeration type are easier to use than a generic Field<VxEnum>. It provides access to the enum type directly.
// in header file class MyExtension : public VxSim::IExtension, public VxGraphics::IGraphic { public: enum eType { kDog, kCat, kElephant, kSpider }; public: MyExtension(VxSim::VxExtension* proxy); VxData::Field<eType> parameterType; }; // in cpp file MyExtension::MyExtension(VxSim::VxExtension* proxy) : VxSim::IExtension(proxy, 0) , VxGraphics::IGraphic(proxy) , parameterType(kSpider , "Animal", &proxy->getParameterContainer()) { } // it is used like an enum void MyExtension::_draw() { if ( parameterType == kSpider ) { [...] } } void MyExtension::useBigAnimal() { parameterType = kElephant; }
In order to have Vortex Toolkit process the enum type information correctly, the enum must be registered to the VxEnum class.
This can be done in the plugin InitializePlugin/UninitializePlugin functions.
// in main plugin file extern "C" SYMBOL bool InitializePlugin(VxPluginSystem::VxPluginManager & pluginManager, int /*argc*/, char ** /*argv*/, std::string * /*error*/) { // register the enum to be able to use Field<MyExtension::eType> // give the enum a name that must be unique, it will be used instead of typeid(MyExtension::eType) Vx::VxEnum::registerEnumeration<MyExtension::eType>("MyExtension_AnimalType") .addValue(MyExtension::kDog, "Dog") .addValue(MyExtension::kCat, "Cat") .addValue(MyExtension::kElephant, "Elephant") .addValue(MyExtension::kSpider , "Spider"); return VxSim::VxExtensionFactory::registerType<MyExtension>(kFactoryKey, pluginManager.getCurrentVXP()); } extern "C" SYMBOL bool UninitializePlugin(VxPluginSystem::VxPluginManager & pluginManager, std::string * /*error*/) { // when plugin is unloaded, enum MUST be unregistered Vx::VxEnum::unregisterEnumeration<MyExtension::eType>(); return VxSim::VxExtensionFactory::unregisterType(kFactoryKey); }
Parsing Content for Enumeration fields
Before Vortex version 6.7, enumeration fields were all defined with Field<VxEnum>. Under the hood, this is still the case. The Field<enum> is only valid when you are dealing with the IExtension, what is really stored within the Container is a Field<VxEnum>. When parsing content using VxExtension* and VxData::Container in a generic way, the FieldBase is a Field<VxEnum>. Calling getType() on the field will yield a type VxData::Types::Type_Enum.
The following example shows the differences.
// The mechanism below has several extensions, one of which is of type MyExtension // But since the code is parsing all extensions, it is done in a generic way for (auto it = mechanism->getExtensions().begin(); it != mechanism->getExtensions().end(); ++it) { const VxData::Container& parameters = it->getExtension()->getParameterContainer(); for (auto it = parameters.begin(); it != parameters.end(); ++it) { VxData::FieldBase& field = *it; // Parsing by type ... else if(field.getType() == Type_VxEnum) { // you get a generic VxEnum VxEnum val = field.getValue<VxEnum>(); int enumValue = val.getValue(); // the type is known, this can be done MyExtension eType = static_cast<MyExtension::eType>(enumValue); // or directly MyExtension eType = field.getValue<MyExtension::eType>(); } ... }
Field of Interface Type - Field<IExtension*>
To refer to other extension, user should use Field<VxExtension*>. However, when you know the actual interface of the Extension that you want to refer to, you could consider using a field of interface.
Those special field are an interface over a Field<VxExtension*> that allows using the field like a VxSmartInterface<I*>, allowing the user to get access to the API directly from the field.
Fields do not normally allow Field of IExtension. In a previous version of Vortex, some fields of specific interfaces were introduced:
- Field<VxDynamics::Part>
- Field<VxDynamics::Assembly>
- Field<VxDynamics::Mechanism>
- Field<VxDynamics::Attachement>
- Field<VxDynamics::AttachementPoint>
- Field<VxDynamics::Constraint>
- Field<VxDynamics::CollisionGeometry>
- Field<VxGraphics::Node>
- Field<VxGraphics::Mesh>
- Field<VxGraphics::Material>
- Field<VxGraphics::Texture>
- Field<VxGraphics::Geometry>
- Field<VxGraphics::Skeleton>
- Field<VxSim::ISimulatorModule>
Vortex Studio may introduce new types as it evolves, but they all work the same.
// in header file class MyOtherExtension : public VxSim::IExtension { public: MyOtherExtension(VxSim::VxExtension* proxy); VxData::Field<VxDynamics::Part> parameterPart; }; // in cpp file MyOtherExtension::MyOtherExtension(VxSim::VxExtension* proxy) : VxSim::IExtension(proxy, 0) , parameterPart(nullptr, "Animal", &proxy->getParameterContainer()) // Value is to be set by code or UI. { } // it is used like an smart interface void MyOtherExtension::_update() { if(paramterPart.getValue() != nullptr) { // using it like a VxSmartInterface<Part> if(parameterPart->outputLinearVelocity.getValue > kMaxVelocity) { ... } } }
Creating your Field<Interface*>
Not all interfaces can be put in a field. In Vortex 2017b, steps were taken to allow a user to create fields of their own type. There are some requirements need to be able to make a Field of your own interface.
If you created your own IExtension and you want to create a Field<T*> where T is your interface, few steps must be taken:
- Use macro VX_DATA_ALLOW_INTERFACE_FIELD(Interface)
- Define a const static VxID named kFieldTypeId, this will be the id of your interface.
- Register your field type ID
Those steps are need to allow your interface field to work.
class MyExtension : public IExtension { ... const static Vx::VxID kFieldTypeId = VxData::Types::registerInterfaceFieldType<MyExtension>("MyExtension", "UI name for MyExtension"); }; VX_DATA_ALLOW_INTERFACE_FIELD(MyExtension) // then the field can be defined in another extension class MyOtherExtension { ... Field<MyExtension> parameterMyExt; // Used like the Part in the example above. };
Parsing Content for Interface fields
Like enumeration fields, interface fields are only valid when you are dealing with an IExtension interface containing them, what is really stored within the Container is a Field<VxExtension*>. When parsing content using VxExtension* and VxData::Container in a generic way, the FieldBase is a Field<VxExtension*>.
Calling getType() on the field will yield a type VxData::Types::Type_ExtensionPtr. To know the interface type, the fieldbase should be cast into a Field<VxExtension*> and getInterfaceType() will give the interface type identifier.
The following example shows generic parsing of extension field.
// The mechanism below has several extensions, one of which is of type MyOtherExtension // But since the code is parsing all extensions, it is done in a generic way for (auto it = mechanism->getExtensions().begin(); it != mechanism->getExtensions().end(); ++it) { const VxData::Container& parameters = it->getExtension()->getParameterContainer(); for (auto it = parameters.begin(); it != parameters.end(); ++it) { VxData::FieldBase& field = *it; // Parsing by type ... else if(field.getType() == Type_VxExtensionPtr) { // you get a generic VxExtension VxExtension* val = field.getValue<VxExtension*>(); // If you need to look at the type auto interfaceType = (static_cast<Field<Part>&>(field)).getInterfaceType(); // interfaceType == Part::kFieldTypeId; } ... }
Container - VxData::Container
Grouping together several data to make a data structure can be done easily with a VxData::Container.
The preferred way to create a custom container is by deriving from VxData::Container, and having fields as class members.
The constructor of the user container should be similar the VxData::Container constructor.
class FluidInteractionContainer : public VxData::Container { public: FluidInteractionContainer ( const Vx::VxID& name, VxData::Container* parent ) : VxData::Container(name, parent) , centerOfBuoyancy(VxMath::Vector3(), "CenterOfBuoyancy", this) , displacedVolume(0.0, "DisplacedVolume", this) , dragTranslationCoefficient(VxMath::Vector3(), "DragCoefficient", this) , liftTranslationCoefficient(VxMath::Vector3(), "LiftCoefficient", this) , dragTorqueScale(VxMath::Vector3(1.0,1.0,1.0), "DragScale", this) { } VxData::Field<VxMath::Vector3> centerOfBuoyancy; VxData::Field<double> displacedVolume; VxData::Field<VxMath::Vector3> dragTranslationCoefficient; VxData::Field<VxMath::Vector3> liftTranslationCoefficient; VxData::Field<VxMath::Vector3> dragTorqueScale; };
For exemple, a VxSim::IDynamic extension could have a container as an output, while a VxGraphics::IGraphic extension has the same container as an input.
When the two are connected, the data flow is guaranteed and all values are correctly transferred.
A container can also be put inside a container.
class Person : public VxData::Container { public: enum eSexe { kMale, kFemale, kUndecided }; Person( const Vx::VxID& name, VxData::Container* parent ) : VxData::Container(name, parent) , name("Name", this) , age(0.0, "Age", this) , sex(kUndecided, "Sex", this) { } VxData::Field<std::string> name; VxData::Field<unsigned int> age; VxData::Field<eSexe> sex; }; class Conference: public VxData::Container { public: Conference( const Vx::VxID& name, VxData::Container* parent ) : VxData::Container(name, parent) , presenter("Speaker", this) , subject("", "Subject", this) { } Person presenter; VxData::Field<std::string> subject; };
Defining a custom container in an extension is very similar to adding a simple VxData::Field<T>.
// in header file class MyExtension : public VxSim::IExtension, public VxGraphics::IGraphic { public: MyExtension(VxSim::VxExtension* proxy); Conference parameterMainEvent; } // in cpp file MyExtension::MyExtension(VxSim::VxExtension* proxy) : VxSim::IExtension(proxy, 0) , VxGraphics::IGraphic(proxy) , parameterMainEvent("MainEvent", &proxy->getParameterContainer()) { }
Adding Fields to a Container at Runtime
In some cases, it might be necessary to add or remove fields at run-times.
This can be done with Container::addField. The added fields are serialized correctly in the file and recreated automatically when the file is loaded again, there is no need to manually recreate them.
class CommunicationExtension : public VxSim::IExtension { VxData::Container inputChannels; public: CommunicationExtension(VxSim::VxExtension* proxy) : inputChannels("Channels", &proxy->getInputContainer()) { } [...] void initChannels(const std::vector<std::string>& channels) { // clear the channels that were previously set (this will delete the fields) inputChannels.clear(); for ( const auto& channel : channels) { // add an input channel std::string name = channel ; // add a Field<double> with the proper name inputChannels.addField(name, VxData::Types::Type_Double); } } };
Array of Fields - VxData::FieldArray<T>
FieldArray<T> is a typed field container that is related to a std::vector<> or Vx::VxArray<>, its API is similar. It is a template class and the template class argument must be a VxData::FieldBase derived class, including VxData::Container and other VxData::FieldArray<>. They are used when you want a variable number of similar items. VxData::FieldArray<T> is designed to be an array of elements of the same type T, where T is a field class.
class MaterialLayer : public VxData::Container { public: MaterialLayer(const Vx::VxID& name, VxData::Container* parent); VxData::Field<std::string> name; VxData::Field<VxGraphics::Texture*> texture; VxData::Field<VxGraphics::Texture*> maskTexture; VxData::Field<Vx::VxReal> factor; VxData::Field<Vx::VxColor> color; }; class Material : public VxSim::IObject, public VxGraphics::IGraphic { public: [...] VxData::FieldArray<MaterialLayer> emissionLayers; }; void Material::_initTextures() { if ( emissionLayers.empty() ) // VxData::FieldArray<> API is similar to std::vector { emissionLayers.resize(1); } for ( auto it = emissionLayers.begin(); it != emissionLayers.end(); ++it ) { if ( (*it)->texture.getValue() != nullptr ) { _initTexture((*it)->texture.getValue()); } } }
Vector - VxData::Vector<T>
VxData::FieldArray<T> is not fast. It is very generic but it does not offer the performance necessary for data that are constantly changing.
This is the main reason for the creation of a field class that has a VxArray<T> as internal data. Since it is contiguous in memory, access and update is much faster.
VxData::Vector<T> cannot be used with all types. Only types having a value are supported.
Numerical | VxData::Vector<int> | |
VxData::Vector<unsigned int> | ||
VxData::Vector<short> | ||
VxData::Vector<unsigned short> | ||
VxData::Vector<double> | same as VxData::Vector<Vx::VxReal> | |
VxData::Vector<bool> | ||
Compound Values | VxData::Vector<Math::Vector2> | |
VxData::Vector<Math::Vector3> | same as VxData::Field<Vx::VxVector3> | |
VxData::Vector<Math::Vector4> | ||
VxData::Vector<Math::Matrix44> | ||
VxData::Vector<Vx::VxTransform > | ||
VxData::Vector<VxMath::Quaternion> | same as VxData::Field<Vx::VxQuaternion> | |
VxData::Vector<Vx::VxColor> | ||
String | VxData::Vector<std::string> | |
VxData::Vector<Vx::VxFilename> |
VxData::Vector<T> are designed to handle large amount of data with little overhead.
For example, a large number of moving particles could be handled with two VxData::Vector<T>.
class ParticlesDataContainer : public VxData::Container { public: ParticlesDataContainer( const Vx::VxID& name, VxData::Container* parent ) : VxData::Container(name, parent) , radii("Radii", this) , positions("Positions", this) { } VxData::Vector< double > radii; VxData::Vector< VxMath::Vector3 > positions; };
Setting up the content of a VxData::Vector<T> is most efficient when directly copying a VxArray<T>. This greatly limits the number of observers called.
class ParticleGenerator : public VxSim::IDynamics, public VxSim::IExtension { public: ParticleGenerator(VxSim::VxExtension* proxy) : VxSim::IDynamics(proxy) , VxSim::IExtension(proxy) , outputParticles("Particles", &proxy->getOutputContainer()) { } [...] ParticlesDataContainer outputParticles; [...] void postStep() { Vx::VxArray<double> r; Vx::VxArray<VxMath::Vector3> p; auto generatedParticles = _generateParticles(); r.reserve(generatedParticles.size()); p.reserve(generatedParticels.size()); for( auto& particle : generatedParticles ) { r.push_back(it->radius); p.push_back(it->position); } // copy temporary data in the outputs : this is the fastest way to update a large number of values in an output outputParticles.radii = r; outputParticles.positions = p; } };
Accessing the data in a VxData::Vector<T> is similar to using a Vx::VxArray<T> or a std::vector<T>.
class ParticleGenerator : public VxGraphics::IGraphic, public VxSim::IExtension { public: ParticleGenerator(VxSim::VxExtension* proxy) : VxGraphics::IGraphic(proxy) , VxSim::IExtension(proxy) , inputParticles("Particles", &proxy->getInputContainer()) { } [...] ParticlesDataContainer inputParticles; [...] void onUpdate() { size_t index = 0; for( auto itRadius = inputParticles.radii.begin(); it != inputParticles.radii.end(); ++it, ++index ) { _drawParticle(*itRadius, inputParticles.positions[index]); } } };
Field Observers
Observers can be used to be notified about a change in a field.
Any field can have an observer. Fields notify their observers whenever they change.
The observer function takes a const FieldBase& argument. The object passed to the observer is the object that is being modified.
The API to add an observer is common to all field types.
VxData::DelegateConnection VxData::FieldBase::addObserver(std::function<void(const FieldBase&)> callback) const;
The VxData::DelegateConnection allows to disconnect() or connect() the observer callback, and to destroy() it. It is important to destroy the observe when it is not useful any longer. Destroy frees the memory associated with the observer, while disconnect temporarily make the observer not responding.
When multiple observers are added, it is convenient to use VxData::ObserverList to control several observers at the same time.
It is good practice to add an observer in the VxSIm::IExtension::onActive callback, and to delete it in the VxSim::IExtension::onInactive callback. This prevents the callbacks to be called during loading and saving, potentially introducing unwanted processing, and it will also prevent the extension to do any action when it is not managed by a module.
On the other hand, if you know your observer will not have any side effects if it is called a bit more often, it is ok to just create it in the constructor.
Observers notifications are immediate; they will be called in the same thread that changed the field. This will bypass the scheduling of the modules.
A better practice is to set a flag and do the processing later. An observers should not change other fields, i.e., neither change a value, nor add or remove a field in a container. It may cascade a series of connections and observers especially when extensions are interconnected.
While in Simulating Application Mode, fields in extensions that are not active will not be called. An active extension is an extension that has a module that manages it.
Single Value Field - VxData::Field<T> Observers
The observers are called when the value inside the field has been changed. This is the basic behavior.
class MyExtension : public VxSim::IExtension, public VxSim::IDynamics { public: VxData::Field<double> inputVelocity; private: bool mVelocityHasChanged; VxData::ObserverList mObservers; public: MyExtension(VxSim::VxExtension* proxy) : VxSim::IExtension(proxy, kCurrentVersion) , inputVelocity(0, "Speed", &proxy->getInputContainer()) , mVelocityHasChanged(false) , mObservers() {} virtual void onActive() override { mObservers << inputVelocity.addObserver([this](const VxData::FieldBase& field) { mVelocityHasChanged = true; }); } virtual void onInactive() override { mObservers.destory(); } };
With single valued fields, Field<T>, the observer can receive directly the modified field or value.
// observer receives the field inputVelocity.addObserver([this](const VxData::Field<double>& field) { mVelocityHasChanged = true; }); // observer receives the value inputVelocity.addObserver([this](double velocity) { mVelocityHasChanged = true; });
For pointer values (e.g., VxData::Field<VxDynamics::Constraint*>), the observer will be called when the object (here, a Constraint*) is changed. It will also be called if it has been deleted. In that case, the value inside the field will become nullptr.
VxData::Field<VxDynamics::Constaint*> inputConstraint; // observer receives the field inputConstraint.addObserver([this](VxDynamics::Constaint* constraint) { if ( constraint == nullptr ) { _eraseData(); } else { _initialise(constraint); } }); // observer receives the value inputVelocity.addObserver([this](double velocity) { mVelocityHasChanged = true; });
When restoring a keyframe, observers will be called automatically. This means that, in most cases, onStateRestore do not need to be implemented since observers will do the job automatically.
Multiple Value Field - VxData::Container and VxData::FieldArray<T> Observers
For multiple value fields, the observer will be called when a value is added, removed, or changed.
The FieldBase received by the observer will help to find out what triggered the observer. If a container is received, it means a field was added to or removed from this container.
If a Field<T> is received, it means that particular field has changed value.
Putting an observer on a container or FieldArray<T> is a simple way of being notified of all changes inside.
When a container has a sub-container, an observer on the top container will receive the notifications of all the changes inside, no matter how deep the field that changed.
class MaterialLayer : public VxData::Container { public: MaterialLayer(const Vx::VxID& name, VxData::Container* parent); VxData::Field<std::string> name; VxData::Field<VxGraphics::Texture*> texture; VxData::Field<VxGraphics::Texture*> maskTexture; VxData::Field<double> factor; VxData::Field<Vx::VxColor> color; }; class Material : public VxSim::IObject, public VxGraphics::IGraphic { public: [...] VxData::FieldArray<MaterialLayer> emissionLayers; [...] }; Material::Material(VxSim::VxObject* proxy) : VxSim::IObject(proxy) , VxGraphics::IGraphic(proxy) , emissionLayers("Emission", &proxy->getParameterContainer()) { emissionLayers.addObserver([this](const VxData::FieldBase& field) { if ( &field == &emissionLayers ) // an item was added or removed from the FieldArray<> { _initaliseLayer(&emissionLayers); } else if ( field->getType() == VxData::Types::Type_DataContainer ) { // an item was added or removed from one of the layer : problem! } else // some value was changed { _updateLayer(field.getParent()); } }); }
VxData::Vector<T> Observers
As with all other fields, the observers are called when the value changes. There are two types of notifications for VxData::Vector<T>. When the field received by the observer has getType() of VxData::Types::Type_Vector, the whole vector has changed, including a possible change in size. If the FieldBase type is VxData::Types::Type_VectorItem, only one value in the vector has changed.
class HeightField : public VxGraphics::IGraphic, public VxSim::IExtension { public: VxData::Vector<double> inputHeights; [...] HeightField(VxSim::VxExtension* proxy) : VxGraphics::IGraphic(proxy) , VxSim::IExtension(proxy) , inputHeights("HeightField", &proxy->getInputContainer()) , mRedrawParticles(false) , mUpdateAllParticles(false) { inputHeights.addObserver([this](const VxData::FieldBase& field) { if ( field.getType() == VxData::Types::Type_Vector ) { // the whole vector has changed _resetHeights(); } else if ( field.getType() == VxData::Types::Type_VectorItem ) { // one value was changed const VxData::VectorItemBase& item = dynamic_cast<const VxData::VectorItemBase&>(field); _updateCell(item->getIndex()); } }); } [...] };
Observer management - VxData::DelegateConnection
Observers are callbacks that are added to the FieldBase object. When an extension adds an observer to its own field, management is trivial since the object observed and the object receiving the notification are deleted at the same time. However, if an extension adds an observer to another extension, care must be taken because one of the objects may be deleted before the other. This management is done with the object returned by addObserver.
class HeightField : public VxSim::IDynamic, public VxSim::IExtension { private: VxData::DelegateConnection mObserverConnection; void _onPartMoved(); public: VxData::Field<VxDynamics::Part*> parameterPart; [...] virtual ~HeightField() { // make sure that when the object is deleted, the observer will never be called mObserverConnection.destroy(); } HeightField(VxSim::VxExtension* proxy) : VxSim::IDynamic(proxy) , VxSim::IExtension(proxy) , parameterPart("TargetPart", &proxy->getParameterContainer()) , mObserverConnection() { parameterPart.addObserver([this](const VxData::FieldBase& field) { // this is called when the parameter TargetPart is changed mObserverConnection.destroy(); // the part we are observing has changed if ( parameterPart.getValue() != nullptr ) { // we have a part to observe VxDynamics::Part* part = parameterPart.getValue(); mObserverConnection = part->outputWorldTransform.addObserver(std::bind(&HeightField::_onPartMoved, this)); } }); } };
Field Serialization
Serialization is supported for all extensions. By default, all inputs, outputs and parameters are saved to a file, and are restored when the file is loaded. When a file is loaded and the values are set in the extension, all observers will be called normally, so if your extension works correctly when a value is changed during simulation, no special code needs to be done to support serialization.
If your extension has private data that is not in the fields, you will need to override these methods:
- IExtension::onSaving must not change anything in the extensions; its purpose is to inject the private data needed to be able to load the extension correctly.
- IExtension::onLoading is the main versioning function. It is called with the data read directly from the file. The data can be set directly to the extension.
Backward Compatibility
If your extension fields are changing and you already have content files saved with your plugin, you will need to override one or several of these methods:
- IExtension::onLoading is called after the data has been read, but before it is set in the extension. The VxData::Container& passed in the first argument contains the data read form file. The passed container contains 3 containers, for input outputs and parameters. You can change the values on these containers, add some data, or extract some data and apply it to the fields in the extension.
- IExtension::onLoaded is called after all data has been set in the extension fields: all the inputs, outputs and parameters have been set to their correct value. This is where the additional data saved in onSaving can be restored. This is the final call to make sure internal data of the extension are ready.
IExtension::onRestoringConnection will be called when a connection cannot be restored because a field cannot be found. This method offer the chance to fix the connection by redirecting to the proper field. See Field connections below.
Correct handling of versioning is mandatory for backward compatibility. For an example on backward compatibility, see code tutorial ExBackwardCompatibility located in folder <your Vortex Installation folder>/tutorials/ExBackwardCompatibility
Advanced backward compatibility: VxSim::IObsolete
When versioning becomes too complex, or the entire structure of an extension system has changed, it might be necessary to make an extension obsolete and to replace it with a new one. IObsolete has a single virtual method that will be called after a file has been totally loaded.
An example will show how to better use IObsolete. Here, we have an extension that has several fields.
// original extension const int kCurrentVersion = 20200915; class MyExtension : public VxSim::IExtension { public: const VxSim::VxFactoryKey kFactoryKey; MyExtension(VxSim::VxExtension* proxy) : VxSim::IExtension(proxy, kCurrentVersion ) { } VxData::Field<double> inputPower; VxData::FieldArray<double> inputPowerCurve; };
inputPowerCurve has been moved to its own extension since it can be reused in a different context. Backward compatibility must be assured by loading the old extension and converting it to the new pair of extensions.
The obsolete extension needs be loadable with its data intact, just with the IObsolete interface added, keeping the same factory key.
// obsolete extension : it will be read, and then replaced const int kCurrentVersion = 20210203; class MyExtension : public VxSim::IExtension, public VxSim::IObsolete { public: const VxSim::VxFactoryKey kFactoryKey; MyExtension(VxSim::VxExtension* proxy) : VxSim::IExtension(proxy, kCurrentVersion) { } virtual Vx::VxSmartPtr<VxExtension> replaceObsoleteExtension(VxSim::VxExtension* extension) override; VxData::Field<double> inputPower; VxData::FieldArray<double> inputPowerCurve; };
The new extensions also need to be created, and they should be registered in the extension factory.
// new extensions class MyNewExtension : public VxSim::IObject { public: const VxSim::VxFactoryKey kFactoryKey; MyNewExtension (VxSim::VxObject* proxy) : VxSim::IObject(proxy) { } virtual bool isCompatible(VxExtension* extension) const override { VxSim::VxSmartInterface<MyNewSubExtension> child = extension; return child.valid(); } VxData::Field<double> inputPower; }; class MyNewSubExtension : public VxSim::IExtension { public: const VxSim::VxFactoryKey kFactoryKey; MyNewSubExtension (VxSim::VxExtension* proxy) : VxSim::IExtension(proxy) { } VxData::FieldArray<double> inputPowerCurve; };
The method replaceObsoleteExtension() will be called when the file has been totally loaded. That's where the replacement is created and initialised.
Vx::VxSmartPtr<VxExtension> MyExtension::replaceObsoleteExtension(VxSim::VxExtension* extension) { // create the replacements auto newExtension = VxSim::VxSmartInterface<MyNewExtension>::create(); auto childExtension = VxSim::VxSmartInterface<MyNewSubExtension>::create(); newExtension->add(childExtension); // transfer the data from the obsolete to the replacement newExtension->inputPower = this->inputPower; childExtension->inputPowerCurve= this->inputPowerCurve; // return the replacement return newExtension.getObject(); }
The obsolete extension will be removed, and replaced with the new ones. Any references to the obsolete extension will be replaced with references to the new extensions.
Keyframes
The keyframes feature is based on fields. Keyframes are supported for all extensions. By default, all inputs, outputs and parameters are saved in a keyframe, and are restored with a keyframe. When the keyframe is restored, all observers will be called normally, so if your extension works correctly when a value is changed during simulation, no special code needs to be done to support keyframes.
If you need to do something specific, especially if your extension has private data that is not in the fields, you need to override these methods:
- IExtension::onStateSave must not change anything in the extensions; its purpose is to inject the data needed to be able to correctly restore the state of the extension.
- IExtension::onStateRestore should assume that all the inputs, outputs and parameters have been set to their correct value, and the simulation will continue shortly. This is the correct call to make sure internal data of the extension are ready.
Keyframes have no versioning. Keyframes saved with a previous version of an extension may not work as expected when the extension is modified.
See Vortex Studio SDK Advanced - Key Frames for more information.
Field Connections
Connecting extensions together is the best way to have data transferred from one object to another. Connections are created easily in the Editor. A connection's purpose is to transfer data from an output to an input.
- A connection is from an output field to an input field.
- Connection will be established if the output type can be converted to the input type.
- Conversion may be with a loss of precision (e.g., double to int).
- Pointer fields cannot be converted.
- Enumeration can be connected to any int type and to string, and vice versa.
- std::string and VxFilename can be connected to each other.
- VxMath::Vector3 and VxMath::Matrix44 can be connected only if VxMath::Vector3 has its physical dimension set to kLength, kAngle, or kScale. The appropriate part of the VxMath::Matrix44 will be used.
- VxData::Container can be connected if they are similar (have the same fields). If one value changes in the output, the corresponding input is changed, however if a field is added to the output container, it will NOT be added to the input container.
- VxData::FieldArray<T> can be connected if they are of the same type. If the output is resized, the input will be resized as well.
- Individual fields inside VxData::Container or inside VxData::FieldArray<T> can be connected.
- Individual values inside VxData::Vector<T> cannot be connected.
Using the SDK, connections are contained in a VxContent::ConnectionContainerExtension.
A network simulator uses connections to know what data needs to be transferred between network nodes. Directly modifying an extension input will not work in a networked simulator; the modified value will stay local on the computer where it was changed. This may happen when an extension changes the inputs of another extension it accesses in a parameter Field.
Using VxSim::IMobile
When an extension implements IMobile, it adds to the extension the notion that it has a position, orientation and potentially a scale. IMobile adds three fields to the extension, they all represent a three-dimensional affine transformation matrix.
VxData::Field<VxMath::Matrix44> inputLocalTransform; // inputParentTransform is the frame of reference for the extension. VxData::Field<VxMath::Matrix44> inputParentTransform; // inputLocalTransform is the offset from the frame of reference for the extension. VxData::Field<VxMath::Matrix44> outputWorldTransform; // outputWorldTransform is the extension's absolute transformation matrix
Always use IMobile::outputWorldTransform if you are interested in the extension's real position, orientation and scaling.
The default calculation of outputWorldTransform is:
outputWorldTransform = inputParentTransform * inputLocalTransform;
This behavior is usually correct, but it can be overridden in your extension with virtual void IMobile::_computeWorldTransform(). This calculation is triggered automatically whenever any of the two inputs of IMobile change.
VxSim::IMobile and VxSim::IObject
When IMobile is combined with IObject in an extension, the extensions outputWorldTransform will be transfered to the inputParentTransform of its IMobile children. The world transform will become the parent transform of its children, and thus their reference frame. Therefore, a hierarchy of IMobile becomes similar to a scene graph, where moving one IMobile will move all its children recursively.
The virtual method _onIMobileInputChanged() can be overriden to change that behavior.
Animating VxSim::IMobile
IMobile must be animated by connecting the source of the animation to inputParentTransform. The animated input becomes the frame of reference from which the world transform is calculated.
Connecting inputLocalTransform is never guaranteed to work, as the frame of reference for the extension may be changed independently.
An animated VxDynamics::Part needs to be animated by modifying its inputParentTransform.
Network
Network is supported for all extensions. Only connected outputs are transferred by network. A large data packet is sent from master to all slaves, containing data for all connections. Smaller data packets are sent from a slave to all other nodes in the network when there is local value change on a connected output in a slave.
Large VxData::Vector<T> may take several simulation updates to be fully transferred. During simulation, changed values are sent immediately, but the entire vector is refreshed in a round-robin algorithm.
ILocalExtension
ILocalExtension is used to stop an extension from broadcasting values in the network. Usually, an extension is active in only one node in the network; there is only one dynamics module, the device is on one computer, the motion platform is in one place. However, some extension are updated simultaneously on several nodes in the network: there are usually several graphic slaves, HumanModule is needed for both graphics and dynamics, etc. Whenever an extension is updated on several nodes, the extension's outputs on each node will be updated. When they are connected, every node would send their value to all other nodes, flooding the network with data packets. ILocalExtension is designed to avoid this, by blocking outgoing network traffic from this extension.
All IGraphics are also ILocalExtension. It is safe to connect a IGraphic to another IGraphic: network won't interfere with the local calculations.
Record/Playback
Record/Playback is supported for all extensions. The values recorded for playback are the same as the values that were sent from the master to the slave. Recording does not need any special consideration when developing an extension. Playback must be handled correctly by modules; they need to handle VxSim::kModePlayingback in preUpdate(), update() and postUpdate(). Normally, a module will do nothing during playback; all inputs and outputs of extensions that were modified during recording will be played back automatically. However, an output device (like graphics, sound, possibly motion platform) should manage kModePlayingback just like kModeSimulating. The goal of playback is to show the same output as when it was simulating.