tyParticleObjectExt Interface

The tyParticleObjectExt interface can be used to query tyFlow particle data.

/*Copyright (c) 2025, 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:

05/14/2024:

* added GetParticleUVWByIndex and GetParticleTFMeshByIndex functions

03/01/2024:

* users should now call ReleaseInstances() on pointers returned by CollectInstances, rather
than deleting them directly

09/07/2021:

* interface now encapsulated in "tyFlow" namespace
* multiple tyParticleObjectExtXXX classes now consolidated into single class
* STL dependency removed. std::vector<T> changed to tyVector<T> (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<Matrix3>, 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 = 1U << 0, //tyInstanceInfo::data* is Mesh*
		inode = 1U << 1, //tyInstanceInfo::data* is INode*
		
		pluginMustDelete = 1U << 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<T> 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<typename T>
	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<T>& 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<T>& 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) : ((size_t)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 released by the querying plugin after use, by calling 
		ReleaseInstances. 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').
		See the ReleaseInstances documentation, below, for more info.
		*/
		virtual tyVector<tyInstanceInfo>* 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<TSTR> GetFloatChannelNamesVec() = 0;
		virtual tyVector<TSTR> GetVectorChannelNamesVec() = 0;
		virtual tyVector<TSTR> 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<tyParticleUVWInfo> 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<Point3> 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;
	};
	
	class tyParticleObjectExt_3 : public tyParticleObjectExt_2
	{
	public:
		/*
		This function cleans up instances created by CollectInstances. Users 
		should call this function, passing the vector returned by
		CollectInstances, instead of deleting the vector returned by 
		CollectInstances themselves. Note: this function was implemented
		in tyFlow v1.106 and should not be called for prior versions of 
		tyFlow. The current version of tyFlow can be checked using the
		MAXScript function: tyFlowVersion(). If a user detects that a prior
		version of tyFlow is installed, they can delete the pointer returned
		by CollectInstances to clean it up, instead of calling ReleaseInstances. 
		*/
		virtual void ReleaseInstances(tyVector<tyInstanceInfo>* instances) = 0;
	};

	class tyParticleObjectExt_4 : public tyParticleObjectExt_3
	{
	public:
		/*
		This functions returns the shape offset of particles.
		Shape offset values are used internally by tyFlow to
		compute pivot point modifications. This function was added
		for internal use and can likely be ignored by 3rd party
		developers.
		*/
		virtual Point3 GetParticleShapeOffsetByIndex(int index) = 0;
	};

	class tyParticleObjectExt_5 : public tyParticleObjectExt_4
	{
	public:

		/*
		This function returns a per-particle UVW override for a specified
		map	channel.
		*/
		virtual UVVert GetParticleUVWByIndex(int index, int channel) = 0;

		/*This is an internal function which returns the tfMesh* of
		a particle. It should not be called by 3rd-party developers*/
		virtual void* GetParticleTFMeshByIndex(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<Matrix3> 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<tyParticleUVWInfo> 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 <unordered_map>
		std::unordered_map<TimeValue, NodeDataStructure*> 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<tyInstance> instances;
		
		tyInstanceInfo(){flags = DataFlags::none;}
	};

	typedef tyParticleObjectExt_5 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);		
	}

};