How to make it faster?


#1

Hi,

I’ve been trying to make my script much faster. I’ve read everything in How to make it faster? help chapter and tried to apply as much as I can.

My code is now 103 lines only (with GUI), mostly while loops, eventually for obj in, ofcourse I’ve disabled viewport redraw and I’m using bitarrays instead arrays wherever I can.

But still my code is pretty slow. It’s a voronoi fracturing script. I haven’t implemented any voronoi point generation pattern yet, I’m using mesh vertices as clusters. As for now that it’s working properly I want to boost up the speed.

As I said it’s pretty slow right now. 92 pieces are beeing cut in about 5-8 seconds. And I would like to make it almost realtime.

I know it can be done, just look here: https://vimeo.com/121768524 or at least take a look for mir vadims voronoi modifier: https://www.youtube.com/watch?v=2Fro0wLw86g

This is my first script that I’m trying to make it better, faster. All the scripts I’ve written out so far were pretty simple tools that didn’t need improvements like this one. I treat this as a great exercise.

So what can I do exept applying all from How to make it faster? chapter? Can I somehow track the times of my loops to detect where is the most computation? Do you have any suggestions for me?


#2

To “profile” your code you can use timestamp().

(
 	st = timestamp(); sh = heapfree
 	for j = 1 to 1000000 do ()
 	format "time:% ram:%
" (timestamp()-st) (sh-heapfree)
 )

Using BitArrays is not always faster than using Arrays.
Using Return() does not always have a huge impact in performance.
Using Exit or Continue does usually have a big impact in performance.

If you are creating nodes on the fly you can:

Avoid selecting-deselecting them

Disable Undo

Move to other panel than Modify

I haven’t found a fixed rule for optimization. In my experience it all depends on the code.


#3

Undo off can really speed up things. Predefining arrays and their sizes also can gain you a few performance. (Mapped functions are really good performance boosters but their usage are very limited. ) If you can convert your editable poly to editable mesh by using snapshot(), it can also speed up things.


#4

1st example is using .Net+3rdparty lib for calculating and im sure Rayfires Voronoi modifier is written as a native C++ plugin.
Not sure if you can get realtime fracturing by using Maxscript only.


#5

IShutter is using Qhull for cluster generation only, the fracturing is done in maxscript. I’m not generating any points for now, I’m using mesh vertices as clusters. However rayfire is probably developed in c++


#6

I’m sure you can! The only thing that may ruin the performance is the bottleneck of maxscript. So at a certain point it will be real-time, and after that point mxs will lose it’s performance. I have written for example a conform tool that can handle thousands of polys almost immediately (based on built in rayIntersect method, that uses brute force intersect search engine) or a splineFFD that can control ten-thousands of polygons with soft selection, all done with pure mxs. So I think you can do a lot of things, and I don’t think that voronoi fractureing is so compute heavy, as a conform tool for example. :slight_smile: (but I’m not sure about that. I didn’t make a tool like that before. :))


#7

Would you mind sharing the piece of code you think is slow and a practical test?


#8

Sure

fn getMiddlePos A B = ( -- getting the middle point position
	(A + B) * 0.5
)

fn setSlicePlane A B = ( -- function to get the rotation of the slice plane
	global rot = (inverse (matrixFromNormal (A - B))).rotationpart
)

fn getVertPos obj num = ( -- this is a function I need to remove
	meshop.getVert obj num
)

fn fracture p1 p2 obj frag = ( -- slicing the object
	collapseStack frag
	Slicer = SliceModifier()
	Slicer.Slice_Type = 3
	addModifier frag Slicer

	setSlicePlane (getVertPos obj p1) (getVertPos obj p2)
	
	frag.modifiers[1].Slice_Plane.rotation = rot
	frag.modifiers[1].Slice_Plane.position = getMiddlePos (getVertPos obj p1) (getVertPos obj p2)
		
	addModifier frag (Cap_Holes ())
)

fn getArrNum bitArr = (
	i = 1
	while bitArr[i]==true do (
		i += 1
	)
	i
)

fn CalculateVolume obj = ( -- calculate volumes
	local Volume= 0.0
	local theMesh = snapshotasmesh obj
	local numFaces = theMesh.numfaces
	for i = 1 to numFaces do (
		local Face= getFace theMesh i
		local vert2 = getVert theMesh Face.z
		local vert1 = getVert theMesh Face.y
		local vert0 = getVert theMesh Face.x
		local dV = Dot (Cross (vert1 - vert0) (vert2 - vert0)) vert0
		Volume+= dV
	)
	delete theMesh
	Volume /= 6
	Volume
)

