tyVolumeObjectExt Interface

The tyVolumeObjectExt interface can be used to query tyFlow volume data.

Developers testing their implementation of the interface can pass “debug” as the plugin name when calling “UpdateVolumes” on a tyFlow object. This will generate density/color values in the shape of a sphere, across a set of 64 individual non-AABB volumes. To preview the structure/shape of these volumes, enable “Display volume interface preview” in the “Interfaces” rollout of the tyFlow object. This will generate a point/line display of the data in the viewport, so developers can see whether their interpretation of the same data is valid. For proper release builds, ensure you do not pass “debug” as the plugin name, as doing so will prevent normal volume data from passing through the interface.

/*Copyright (c) 2026, Tyson Ibele Productions Inc.
All Rights Reserved.

Permission is hereby granted, free of charge, to any person obtaining a copy of
this file ("tyVolumeObjectExt.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 "object.h"

#if MAX_RELEASE < 25000
#include "box3.h"
#include "matrix3.h"
#include "point2.h"
#include "point3.h"
#include "point4.h"
#else
#include "Geom/box3.h"
#include "Geom/matrix3.h"
#include "Geom/point2.h"
#include "Geom/point3.h"
#include "Geom/point4.h"	
#endif

#define TYVOLUME_INTERFACE_V1 Interface_ID (0x1213b15, 0x1e23513)
#define TYVOLUME_INTERFACE_V2 Interface_ID (0x1213b16, 0x1e23514)
#define TYVOLUME_INTERFACE_V3 Interface_ID (0x1213b17, 0x1e23515)
#define TYVOLUME_INTERFACE_V4 Interface_ID (0x1213b18, 0x1e23516)
#define TYVOLUME_INTERFACE_V5 Interface_ID (0x1213b19, 0x1e23517)
#define TYVOLUME_INTERFACE_V6 Interface_ID (0x1213b20, 0x1e23518)

/*

tyVolumeObjectExt CHANGELOG:

08/28/2026:

* added tyVolume_v4 and TYVOLUME_INTERFACE_V6 which expose raw
  voxel arrays and LUTs. TYVOLUME_INTERFACE_V6 is implemented
  in tyFlow v2.011+. Fixed "celsius" misspelling in temperature
  units enum.

04/17/2026:

* added TYVOLUME_INTERFACE_V5 with new function to get interface
  license activation state. TYVOLUME_INTERFACE_V5 is implemented
  in tyFlow v2.007+.

04/15/2026:

* added tyVolume_v3 and TYVOLUME_INTERFACE_V4 which expose voxel 
  size accessor function. TYVOLUME_INTERFACE_V4 is implemented 
  in tyFlow v2.006+.

03/25/2026:

* added tyVolume_v2 and TYVOLUME_INTERFACE_V3 with new functions to 
  return fuel color and raymarching step size

03/10/2026:

* added TYVOLUME_INTERFACE_V2 with new function to check if 
  volumes changed by user since last call to UpdateVolumes

09/26/2025:

* fixed incorrect Interface_ID
* fixed includes for Max 2023+

05/20/2024:

* initial creation of basic volume interface

*/

namespace tyFlow
{
	/*
	A wrapper class containing all relevant properties and
	accessor functions for a given volume.
	*/
	class tyVolume_v1
	{
	public:
		enum ScalarType : int
		{
			density,
			fuel,
			temperature,
		};

		enum VectorType : int
		{
			color,
			velocity,
		};

		/*
		The world-space transform of the volume, relative
		to its zero-th voxel coordinate.
		*/
		Matrix3 transform;

		/*
		The number of voxels the volume contains
		along each X/Y/Z axis. The total number of
		voxels for a given volume is:
		dimensions.x * dimensions.y * dimensions.z
		*/
		IPoint3 dimensions;

		/*
		Accessor functions and enums which allow you to query
		per-voxel volume values, given a volume-local XYZ coordinate
		and a voxel data type.
		Fractional coordinates will be trilinear interpolated.
		*/
		virtual float GetScalar(const Point3 xyz, const ScalarType type) = 0;
		virtual Point3 GetVector(const Point3 xyz, const VectorType type) = 0;

		/*
		Accessor functions which allow you to query fire display
		values, that match tyFlow's own viewport display of
		volume fire. By passing a temperature value (obtained
		through the GetScalar function), fire opacity/color
		values will be returned, which correspond to the fire
		opacity/color gradients assigned in tyFlow's volume
		display UI.
		Color values are not clamped and may have xyz components
		greater than 1, depending on the fire color intensity
		value assigned in tyFlow's volume display UI.
		*/
		virtual float GetFireOpacity(const float temperature) = 0;
		virtual Point3 GetFireColor(const float temperature) = 0;

