/*Copyright (c) 2024, Tyson Ibele Productions Inc. All Rights Reserved. Permission is hereby granted, free of charge, to any person obtaining a copy of this file ("tyParticleObjectExt.h") and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ #pragma once //headers from Max SDK #include "IParticleObjectExt.h" #include "object.h" #include "mesh.h" #define TYPARTICLE_INTERFACE_V2 Interface_ID(0x1213b15, 0x1e23511) #define TYPARTICLE_INTERFACE_FORCED_V2 Interface_ID(0x1213b15, 0x1e23514) #define PARTICLEOBJECTEXT_INTERFACE_FORCED_V2 Interface_ID(0x1213b15, 0x1e23512) //undef macros from legacy interface! #undef GetTyParticleInterface #undef GetTyParticleInterfaceForced #undef GetParticleInterfaceForced /* tyParticleObjectExt (v2) CHANGELOG: 09/07/2021: * interface now encapsulated in "tyFlow" namespace * multiple tyParticleObjectExtXXX classes now consolidated into single class * STL dependency removed. std::vector changed to tyVector (defined below), and some virtual functions have "Vec" suffix to avoid naming collisions with legacy tyParticleInterface * CollectInstances/CollectInstanceNodes consolidated into single CollectInstances function, where differentiation between Mesh/INode pointers is now done in tyInstanceInfo class in coordination with the bits set in tyInstanceInfo::flags * "dataFlags" argument of CollectInstances now used to specify the type of data to collect. * "plugin" argument of UpdateTyParticles/CollectInstances now TSTR instead of enum * tm0/tm1 of tyInstance struct replaced with tyVector, as a future-proof way of potentially supporting multi-segment motion blur (more than two transforms for a single motion blur interval query) * TimeValue property (t) added to tyInstance struct, for more specific instanced node evaluations * further information about these changes are listed in the relevant comments below * interface query macros replaced with proper functions */ class Mtl; /* The tyParticleInterface interface allows you to access a tyFlow's custom data channels, similar to how position/rotation/scale/etc values are accessed through the regular IParticleObjectExt interface. USAGE: tyFlow::tyParticleInterface* tyObj = NULL; //...acquire interface from baseObject here... if (tyObj) { //UpdateTyParticles wraps UpdateParticles. Do not also call //UpdateParticles because it will clear out some data cached //by UpdateTyParticles. tyObj->UpdateTyParticles(node, t); //To ensure maximum data access speed, we convert our channel //strings into channel indices outside of the particle loop //Channel strings are arbitrary and defined by the user inside //the tyFlow's various operators. Safety checks are in place to //ensure attempts to access a missing channel will not cause //any errors. A default value for missing channels will simply //be returned instead (0.0f, Point3::Origin, Matrix3(1)) //Note: channel names are case-sensitive int floatChannel1 = tyObj->FloatChannelToInt(_T("myFloatChannel")); int VectorChannel1 = tyObj->VectorChannelToInt(_T("myVectorChannel1")); int VectorChannel2 = tyObj->VectorChannelToInt(_T("myVectorChannel2")); int TMChannel1 = tyObj->TMChannelToInt(_T("myTMChannel")); int numParticles = tyObj->NumParticles(); for (int q = 0; q < numParticles; q++) { float f1 = tyObj->GetCustomFloat(q, floatChannel1); Point3 v1 = tyObj->GetCustomVector(q, vectorChannel1); Point3 v2 = tyObj->GetCustomVector(q, vectorChannel2); Matrix3 tm1 = tyObj->GetCustomVector(q, TMChannel1); //...etc } } */ namespace tyFlow { struct tyParticleUVWInfo { int channel; UVVert value; }; enum DataFlags : unsigned int { none = 0, mesh = 1 << 0, //tyInstanceInfo::data* is Mesh* inode = 1 << 1, //tyInstanceInfo::data* is INode* pluginMustDelete = 1 << 31, //set in tyInstanceInfo::flags if //plugin must delete data pointer after use }; /* The legacy tyParticleInterface relied on STL vectors to pass data back and forth. However, STL vectors compiled with different versions of MSVS may not have the same memory alignments, which will ultimately cause crashes during data op between tyFlow and other plugins that are not compiled with the same verison of MSVS. Also, Max's built-in Tab dynamic array class has its own limitations which can lead to program instability (its assignment operator doesn't properly copy values, so if you append a value that's later destroyed out-of-scope, the Tab will be corrupted). As an alternative to both, the tyVector class defined below is a lightweight, dynamic array class which should allow for easy conversion to the new tyParticleInterface, from legacy interface code. */ template class tyVector { private: T* _array; size_t _size; size_t _alloc; public: //constructor/destructor/copy tyVector() : _array(NULL), _size(0), _alloc(0) {} tyVector(const tyVector& v) { if (this == &v) {return;} _array = NULL; *this = v; } ~tyVector() { if (_array) { delete[] _array; } } //iterator T* begin() const { return _array; } T* end() const { return _array + _size; } //assignment tyVector& operator=(const tyVector& v) { if (this == &v) {return *this;} clear(); resize(v.size()); for (int h = 0; h < v.size(); h++) { _array[h] = v[h]; } return *this; } //index access T& operator[](const size_t i) { return _array[i]; } const T& operator[](const size_t i) const { return _array[i]; } T& front() { return _array[0]; } T& back() { return _array[_size-1]; } const T& front() const { return _array[0]; } const T& back() const { return _array[_size-1]; } //examination functions size_t size() const { return _size; } size_t capacity() const { return _alloc; } //modification functions void clear() { if (_array){delete[] _array;} _array = NULL; _size = 0, _alloc = 0; } void push_back(T& v) { resize(_size + 1); _array[_size - 1] = v; } void reserve(size_t s) { if (s > _alloc) { _alloc = s; auto tmp = new T[_alloc]; if (_array) { for (int h = 0; h < _size; h++) { tmp[h] = _array[h]; } delete[] _array; } _array = tmp; } } void resize(size_t s) { if (_size != s) { if (s == 0) {clear();} else if (s < _alloc){_size = s;} else { reserve(s < 4 ? s : ceil(s * 1.5)); _size = s; } } } }; struct tyInstanceInfo; struct tyInstance; class tyParticleObjectExt : public IParticleObjectExt { public: /* This function is similar to UpdateParticles, found in the original IParticleObjectExt interface. The plugin argument of this function takes the name of the plugin querying this interface, in lowercase letters. Ex: _T("arnold"), _T("octane"), _T("redshift"), _T("vray"), etc. This is a somewhat arbitrary value, but by having plugins identify themselves during a query, tyFlow can internally determine if any plugin-specific edge-cases need to be processed. */ virtual void UpdateTyParticles(INode* node, TimeValue t, TSTR plugin) = 0; /* This helper function collects instances (particles that share the same data pointer) and groups them together, along with any per-particle property overrides. It is a quick way to collect all particle instances for rendering. The arguments 'moblurStart' and 'moblurEnd' should be the start and end of the desired motion blur interval, for proper particle transform retrieval. Note: this function calls UpdateTyParticles internally for all time values, so UpdateTyParticles does not need to be manually called before calls to CollectInstances. The dataFlags argument of this function takes flags related to what type of instancing data the function should collect. For only Mesh* instancing, pass DataFlags::mesh. For only INode* instancing, pass DataFlags::inode. For both, pass (DataFlags::mesh | DataFlags::inode). See comments below within the tyInstance struct for more information about instance data pointers. The plugin argument of this function takes the name of the plugin querying this interface, in lowercase letters. Ex: _T("arnold"), _T("octane"), _T("redshift"), _T("vray"), etc. This is a somewhat arbitrary value, but by having plugins identify themselves during a query, tyFlow can internally determine if any plugin-specific edge-cases need to be processed. The return value is a pointer to a tyVector of tyInstanceInfo that MUST be deleted by the querying plugin after use. Be sure that any internal objects which have been flagged for deletion have been cleaned up prior to deleting this pointer (any tyInstanceInfo data flagged with 'pluginMustDelete'). */ virtual tyVector* CollectInstances( INode* node, DataFlags dataFlags, TimeValue moblurStart, TimeValue moblurEnd, TSTR plugin) = 0; /* These functions return a list of active channel names for each data type */ virtual tyVector GetFloatChannelNamesVec() = 0; virtual tyVector GetVectorChannelNamesVec() = 0; virtual tyVector GetTMChannelNamesVec() = 0; /* These functions convert channel strings into channel integers */ virtual int FloatChannelToInt(TSTR channel) = 0; virtual int VectorChannelToInt(TSTR channel) = 0; virtual int TMChannelToInt(TSTR channel) = 0; /* These functions return custom data values for particle indices using channel integers */ virtual float GetCustomFloat(int index, int channelInt) = 0; virtual Point3 GetCustomVector(int index, int channelInt) = 0; virtual Matrix3 GetCustomTM(int index, int channelInt) = 0; /* This function returns per-particle export group flags A return value of 0 means no flags have been set. */ virtual unsigned int GetParticleExportGroupsByIndex(int index) = 0; /* This function returns per-particle instance ID. This is a user-defined ID that can be arbitrary and independent from each particle's birth ID. */ virtual int GetParticleInstanceIDByIndex(int index) = 0; /* This function returns per-particle instanceNode. This is a user-defined render-only node which corresponds to each particle. NULL means no node has been assigned. */ virtual INode* GetParticleInstanceNodeByIndex(int index) = 0; /* This function returns per-particle mass values. */ virtual float GetParticleMassByIndex(int index) = 0; /* This function returns per-particle mesh matID overrides. A return value of -1 means no override is set on the particle. */ virtual int GetParticleMatIDByIndex(int index) = 0; /* This function returns per-particle material (Mtl*) overrides. A return value of NULL means no override is set on the particle and thus the default node material should be used. */ virtual Mtl* GetParticleMtlByIndex(int index) = 0; /* This function returns per-particle simulation group flags A return value of 0 means no flags have been set. */ virtual unsigned int GetParticleSimGroupsByIndex(int index) = 0; /* This function returns per-particle spin values in per-frame units */ virtual Point3 GetParticleSpinPoint3ByIndex(int index) = 0; /* This function returns per-particle UVW overrides for specific map channels. The return value is an array which contains a list of overrides and the map channel whose vertices they should be assigned to. An empty array means no UVW overrides have been assigned to the particle. */ virtual tyVector GetParticleUVWsVecByIndex(int index) = 0; /* This function returns the map channel where per-vertex velocity data (stored in units/frame) might be found, inside any meshes returned by the tyParticleInterface. Note: not all meshes are guaranteed to contain velocity data. It is your duty to check that this map channel is initialized on a given mesh and that its face count is equal to the mesh's face count. If both face counts are equal, you can retrieve vertex velocities by iterating each mesh face's vertices, and applying the corresponding map face vertex value to the vertex velocity array you are constructing. Vertex velocities must be indirectly retrieved by iterating through the faces like this, because even if the map vertex count is identical to the mesh vertex count, the map/mesh vertex indices may not correspond to each other. Here is an example of how vertex velocities could be retrieved from the velocity map channel, through a tyParticleInterface: //// std::vector vertexVelocities(mesh.numVerts, Point3(0,0,0)); int velMapChan = theTyParticleInterface->GetMeshVelocityMapChannel(); if (velMapChan >= 0 && mesh.mapSupport(velMapChan)) { MeshMap &map = mesh.maps[velMapChan]; if (map.fnum == mesh.numFaces) { for (int f = 0; f < mesh.numFaces; f++) { Face &meshFace = mesh.faces[f]; TVFace &mapFace = map.tf[f]; for (int v = 0; v < 3; v++) { int meshVInx = meshFace.v[v]; int mapVInx = mapFace.t[v]; Point3 vel = map.tv[mapVInx]; vertexVelocities[meshVInx] = vel; } } } } */ virtual int GetMeshVelocityMapChannel() = 0; }; class tyParticleObjectExt_2 : public tyParticleObjectExt { public: /* This function returns 64-bit particle IDs, in case an interface has particle IDs whose value exceeds INT_MAX. Currently only necessary for tyCache objects loading multi-partition PRT files. */ virtual __int64 GetParticleBornIndex64(int index) = 0; /* This function returns additional bitflags assigned to particles. */ virtual unsigned int GetParticleFlagsByIndex(int index) = 0; }; struct tyInstance { /*ID contains the unique Birth ID of source particles. This value is guaranteed to be unique for each particle in the flow. This value can be negative or zero*/ __int64 ID; /*instanceID contains the arbitrary, user-defined instance ID of source particles. Texmaps can make use of this value at rendertime. This value can be negative or zero*/ __int64 instanceID; /*tms contains the instance's transform(s) spread evenly over the motion blur interval (the interval specified by the arguments passed to CollectInstances), in temporal order. A tms tyVector with a single element represents a static instance. A tms tyVector with two elements contains the transforms at the start and end of the interval. A tms tyVector with three elements contains the transforms at the start, center, and end of the interval, etc. A tms tyVector with more than two elements allows a renderer to compute more accurate multi-sample motion blur. Instance velocity/spin, should those properties be required, should be derived from these values (typically from the first/last entry)*/ tyVector tms; /*mappingOverrides contains mapping override data for channels specified in the tyParticleUVWInfo struct. Each value should override all mapping vertex values of the instance mesh for the specified mapping channel*/ tyVector mappingOverrides; /*materialOverride contains the material override for the instance. A value of NULL means no override should be applied*/ Mtl* materialOverride; /*matIDOverride contains the material ID override for the instance. A value of -1 means no override should be applied*/ int matIDOverride; /*vel is the per-frame particle velocity of the instance. Note: this value is stored for completeness, but should not be used by developers to calculate motion blur. Motion blur should be calculated using the tms tyVector instead. */ Point3 vel; /*spin is the per-frame particle spin of the instance. Note: this value is stored for completeness, but should not be used by developers to calculate motion blur. Motion blur should be calculated using tm0 and tm1 instead. */ Point3 spin; /*t is the time value stored in an instance which is intended to represent the time at which a node-based instance should have its corresponding scene node evaluated for processing. This allows users to instance scene nodes and also assign per-instance time offsets to their animation. The way in which nodes are evaluated and processed for rendering is up to the developer, although repeated calls to EvalWorldState for many instances would be slow (so a cached, time-mapped, internal data structure would be a better solution). If scene nodes are converted into an internal data structure for rendering, these structures should be created and grouped together based on shared t values. For example, the pseudocode for processing t values might look something like this: #include std::unordered_map nodeDataStructures; for (auto &instance : instanceInfo.instances) { if (!nodeDataStructures[instance.t]) { auto &os = EvalWorldState((INode*)instanceInfo.data, instance.t); nodeDataStructures[instance.t] = new NodeDataStructure(os); } } In this example, NodeDataStructure would be a class which copies and collects relevant INode info (ex: Mesh, Light, etc) for rendering. The data contained within it would exist indepdently from the corresponding scene node's current-frame data, acting as a cache of the scene node's data for any other given frame, depending on which t values are stored in each instance. A renderer would then query the map for the relevant node data when rendering an instance with a particular t value. Overall, it is up to the developer to decide how to interpret these t values, or whether to utilize them at all. */ TimeValue t; }; struct tyInstanceInfo { /*these flags are used to define the type of data stored in the void pointer, and any other relevant information, like whether the plugin must delete the pointer once it's finished using it. */ DataFlags flags; /*the data pointer contains the relevant class that should be instanced. Currently, it is either a Mesh* or an INode*, and the flags variable can be queried to find out which class type it is. The flags variable will only have one class type flag set, but may have other relevant information flagged as well, so you should not test for the class type with the equality ('==') operator, but instead bitwise AND operator ('&'). For example: if (flags == DataFlags::mesh){auto mesh = (Mesh*)data;} //incorrect if (flags == DataFlags::inode){auto node = (INode*)data;} //incorrect if (flags & DataFlags::mesh){auto mesh = (Mesh*)data;} //correct if (flags & DataFlags::inode){auto node = (INode*)data;} //correct If the "pluginMustDelete" flag is set, you must delete this pointer after use. Be sure to cast to relevant class before deletion so the proper destructor is called. NOTE: In the past, tyFlow only generated Mesh* instances, but that precluded tyFlow from being able to instance things like lights, atmospherics, etc. By including INode* as an instanceable data type, users can potentially instance any creatable object. So if you're a renderer dev implementing this interface, please consider also supporting INodes returned by this interface, so that users can instance any object with your renderer. INodes can be passed through this interface by tyFlow using the Instance Node operator. See the CollectInstances function comments for more information about how to tell the interface which data type (Mesh* or INode* or both) you wish to collect. */ void *data; /*meshVelocityMapChannel defines which map channel of Mesh* data contains per-vertex velocity data (stored in units/frame). A value of -1 means the mesh contains no per-vertex velocity data.*/ int meshVelocityMapChannel; /*instances is an array of tyInstances that all share the same data pointer (defined above). It also contains any overrides which should be applied on a per-instance basis.*/ tyVector instances; tyInstanceInfo(){flags = DataFlags::none;} }; typedef tyParticleObjectExt_2 tyParticleInterface; inline tyParticleInterface* GetTyParticleInterface(BaseObject* obj) { return (tyParticleInterface*)obj->GetInterface(TYPARTICLE_INTERFACE_V2); } /* Helper interface to force the retrieval of a regular tyParticleInterface interface, on tyFlow/tyCache objects, even if their "particle interface" option is disabled. */ inline tyParticleInterface* GetTyParticleInterfaceForced(BaseObject* obj) { return (tyParticleInterface*)obj->GetInterface(TYPARTICLE_INTERFACE_FORCED_V2); } /* Helper interface to force the retrieval of a regular IParticleObjectExt interface, on tyFlow/tyCache objects, even if their "particle interface" option is disabled. */ inline IParticleObjectExt* GetParticleInterfaceForced(BaseObject* obj) { return (IParticleObjectExt*)obj->GetInterface(PARTICLEOBJECTEXT_INTERFACE_FORCED_V2); } };