fn colorizeGroup Objectz Colors = ( -- actual colouring
	i=1
	while i<=Objectz.count do (
		Objectz[i].wirecolor = Colors[i]
		i+=1
	)
)

fn setColorByVolume Objectz = ( -- collect object by volume, set colors
	local colors = #()
	j=1
	while j<=Objectz.count do (
		volume = CalculateVolume Objectz[j]
		append colors volume
		j+=1
	)
	scaleV = (amax colors) - (amin colors)
	scaleR = maxCol.x - minCol.x
	scaleG = maxCol.y - minCol.y
	scaleB = maxCol.z - minCol.z
	
	global outColors=#()
	i=1
	while i<=Objectz.count do (
		R = ((colors[i] - (amin colors))/(amax colors))*scaleR
		G = ((colors[i] - (amin colors))/(amax colors))*scaleR
		B = ((colors[i] - (amin colors))/(amax colors))*scaleR
		outColor = [R,G,B]
		append outColors outColor
		i+=1
	)
)	

	maxCol = [220,220,220]
	minCol = [5,5,5]

try(destroyDialog NightmareFracture)catch()
rollout NightmareFracture "NightmareFracture" width:200 height:200 (
-- 	maxCol = [220,220,220]
-- 	minCol = [5,5,5]
	
	button __testScene "Create Test Objects"
	button __fracture "Fracture"
	colorpicker __minCol "Low Color:" color:[5,5,5] pos:[20, 150] width: 120 height:15
-- 	on __minCol changed arg do __minCol.color=minCol
	colorpicker __maxCol "High Color:" color:[220,220,220] pos:[20, 175] width: 120 height:15
-- 	on __maxCol changed arg do __maxCol.color=minCol
	
	on __testScene pressed do (
		with redraw off (
			for obj in geometry do (
				if obj==theGeo do delete obj
			)
			global theGeo = Box lengthsegs:1 widthsegs:1 heightsegs:1 length:150 width:150 height:150 mapcoords:on pos:[0,0,0] 
			centerPivot theGeo
			theGeo.position = [0,0,0]
			global theVor = Geosphere radius:25 pos:[0,0,0] segs:3 boxmode:on
			theNoise = Noisemodifier()
			addModifier theVor theNoise
			theNoise.scale = 10
			theNoise.strength = [100,100,100]
			theNoise.seed = random 0 999999
		)
	)
	
	on __fracture pressed do (
		local startF = timeStamp()
		Undo off()
		suspendEditing()
		with redraw off (
			VertexCloud = getNumVerts theVor
			clusters=#{}
			fragmens=#()
			
			for i=1 to VertexCloud do (
				append clusters i
			)

			cluster=1
			while cluster <= clusters.count do ( -- cutting each piece
				local start = timeStamp()
				resetXform theGeo
				
				KeepCopy = snapshot theGeo
				
-- 				format "KeepCopy is classof %
" (classof KeepCopy)
				
				clusters[cluster]=false
				i=1
				for i in clusters do ( -- cutting each slice
					num = getArrNum clusters
					if num==undefined do num=1
			 		fracture num i theVor theGeo
				)
				clusters[cluster]=true
				
				centerPivot theGeo
				theGeo.name = uniquename "frag_001"
-- 				theGeo.wirecolor = [random 0 255, 255, 255]
				
				collapseStack theGeo
				append fragmens theGeo
				
				theGeo = KeepCopy
-- 				format "theGeo is classof %
" (classof theGeo)
				cluster+=1
				
				local end = timeStamp()
-- 				format "fracured piece no % within % seconds
" cluster ((end - start) / 1000.0)
			)
			delete KeepCopy
		)
		setColorByVolume fragmens
		colorizeGroup fragmens outColors
		Undo on()
		resumeEditing()
		
		local endF = timeStamp()
		
		format "fractured % fragments within: % seconds
" VertexCloud ((endF - startF) / 1000.0)
	)
)createDialog NightmareFracture

All you need is just execute, it will create test objects.

I just added colouring by volume size.

Sorry about poor descriptions in the code, but I’m writing it “on the fly” so I don’t need to comment this right now. But I’ll probably do it soon because it’s starting to be more complicated


#9

Thank you for sharing it.

At first glance, without testing the code yet, I can see you are using modifiers, collapsing the stack, etc.

There is no way you can use all these actions and get a good performance. You may get a decent speed, but if you really want to make it the fastest way possible in MXS, I would suggest building all the meshes from the ground.

Perhaps you could have a set of predefined blocks and modify the vertices, UVs and SG to speed it up. Don’t know how many different meshes does Voronoi generate, but if they are just a few, it may work.

I will try it and see how it goes.


#10