		/*
		Accessor functions which allow you to query per-volume
		opacity multipliers, assigned in tyFlow's volume display UI.
		These multipliers should be applied uniformly to all
		voxels in the volume.
		*/
		virtual float GetDensityOpacityMultiplier() = 0;
		virtual float GetFuelOpacityMultiplier() = 0;
		virtual float GetOverallOpacityMultiplier() = 0;

		/*
		Helper function to return this volume's
		logical coordinates, relative to the local parent
		interface origin (the inverse transform of the first
		volume returned by the parent interface).

		The validity of logical coordinates is dependent
		on the following three volume interface axioms:

		(1) All volumes returned by a particular
		volume interface have the same scale and voxel dimensions.

		(2) No two volumes returned by a particular volume
		interface will share the same world-space transform.

		(3) All volume local-interface-coordinates
		are evenly-divisible by their dimensions.

		These axioms entail that all volumes returned
		by a particular interface will be the same size, and be
		non-overlapping.

		Given these axioms, it's easy to calculate logical
		coordinates for a volume, using the following
		formula:

		Point3 logicalCoordinate =
		(
				volume->transform *
				Inverse(interface->GetVolume(0)->transform)
		).GetTrans() / volume->dimensions;

		So, the logical coordinates of the first volume
		returned by a volume interface would be [0,0,0].
		A volume immediately adjacent to the right of
		the first volume would have logical coordinates
		of [1,0,0]. A volume immediately adjacent to the
		top of that volume would have logical coordinates
		of [1,0,1], and so on.

		Because the logical coordinates of volumes can be
		hashed in fewer bits than regular world-space
		coordinates, they are optimal candidates for spatial
		hashing structures which may be used to accelerate
		world-space-to-volume lookups. You can also use
		logical coordinates to quickly query an interface for
		the neighbor volumes of a particular volume, when doing
		things like trilinear interpolation. Ex:

		//get neighbor to the right of this volume, if it exists
		tyVolume* neighborVolume =
		interface->GetVolume(volume->GetLogicalCoordinates() +
		IPoint3(1,0,0));
		*/
		virtual IPoint3 GetLogicalCoordinates() = 0;
	};

	class tyVolume_v2 : public tyVolume_v1
	{
	public:

		/*
		Accessor function which allows you to query fuel display
		color that matches tyFlow's own viewport display of
		volume fuel. Fuel color is the same for all voxels containing
		fuel values.
		*/
		virtual Point3 GetFuelColor() = 0;
	};

	class tyVolume_v3 : public tyVolume_v2
	{
	public:

		/*
		Accessor function which returns the size of volume 
		voxels in world-space coordinates. 
		*/
		virtual float GetVoxelSize() = 0;
	};

	class tyVolume_v4 : public tyVolume_v3
	{
	public:

		/*
		Accessor function which returns a raw voxel array
		of the given ScalarType for the volume. 
		Returns NULL if the ScalarType isn't available.	

		Individual voxels can be accessed using the following
		formula for a given XYZ coordinate, where X/Y/Z are 
		all greater than or equal to zero and less than the
		corresponding dimension:

		int arrayIndex = 
		x + (y * dimensions.x) + (z * dimensions.x * dimensions.y);
		*/
		virtual const float* GetScalarArray(const ScalarType type) = 0;

		/*
		Accessor function which returns a raw voxel array
		of the given VectorType for the volume.
		Returns NULL if the VectorType isn't available.

		Individual voxels can be accessed using the following
		formula for a given XYZ coordinate, where X/Y/Z are 
		all greater than or equal to zero and less than the
		corresponding dimension:

		int arrayIndex =
		x + (y * dimensions.x) + (z * dimensions.x * dimensions.y);
		*/
		virtual const Point3* GetVectorArray(const VectorType type) = 0;

		/*
		Accessor function which returns the min/max temperature
		range for the fire color/opacity LUTs.
		*/
		virtual Point2 GetFireLUTMinMax() = 0;

