PFlow - reading user data channels from outside of PFlow - solved


Ok, after a lot of fighting and with a lot of help from users here, I’ve found a way to do that.
Just use the following code in you script where you want to read the data from PFlow:

-- insert a script operator into every event in PFlow. This script will write out user channel values to a global particle_user_data array
global particle_user_data=#()
undo "Pflow edit" on ( -- undo is necessary for Max to see the PFlow change
	ms_op = Script_Operator name:(uniquename "EFXtemp_ms_op_")
	ms_op.Proceed_Script="on ChannelsUsed pCont do 
 pCont.useInteger = true 
	 pCont.useFloat = true 
 pCont.useVector = true 
 pCont.useMatrix = true 

 on Proceed pCont do 
 for i in 1 to pnum do 
 pCont.particleIndex = i 
	for o in helpers where classof o == Event do (
	if o.numactions()>0 AND classof (o.getAction 1)==Birth then o.insertAction (instance ms_op) 2 else o.insertAction (instance ms_op) 1
-- parse through animation range reading Particle User data from the global particle_user_data array at each frame
start = (animationrange.start as integer)*framerate/4800
end = (animationrange.end as integer)*framerate/4800
pf=$ -- store the PFlow object you want to work with	
for fr in start to end do (
	completeRedraw() -- without it Max will sometimes seem to hang until the whole process is over
	if pf.numParticles()>0 then for i in 1 to pf.numParticles() do (
		-- do something with the data - remember it will be overwritten on the next frame with new data, so you have to copy it here to a new array or something
-- PFlow cleanup
delete $'EFXtemp_ms_op_*'
try particleFlow.delete $'Action Recovery' catch()


i didn’t check it for working but

(animationrange.end as integer)*framerate/4800 is same as animationrange.end.frame
…*framerate/4800 is the same as …/ticksperframe


New day, new lesson learned :slight_smile:


you can do the same thing but much easier. You don’t need to collect array of user channel values for every particle (it kills memory pretty soon). You can just store ParticleContainer itself on Procced event.

Here is a scenario:

  1. create custom Attribute for the patricleFlow of type #MaxObject (lets say “container”).
  2. On Proceed event check NumParticles in current ParticleContainer and if the number more then ZERO assign it to “container” property.
  3. Get any data for any particle using MaxscriptParticleContainer interface.


Well this script stores user data for every particle but just on the current frame, on the next frame it’s ovewritten by new data so memory is not so much an issue.

Also with your idea I also have to add a script operator to every event, that would do what you suggested, because for some reason any access to user data from the global event resets the user data :frowning: as I posted in another thread.

The third thing, are you absolutely sure that it would work with User data channels.
I’ve seen many people fighting with this and for some reason user data channels are accessible only from within script operators - that’s why I’ve had this long issue to find a way of accessing it from an external script :slight_smile:


any massive array operations made with max script causes memory leaking. And it’s THE issue

you can add you script operator in any place and store ParticleContainer before global event resets the data

i’m absolutely sure that my code works for any particle data including custom channels

try(destroydialog testRol) catch()
global pcf
global pAmount = if pAmount == undefined then 100 else pAmount 
rollout testRol "PF Update by denisT" 
	spinner sp "Amount: " range:[0,1000,pAmount] type:#integer fieldwidth:56
	button create "Create PF" width:180 offset:[0,4]
	fn script =
		source = ""
		source += "on ChannelsUsed pCont do (pCont.usePosition = pCont.useVector = on)
		source += "on Proceed pCont do if pCont.NumParticles() > 0 do
		source += "(
		source += "	(pCont.getParticleSystemNode()).container = pCont
		source += "	seed currenttime
		source += "	for i=1 to pCont.NumParticles() do
		source += "	(
		source += "		pCont.particleIndex = i
		source += "		pCont.particleVector = [i, pCont.particleID, random 0.0 1.0]
		source += "	)
		source += ")

	local op1
	on create pressed do --undo "Create" on 
		try (delete objects) catch()
		pcf = PF_Source name:"Test" X_Coord:0 Y_Coord:0 Emitter_Length:10 Emitter_Width:10 \
			Quantity_Viewport:100 Show_Logo:on Show_Emitter:on wirecolor:blue

		custAttributes.add pcf \
			attributes DTS_NodeAttributes (parameters extra (container type:#maxobject))
		op1 = Birth name:"_birth" amount:pAmount 
		op2 = Position_Icon()
		op3 = Speed()
		op4 = ShapeStandard shape:2 size:0.8
		op5 = DisplayParticles name:"_display" color:pcf.wireColor
		op6 = RenderParticles()
		ev1 = Event name:"_event"
		ev1.SetPViewLocation (pcf.X_Coord) (pcf.Y_Coord+100)
		op7 = Script_Operator name:"_operator"
		op7.Proceed_Script = script()
		ev1.AppendAction op1
		ev1.AppendAction op2
		ev1.AppendAction op3
		ev1.AppendAction op4
		ev1.AppendAction op5
		pcf.AppendAction op6
		pcf.AppendAction op7
		pcf.AppendInitialActionList ev1
		sp.value = pcf._event._birth.amount
		select pcf
	on sp changed val do if iskindof pcf PF_Source do
		pcf._event._birth.amount = pAmount = sp.value
		pcf.activateParticles on
createdialog testRol width:200 height:60

slidertime = 20
pcf.container.particleIndex = 10
pcf.getParticlePosition 10


Oh you’re good :slight_smile:


This thread has been automatically closed as it remained inactive for 12 months. If you wish to continue the discussion, please create a new thread in the appropriate forum.