I’ve looked at the code and what slows it down are indeed the modifiers and collapsing the stack.

Other things could be improved but they wouldn’t have as much impact as building the geometry mesh instead of using modifiers.

For 93 blocks the code makes 8372 calls to:
collapseStack node
addModifier node (SliceModifier())
addModifier node (Cap_Holes ())

All inside the Fracture() function. I don’t see a way to considerably speed it up other than building the meshes yourself.

I have never implemented a Voronoi algorithm, so I don’t know how much it would cost to build the cells, but once you have that data building the meshes shouldn’t take too long. I think that’s what we see in the first video you posted. All the Voronoi calculations are done in C# and then the meshes are built in Max.

Also the video doesn’t show if the mapping, materials IDs, are set. If they are not, it would be a little faster.


#11

slice and cap can be done on editable_poly level. it doesn’t need modifiers


#12

I have just run through the code you shared and I think the proboolean or procutter might be a better solution. I would give them a chance if it works worse or better. (Using modifiers are slow because they always have to go through the modifiers stack pipeline which is slower than using base class object methods.)

You shold take a look at the methods that proboolean and procutter offer. (Just an idea… :))

Oh and I noticed that you don’t use polyop methods (that’s what denisT mentioned). Search them in mxs help. They are a lot more faster.


#13

Take a look here:
http://forums.cgsociety.org/archive/index.php?t-251559.html


#14

no… it’s too old. i’m sure you can do it better today :wink:

the bottle neck of any modifier using is ‘a lot of notifications’ are forced to be taken by system. usually the first one makes all job, but every next does do it again anyway.

so… try to use editable poly methods. it’s what i would do

(btw… i usually prototype my every tool by the mxs first. and if the idea works i rewrote it on c++ sdk and get 10+ times speedup with almost zero memory use)


#15

the first link shows the thing that can be easily reproduced. technically it can be done by pure mxs with similar performance.

the second one is big. many options, big and smart user interface.
but… hmm… performance is not really impressive. all shown is not more than 50K verts, but we can see lugging.


#16

http://www.scriptspot.com/3ds-max/scripts/fracture-voronoi

Despite it’s really old, it’s still pretty fast. If we take into consideration that it uses old methods, it’s a really good job.(modifiers in this case)

So I would take a look at the things, in this script, if I may find something useful.


#17

Maxscript has a huge runtime overhead compared to compiling a plugin in C++. When you are running a maxscript and then break in the visual studio debugger, you can easily see a callstack as tall as your screen (And then some too). Now that you have a proof of concept, write it in C++ and stop using maxscript: a dead language.


#18

Just for fun, using polyop methods. First, slicing for each pair:

try destroyDialog ::NightmareFracture catch()
  rollout NightmareFracture "NightmareFracture" width:200 height:200
  (
  	local obj, points
  
  	button __testScene "Create Test Objects"
  	button __fracture "Fracture"
  	colorPicker __minCol "Low Color:" color:[5, 5, 5] pos:[20, 150] width: 120 height:15
  	colorPicker __maxCol "High Color:" color:[220, 220, 220] pos:[20, 175] width: 120 height:15
  
  	local getVertPos = polyOp.getVert
  	local deleteVerts = polyOp.deleteVerts
  	local cap = polyOp.capHolesByEdge
  	local slice = polyOp.slice
  
  	fn isPositiveDir pos vector vectorPos =
  		dot vector (pos - vectorPos) > 0
  
  	fn slicePoly obj pos vector =
  	(
  		local verts = obj.verts as bitArray
  		slice obj #all (ray pos vector)
  		deleteVerts obj (for vert in verts where isPositiveDir (getVertPos obj vert) vector pos collect vert)
  		cap obj #all
  	)
  
  	on __testScene pressed do
  	(
  		obj = Box lengthSegs:1 widthSegs:1 heightSegs:1 length:150 width:150 height:150 mapCoords:on pivot:[0, 0, 75] pos:[0, 0, 0]
  		vor = convertToMesh (GeoSphere radius:25 pos:[0, 0, 0] segs:3 boxMode:on)
  		addModifier vor (NoiseModifier scale:10 strength:[100, 100, 100] seed:(random 0 1e6))
  		points = for vert in vor.verts collect vert.pos
  	)
  
  	on __fracture pressed do with undo off, redraw off if obj != undefined AND points != undefined do
  	(
  		local start = timeStamp()
  		local pointCount = points.count
  
  		setCommandPanelTaskMode mode:#create
  
  		local tempMesh = copy obj pivot:[0, 0, 0]
  		resetXForm tempMesh
  		convertTo tempMesh Editable_Poly
  
  		local chunks = for p in points collect copy tempMesh prefix:#voro pivot:p isHidden:true
  
  		for p1 = 1 to pointCount - 1 do for p2 = p1 + 1 to pointCount while NOT keyboard.ESCpressed do
  		(
  			local pos = points[p1]
			local vec = (points[p2] - pos) / 2
			slicePoly chunks[p1] (pos + vec) vec
			slicePoly chunks[p2] (pos + vec) -vec
  		)
  
  		delete tempMesh
  		select chunks
  		selection.isHidden = false
  		redrawViews()
  
  		format "fractured % fragments within: % seconds
" pointCount ((timeStamp() - start) / 1e3)
  	)
  )
  createDialog NightmareFracture