		/*
		Accessor function which returns the fire color LUT, which can 
		be used to compute fire color values from a given voxel's 
		temperature value. Returns a valid pointer if the fire color
		LUT is available, and sets size to the size of the LUT. Returns
		NULL if the fire color LUT is not available.

		A fire color value can be computed using the following snippet:

		float temperature = .... //voxel temperature
		Point3 fireColor;
		unsigned int size;
		const Point3* fireColorLUT = GetFireColorLUT(size);
		if (fireColorLUT)
		{
			Point2 LUTMinMax = GetFireLUTMinMax();
			float temperatureRange = 
				std::max(1e-5f, LUTMinMax.y - LUTMinMax.x);
			float temperatureInterp = std::clamp(
				(temperature - LUTMinMax.x) 
				/ temperatureRange, 0.0f, 1.0f);
			unsigned int temperatureLUTIndex = temperatureInterp * (size - 1);

			fireColor = fireColorLUT[temperatureLUTIndex];
		}	

		Values computed with this LUT for a given temperature should 
		match those returned by the GetFireColor(...) function. 
		*/
		virtual const Point3* GetFireColorLUT(unsigned int& size) = 0;


		/*
		Accessor function which returns the fire opacity LUT, which can
		be used to compute fire opacity values from a given voxel's
		temperature value. Returns a valid pointer if the fire opacity
		LUT is available, and sets size to the size of the LUT. Returns
		NULL if the fire opacity LUT is not available.

		A fire opacity value can be computed using the following snippet:

		float temperature = .... //voxel temperature
		float fireOpacity = 0;
		unsigned int size;
		const float* fireOpacityLUT = GetFireOpacityLUT(size);
		if (fireOpacityLUT)
		{
			Point2 LUTMinMax = GetFireLUTMinMax();
			float temperatureRange = 
				std::max(1e-5f, LUTMinMax.y - LUTMinMax.x);
			float temperatureInterp = std::clamp(
				(temperature - LUTMinMax.x) 
				/ temperatureRange, 0.0f, 1.0f);
			unsigned int temperatureLUTIndex = temperatureInterp * (size - 1);

			fireOpacity = fireOpacityLUT[temperatureLUTIndex];
		}

		Values computed with this LUT for a given temperature should 
		match those returned by the GetFireOpacity(...) function.
		*/
		virtual const float* GetFireOpacityLUT(unsigned int& size) = 0;
	};

	/*
	The tyVolumeObjectExt interface class can be used to access
	a tyFlow object's volume data.
	*/
	class tyVolumeObjectExt_v1
	{
	public:
		/*
		This function prepares tyFlow volumes for API access and
		should be called on a per-frame basis prior to calling any
		other volume API functions.

		The plugin argument of this function takes the name of the
		plugin querying this interface, in lowercase letters.
		Ex: _T("arnold"), _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.

		If you pass "debug" as the plugin name, a set of debug
		volumes will be generated, whose combined densities form
		a sphere at the world origin, with interpolated RGB coloring.
		This mode can be useful for sanity-checking code that
		references this interface. A point-based preview of this
		data can be displayed in the viewport by enabling "Display
		volume debug data" in the "Debugging" rollout of any
		tyFlow object.
		*/
		virtual void UpdateVolumes(const TimeValue t, const TSTR plugin) = 0;

		/*
		Developers should call this once they are done processing
		tyFlow volume data, to clear any internal allocations that
		may have been made during the prior call to UpdateVolumes.
		*/
		virtual void ReleaseVolumes() = 0;

		/*
		Returns the total number of volumes available. Depending on
		the complexity of sparse simulation data exposed by this
		interface, the number of volumes returned may be in the
		tens of thousands. All tyVolume accessor functions are
		thread-safe, so multi-threaded processing of these volumes
		is recommended.
		*/
		virtual int NumVolumes() = 0;

		/*
		Returns a tyVolume by index (range: 0 - [NumVolumes()-1]).
		The interface retains ownership of these pointers and
		cleans them up in the call to ReleaseVolumes().
		*/
		virtual tyVolume_v1* GetVolume(const int index) = 0;

		/*
		Returns the tyVolume (if it exists, otherwise NULL) located
		at the specified logical coordinate point, as well as
		its interface index.
		The interface retains ownership of these pointers and
		cleans them up in the call to ReleaseVolumes().
		*/
		virtual tyVolume_v1* GetVolume(
			const IPoint3 logicalCoordinates, 
			int &index) = 0;

		/*
		Converts a world-space position coordinate into a composite
		volume coordinate. The index component of the coordinate is
		the volume index (or -1 if the position coordinate is outside
		of all volumes), and the xyz component of the coordinate is
		the volume-local voxel XYZ coordinate that can be used to
		query per-voxel values of the indexed volume using the volume's
		scalar/vector accessor functions.
		*/
		struct VolumeCoordinate
		{
			int index = -1;
			Point3 xyz;
		};

		virtual VolumeCoordinate GetVolumeCoordinate(const Point3 pos) = 0;

