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 "mesh.h"
#include "object.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);
    }

};