Notifications
Clear all

[Closed] Baking a Psys to Animated Meshes

I have a particle system with fragments that I want to convert to meshes with keyframes.
I’m working from Bobo’s Snapshot Particles script, which snapshots the psys just fine.

The issue I’m running into is getting the snapshot-ed mesh’s transform to match to the particle’s transform. It seems like the ordering of the particles changes with every frame (so the snapshot meshes wildly rotate around and scale weird instead of just looking like their particle shape counter parts).

Anyone know why this doesn’t work as expected? Am I missing something?
Here is a link to a scene to test the code below on.


 
 (   
 	global bakerRL
 	try destroyDialog bakerRL catch()
 	rollout bakerRL "Psys Baker "
 	(
 		progressBar pbProgress1 "" pos:[5,5] width:198 height:30 value:0 color:[0,255,0]
 		progressBar pbProgress2 "" pos:[5,35] width:198 height:20 value:0 color:[255,0,0]
 		
 		on bakerRL open do 
 		(
 			if (selection.count == 1 and classof selection[1] == PF_Source) then 
 			(
 				local pick_pflow = selection[1]
 				local holder_mesh = Editable_Mesh()
 				local theTotal = pick_pflow.numParticles()
 				with undo off
 				(
 					local newMeshArr = #()
 					newMeshArr[theTotal] = 0
 					--snapshot each particles mesh
 					i = 0
 					finish = false
 					while not finish do
 					(
 						i += 1
 						pick_pflow.particleIndex = i
 						current_mesh = pick_pflow.particleShape
 						if current_mesh == undefined then
 							finish = true
 						else
 						(
 							new_mesh = Editable_Mesh()
 							new_mesh.mesh = current_mesh
 							new_mesh.transform = pick_pflow.particleTM
 							newMeshArr[i] = new_mesh
 							
 							local prog = 100 * (i) / theTotal
 							pbProgress1.value = prog
 						)
 					)
 					--now we can set the transforms for all the timeline keys
 					animButtonState = true
 					local myAnimStart = ((animationRange.start as integer)/TicksPerFrame)
 					local myAnimEnd = ((animationRange.end as integer)/TicksPerFrame)
 					for j = myAnimStart to myAnimEnd do
 					(
 						sliderTime = j
 						for g = 1 to theTotal by 1 do
 						(
 							pick_pflow.particleIndex = g
 							newMeshArr[g].transform = pick_pflow.particleTM
 						)
 						myPercent = 100 * j / myAnimEnd			
 						pbProgress2.value = myPercent
 					)
 					animButtonState = false
 				)
 			)
 		)
 	)
 	createDialog bakerRL 210 60 100 100 style:#(#style_toolwindow, #style_sysmenu)
 )
 

Anyone’s help is appreciated.

2 Replies
 lo1

haven’t tried your example, but I know this script by Ofer Zelichover gives good results for baking pflows:

http://www.scriptspot.com/3ds-max/scripts/pflow-baker

lo, thanks for the pointer to that script. i figured out what the issues were:

Any particle flow can contain any number of particles at any given frame, and the <PF_Source>.numParticles() method reflects this by returning the total number of particles ‘alive’ at the current frame. this number changes as operators add or subtract particles from the particle flow. the <PF_Source>.particleIndex changes as well, so you can’t rely on correlating a node to a particleIndex. Instead, you have to use <PF_Source>.particleID, which (I think) is a unique ID assigned at birth to a particle. In the code example below, I’m creating two arrays and storing references to nodes in one, and particleIDs in the other, so later on I can set the transforms of the snapshotted meshes.

The other issue is that particles can be born on arbitrary frames, so I’ve got to step through every frame checking/collecting unique particleIDs. If the particle flow spawns particles on a negative frame, or outside the timeline’s range, the script below ignores them, which is not elegant, and is difficult to check for (I think).