		/*
		Helper function to convert temperature values from internal,
		normalized units to the specified unit type.
		*/
		enum TemperatureUnitType : int
		{
			normalized,
			celsius,
			fahrenheit,
			kelvin,
		};

		virtual float ConvertTemperature (
			const float temperature,
			const TemperatureUnitType units) = 0;
	};

	class tyVolumeObjectExt_v2 : public tyVolumeObjectExt_v1
	{
	public:
		/*
		Returns true if the user modified the underlying volumes since the
		last call to UpdateVolumes. If you are caching the returned data (ex:
		uploading the volumes to the GPU for rendering), calling this function
		tells you whether you need to re-cache the data since your last call 
		to UpdateVolumes, or whether your cached data is still up-to-date. 
		*/
		virtual bool VolumesChangedSinceLastUpdate() = 0;
	};

	class tyVolumeObjectExt_v3 : public tyVolumeObjectExt_v2
	{
	public:

		/*
		Returns a tyVolume by index (range: 0 - [NumVolumes()-1]).
		The interface retains ownership of these pointers and
		cleans them up in the call to ReleaseVolumes().
		*/
		virtual tyVolume_v2* GetVolume(const int index) = 0;

		/*
		Returns the tyVolume (if it exists, otherwise NULL) located
		at the specified logical coordinate point, as well as
		its interface index.
		The interface retains ownership of these pointers and
		cleans them up in the call to ReleaseVolumes().
		*/
		virtual tyVolume_v2* GetVolume(
			const IPoint3 logicalCoordinates,
			int& index) = 0;

		/*
		Returns the world-space raymarching step size specified
		by the corresponding volume display operator in tyFlow. 
		This is an optional parameter that 3rd-parties can use
		to match their raymarching step size with tyFlow's viewport
		volume display step size, when rendering volumes returned 
		by this interface.
		*/
		virtual float GetVolumeRaymarchingStepSize() = 0;

	};

	class tyVolumeObjectExt_v4 : public tyVolumeObjectExt_v3
	{
	public:

		/*
		Returns a tyVolume by index (range: 0 - [NumVolumes()-1]).
		The interface retains ownership of these pointers and
		cleans them up in the call to ReleaseVolumes().
		*/
		virtual tyVolume_v3* GetVolume(const int index) = 0;

		/*
		Returns the tyVolume (if it exists, otherwise NULL) located
		at the specified logical coordinate point, as well as
		its interface index.
		The interface retains ownership of these pointers and
		cleans them up in the call to ReleaseVolumes().
		*/
		virtual tyVolume_v3* GetVolume(
			const IPoint3 logicalCoordinates,
			int& index) = 0;	

	};

	class tyVolumeObjectExt_v5 : public tyVolumeObjectExt_v4
	{
	public:

		/*
		3rd-parties accessing the tyVolumeObjectExt interface may want
		to warn end-users if volumes exist, but are inaccessible
		through the interface (for example, if tyFlow is unlicensed,
		NumVolumes() will return zero even if internal volumes exist).
		However, just checking if NumVolumes() is non-zero is
		insufficient, because there may simply be no internal volumes
		to return at the time of query, even if a valid license is
		activated. This function allows you to check whether the
		interface license is activated, and how many actual, internal
		volumes exist, independently. If the function returns false, and
		numActualVolumes is greater than zero, then the end-user
		can be notified that the reason the interface is returning
		no volume data (NumVolumes() is zero) is because no license
		is activated. If the function returns false, and
		numActualVolumes is zero, then no warning is necessary
		because no volume data will be returned either way.
		This function should only be called after UpdateVolumes().
		*/
		virtual bool LicenseActivated(int& numActualVolumes) = 0;
	};


	class tyVolumeObjectExt_v6 : public tyVolumeObjectExt_v5
	{
	public:

		/*
		Returns a tyVolume by index (range: 0 - [NumVolumes()-1]).
		The interface retains ownership of these pointers and
		cleans them up in the call to ReleaseVolumes().
		*/
		virtual tyVolume_v4* GetVolume(const int index) = 0;

		/*
		Returns the tyVolume (if it exists, otherwise NULL) located
		at the specified logical coordinate point, as well as
		its interface index.
		The interface retains ownership of these pointers and
		cleans them up in the call to ReleaseVolumes().
		*/
		virtual tyVolume_v4* GetVolume(
			const IPoint3 logicalCoordinates,
			int& index) = 0;

		enum VolumeComponents : int
		{
			none			= 0,
			voxels			= 1 << 0,
			displaySettings	= 1 << 1,
		};