Second, sorting by distance, slicing stops when the diagonal of the chunk is smaller than double the distance from the next point (i.e. next slice would be outside the mesh, no point continuing). Won’t make a difference here (might even slow it down instead) but useful in other cases:

try destroyDialog ::NightmareFracture catch()
  rollout NightmareFracture "NightmareFracture" width:200 height:200
  (
  	local obj, points
  
  	button __testScene "Create Test Objects"
  	button __fracture "Fracture"
  	colorPicker __minCol "Low Color:" color:[5, 5, 5] pos:[20, 150] width: 120 height:15
  	colorPicker __maxCol "High Color:" color:[220, 220, 220] pos:[20, 175] width: 120 height:15
  
  	local getVertPos = polyOp.getVert
  	local deleteVerts = polyOp.deleteVerts
  	local cap = polyOp.capHolesByEdge
  	local slice = polyOp.slice
  
  	struct pointProps (id, pos)
  
  	fn getLengthSquared v =
  		dot v v
  
  	fn isPositiveDir pos vector vectorPos =
  		dot vector (pos - vectorPos) > 0
  
  	fn compareDistance p1 p2 pointArr: centerPoint: =
  		getLengthSquared (pointArr[p1].pos - centerPoint) - getLengthSquared (pointArr[p2].pos - centerPoint) -- assuming the distance won't be smaller than one
  
  	fn slicePoly obj pos vector =
  	(
  		local verts = obj.verts as bitArray
  		slice obj #all (ray pos vector)
  		deleteVerts obj (for vert in verts where isPositiveDir (getVertPos obj vert) vector pos collect vert)
  		cap obj #all
  	)
  
  	on __testScene pressed do
  	(
  		obj = Box lengthSegs:1 widthSegs:1 heightSegs:1 length:150 width:150 height:150 mapCoords:on pivot:[0, 0, 75] pos:[0, 0, 0]
  		vor = convertToMesh (GeoSphere radius:25 pos:[0, 0, 0] segs:3 boxMode:on)
  		addModifier vor (NoiseModifier scale:10 strength:[100, 100, 100] seed:(random 0 1e6))
  		points = for vert in vor.verts collect pointProps id:i pos:vert.pos
  	)
  
  	on __fracture pressed do with undo off, redraw off if obj != undefined AND points != undefined do
  	(
  		local start = timeStamp()
  		local pointCount = points.count
  
  		setCommandPanelTaskMode mode:#create
  
  		local tempMesh = copy obj pivot:[0, 0, 0]
  		resetXForm tempMesh
  		convertTo tempMesh Editable_Poly
  
  		local chunks = for p = 1 to pointCount while NOT keyboard.ESCpressed collect
  		(
  			local center = points[p].pos
  
  			local neighborPoints = (#{1..pointCount} - #{p}) as array
  			qsort neighborPoints compareDistance pointArr:points centerPoint:center
  
  			local chunk = copy tempMesh prefix:#voro pivot:center isHidden:true
  
  			local distSquared = 0
  			local radiusSquared = getLengthSquared (points[neighborPoints[neighborPoints.count]].pos - center)
  			local i = 0
  
  			do
  			(
  				local vector = points[neighborPoints[i += 1]].pos - center
  				distSquared = getLengthSquared vector
  				radiusSquared = amin radiusSquared (4 * getLengthSquared (chunk.max - chunk.min))
  
  				slicePoly chunk (center + vector / 2) vector
  			)
  			while radiusSquared >= distSquared AND i < pointCount - 1
  
  			chunk
  		)
  
  		delete tempMesh
  		select chunks
  		selection.isHidden = false
  		redrawViews()
  
  		format "fractured % fragments within: % seconds
" pointCount ((timeStamp() - start) / 1e3)
  	)
  )
  createDialog NightmareFracture

#19

And how much faster the new with polyop than the original with modifiers? (Just courious. I can’t test it right now since I’m not at home) :rolleyes:


#20

Not that much, about 7 times faster.