Here is a script that does what I want it to do on the archive I provided a link to.
It’s not pretty, and I need to refactor it to step through the timeline’s frames only once, but it will bake a particle system to meshes with keyframes.


 
 (	--select a particle flow to operate on, then run this script
 	(
 		source = ""
 		source += "using System;
"
 		source += "using System.Runtime.InteropServices;
"
 		source += "class WindowsGhosting
"
 		source += "{
"
 		source += " [DllImport(\"user32.dll\")]
"
 		source += " public static extern void DisableProcessWindowsGhosting();
"
 		source += "}
"
 		csharpProvider = dotnetobject "Microsoft.CSharp.CSharpCodeProvider"
 		compilerParams = dotnetobject "System.CodeDom.Compiler.CompilerParameters"
 		compilerParams.GenerateInMemory = on
 		compilerResults = csharpProvider.CompileAssemblyFromSource compilerParams #(source)
 		assembly = compilerResults.CompiledAssembly
 		windowsGhosting = assembly.CreateInstance "WindowsGhosting"
 		windowsGhosting.DisableProcessWindowsGhosting()
 	)
 	global bakerRL
 	try destroyDialog bakerRL catch()
 	rollout bakerRL "Psys Baker "
 	(
 		label myLbl "Building meshes for all particles..." pos:[5,4] width:200
 		progressBar pbProgress1 "" pos:[5,20] width:198 height:20 value:0 color:[0,255,0]
 		progressBar pbProgress2 "" pos:[5,45] width:198 height:20 value:0 color:[255,0,0]
 		
 		label myLbl2 "Baking animation to meshes..." pos:[5,70] width:200
 		progressBar pbProgress3 "" pos:[5,87] width:198 height:20 value:0 color:[0,255,0]
 		progressBar pbProgress4 "" pos:[5,107] width:198 height:20 value:0 color:[255,0,0]
 		
 		on bakerRL open do 
 		(
 			if (selection.count == 1 and classof selection[1] == PF_Source) then 
 			(
 				local pick_pflow = selection[1]
 				local myAnimStart = ((animationRange.start as integer)/TicksPerFrame)
 				local myAnimEnd = ((animationRange.end as integer)/TicksPerFrame)
 				--local myAnimEnd = 10 --testing
 					
 				with undo off
 				(
 					print "Welcome to Psys Baker v1."
 					local nodesArr = #()
 					local IDsArr = #()
 					local theLayer = layerManager.newLayerFromName ("BakedPsysMeshes")
 					
 					fn checkID myID myIDsArr = 
 					(
 						check = false
 						for i=1 to myIDsArr.count do
 							(if myID == myIDsArr[i] then check = true)
 						return check
 					)
 					
 					--for every frame, check for new particle IDs, if found, add to array with reference to corresponding scene node
 					for j = myAnimStart to myAnimEnd do
 					(
 						sliderTime = j
 						i = 0
 						finish = false
 						while not finish do
 						(
 							i += 1
 							pick_pflow.particleIndex = i
 							current_mesh = pick_pflow.particleShape
 							if current_mesh == undefined then
 								finish = true
 							else
 							(
 								--is the particleID a unique ID (have we collected it already?)
 								local newID = pick_pflow.particleID
 								
 								if (checkID newID IDsArr) then 
 								(	--if true, then skip this particle, we've already collected it				
 								) else
 								(	--if false, then collect this particle ID and create a mesh for it, store both in arrays to be used later on
 									new_mesh = Editable_Mesh()
 									new_mesh.mesh = current_mesh
 									new_mesh.transform = pick_pflow.particleTM
 									
 									--put the new meshes onto a layer of their own
 									theLayer.addNode new_mesh
 									
 									append nodesArr new_mesh
 									append IDsArr newID
 									--we can correlate node to ID using these 2 associated arrays
 								)
 								local prog = 100 * (i) / myAnimEnd
 								pbProgress1.value = prog
 							)
 							local prog = 100 * (j) / myAnimEnd
 							pbProgress2.value = prog
 						)
 					)
 					print "Collection complete."
 					local theCount = nodesArr.count
 					format "Total unique particles: %.  Total meshes built: %.
" (IDsArr.count) (nodesArr.count)
 					if theCount != IDsArr.count then print "Associated array sizes do not match, errors will be present."
 					print "Baking animations to meshes..."
 					
 					--now we can set the transforms for all the timeline keys
 					animButtonState = true
 					for j = myAnimStart to myAnimEnd do
 					(
 						sliderTime = j
 						for g = 1 to theCount by 1 do
 						(
 							try(pick_pflow.particleID = IDsArr[g]; nodesArr[g].transform = pick_pflow.particleTM)catch()
 							--this will return an index out of range error if the particle hasn't been born yet, so try()catch() it
 							
 							local myPercent = 100 * g / theCount			
 							pbProgress3.value = myPercent
 						)
 						local myPercent = 100 * j / myAnimEnd	
 						pbProgress4.value = myPercent
 					)
 					animButtonState = false
 					print "Baking complete."
 					
 				)
 			) else (messagebox "Please select one particle flow to bake.")
 		)
 	)
 	createDialog bakerRL 210 130 100 100 style:#(#style_toolwindow, #style_sysmenu)
 )