		/*
		Returns a mask containing VolumeComponents flags that 
		describe which parts of the volume have changed since the
		last call to UpdateVolumes. 
		
		A return value of VolumeComponents::none entails that no part 
		of the volume has changed since last update. 
		A return value with VolumeComponents::voxels flag set means that voxel
		values have changed since the last update and all volumes need to
		be re-synchronized.
		A return value with VolumeComponents::displaySettings flag set means
		that display settings (opacity overrides, color overrides, LUTs, etc)
		have changed and need to be re-synchronized.
		
		For example, if a renderer is independently syncing grids and display
		settings with the accessor functions provided by tyVolume_v4, and 
		VolumeComponentsChangedSinceLastUpdate returns a mask which only 
		contains the VolumeComponents::displaySettings flag, then only display 
		settings (LUTs/opacity/color/etc overrides returned by the interface)
		will need to be synchronized by the renderer again - any grids synchronized 
		since the prior call to UpdateVolumes will still be valid.

		NOTE: if VolumeComponentsChangedSinceLastUpdate returns a non-zero 
		value, UpdateVolumes will need to be called again prior to synchronization. 
		As of tyFlow v2.100, UpdateVolumes will only internally update what's 
		necessary in order to ensure all accessor functions are up to date,
		leading to faster results when UpdateVolumes is called multiple times
		in succession, or when only LUTs changed since the last update, etc.
		
		Another caveat to keep in mind when using a workflow involving 
		VolumeComponentsChangedSinceLastUpdate is that the original ReleaseVolumes 
		function will always flag all voxels and displaySettings as changed because
		it clears out all cached volume data. So if you want to retain cached
		data that has not changed since the last call to UpdateVolumes, do not
		call ReleaseVolumes until you are ready for the entire cache to be cleared. 

		So an example setup for a renderer might be something like:
		* [user begins IPR rendering]
		* Call UpdateVolumes
		* Synchronize grids/LUTs
		* Render frame
		* [user swivels camera in scene]
		* Call VolumeComponentsChangedSinceLastUpdate()
		* No volume components changed, so no synchronization necessary
		* Render frame
		* [user tweaks volume LUT]
		* Call VolumeComponentsChangedSinceLastUpdate()
		* displaySettings flag set, so re-synchronize display data
		* Render frame
		* [user advanced a frame on the timeline]
		* Call VolumeComponentsChangedSinceLastUpdate()
		* voxels and displaySettings flags set, so re-synchronize all volume data
		* Render frame
		* [user ends IPR rendering]
		* Call ReleaseVolumes
		*/
		virtual VolumeComponents VolumeComponentsChangedSinceLastUpdate() = 0;
	};

	/*
	Helper functions to return the tyVolumeInterfaces from any given
	base object, if they're implemented. Developers should not
	assume that only tyFlow objects will return the volume interface,
	as other tyFlow-adjacent object classes may be extended to return
	this interface in the future (ex: tyCache). In other words, check
	all base objects for this interface, not just objects with a
	matching tyFlow ClassID.
	*/
	inline tyVolumeObjectExt_v1*
	GetTyVolumeInterface_v1 (BaseObject *obj)
	{
		return (tyVolumeObjectExt_v1*)obj->GetInterface(TYVOLUME_INTERFACE_V1);
	}

	inline tyVolumeObjectExt_v2*
	GetTyVolumeInterface_v2(BaseObject* obj)
	{
		return (tyVolumeObjectExt_v2*)obj->GetInterface(TYVOLUME_INTERFACE_V2);
	}

	inline tyVolumeObjectExt_v3*
		GetTyVolumeInterface_v3(BaseObject* obj)
	{
		return (tyVolumeObjectExt_v3*)obj->GetInterface(TYVOLUME_INTERFACE_V3);
	}

	inline tyVolumeObjectExt_v4*
		GetTyVolumeInterface_v4(BaseObject* obj)
	{
		return (tyVolumeObjectExt_v4*)obj->GetInterface(TYVOLUME_INTERFACE_V4);
	}

	inline tyVolumeObjectExt_v5*
		GetTyVolumeInterface_v5(BaseObject* obj)
	{
		return (tyVolumeObjectExt_v5*)obj->GetInterface(TYVOLUME_INTERFACE_V5);
	}

	inline tyVolumeObjectExt_v6*
		GetTyVolumeInterface_v6(BaseObject* obj)
	{
		return (tyVolumeObjectExt_v6*)obj->GetInterface(TYVOLUME_INTERFACE_V6);
	}
};