Pivot Painter - Understanding Values corretly


First of all → The Pivot pointer is awesome.

But unfortunately I am trying since some time ago now how to use it correctly. I dont know what kind of values I have to put in even to make a simple cylinder be animated the way I want.

I use the default material from example 1.17 “Rotate_PerObjectPivot_wind_DirectionalAnimation” in content example. When I make a tube in 3ds max and select to make mesh black I kinda get a similar movement like in the content example. But the bottom of the cylinder also moves along.

Now I tried to stop the bottom of the tube to move along the “material wave”. But no matter what kind of values I put into the “Per Object Painter” the color preview always stays pink.

Can somebody explain me a bit about how to put values in correctly?

I already whatched the tutorial videos in the docs.


Edit: For example in the following picture you see my value’s from the pivot painter and alpha preview. What do I have to change so that the material wave is affecting everything but the bottom? The color preview shows the whole cylinder in pink… . With this settings the cylinder just stands still.


Ok I misunderstood that those examples just make use of the color information in the pivot painter.

When I rotate the the cylinder -90 degree on the axis around the Y-Axis then Pain the Selection Set I get the same rotational movement like in the example. But I am not really sure how the math behind it works in the blueprint.

Basically from what I understand, when you store the -90 rotation in the pivot painter, the “Pivot Postion” in the blueprint gives out negative values, like fro example -1. This minus value then ends up in the “Sine_Remapped” node giving the opposite RotationAngle Input to the “RotationAboutAxis” node. The same would happen when you set wind direction to (-1,0,0).

Also I don’t understand the Pivot Position Output. What is ment by “Pivot Point Information for each element”? What is ment by Element? Vertex? Branch? DetachedMesh?

Furthermore I don’t understand the value’s in the offset animation and increasing value by one unit part of the BP. For example the sphere mask is using an attenuation radius of 256 and for increasing by steps the dot product from wind and pivot position is divided by 128. I may understand that from the size of the object one has to adjust those value’s for desired effects, but how come those exact value’s were choses for this mesh size?

Am I the only one who has difficulty in understanding on how to use the pivot pointer correctly? If not it might be nice to get some more in depth tutorials about how to set up things in the right way.

I mean at the moment I barely know how to use it for a simple whipping for trees and that after I spend that last days on it…

hi spaceharry,

i have been looking into the same thing. i’m interested in replicating the script in blender. it takes time to reverse engineer pivot painter max script. understanding how it works makes things easier, so if any help from epic guys is very much appreciated.

The data format is listed in the help section of the script. The actual packing of data is fairly simple although a little non intuitive. The bulk of the script relates to the ui and prep tools section.

This is the text defining the data layout in the help section of the script (ignore the \r markup ):

Hierachy Painter Output\r
Child data:\r
Vert Alpha: parent[X][X] (0-1) \r
Vert Color: parent position XYZ (0-1)\r
UV channel 3: child pivot position XY (WS)\r + child [X][X],[X][Y] (0-1)\r
UV channel 4 R: child pivot position Z (WS)\r + child [X]Z\r
UV channel 4 G: parent[X]Y * Z sign\r

Parent Information: \r
Vert Alpha: parent[X][X] (0-1)\r
Vert Color: parent position XYZ (0-1)\r
UV channel 3: 0\r
UV channel 4 R: 0\r
UV channel 4 G: parent[X][Y] (0-1) * Z sign\r
Per Object Painter Output\r
With Optimize Off:\r
Vert alpha : Custom Falloff\r
Vertex color : X Vector (rotation) (0-1)\r
UV channel 3: Pivot position XY (WS)\r
UV channel 4 R: Pivot position Z (WS)\r
UV channel 4 G: Random value (0-1)\r

With Optimize On:\r
Vert alpha : Empty\r
Vertex color : Empty\r
UV channel 3: Pivot position XY(WS)\r
UV channel 4 R: Pivot position Z (WS)\r
UV channel 4 G: Random value (0-1)

This is the code specifically used to paint per object values (I’ll point out the necessary parts after) :

fn paintLocalValues paintVectorOrRotation:1	optimized:false=(
		with redraw off (
			for i=1 to LeafArray.count do(
					currMesh = LeafArray*
				if optimized==true then (
					polyop.setVertColor currMesh 3 #all [pivotPos[1],pivotPos[2],0]
					polyop.setVertColor currMesh 4 #all [pivotPos[3],(random 0.0 255.0),0]
				) else (
					localBoundingBox=nodeGetBoundingBox currMesh currMesh.transform
					localBoundingBoxDist= distance localBoundingBox[1] localBoundingBox[2]
					if paintVectorOrRotation == 1 then (
						myAngle=((((normalize currMesh.transform[1])*[1,-1,1])+1)/2)*255 -- Branch x axis vector --make the value 0-1 in unreal -- unreal inverts vert color y
					) else (
						myAngle= in coordsys world quatToEuler2 (inverse currMesh.rotation);
						myAngle= [myAngle.x,myAngle.y,myAngle.z]
					polyop.setVertColor currMesh 0 #all myAngle
					polyop.setVertColor currMesh 3 #all pivotPos
					polyop.setVertColor currMesh 4 #all [pivotPos[3],(random 0.0 255.0),0]--leaf pivot (0-1), leaf z rotation
					--Paint alpha per vertex 
					for v=1 to (getNumVerts currmesh) do (
						if keyboard.escPressed do ResumeEditing()
						currVert=polyop.getVert currMesh v
						currVertBaseObj=polyop.getVert currMesh.baseobject v
						finXScale = (distance gradBBX gradBBXTwo)
						finYScale = (distance gradBBY gradBBYTwo)
						finZScale = (distance gradBBZ gradBBZTwo)
						finXVal= (pow ((distance [0,currVertBaseObj[1],0] gradBBX)/finXScale) leafWingFalloffPowerX ) + (pow((distance [0,currVertBaseObj[1],0] gradBBXTwo)/finXScale) leafWingFalloffPowerX)
						finYVal= (pow ((distance [0,currVertBaseObj[2],0] gradBBY)/finYScale) leafWingFalloffPowerY ) + (pow((distance [0,currVertBaseObj[2],0] gradBBYTwo)/finYScale) leafWingFalloffPowerY)
						finZVal= (pow ((distance [0,currVertBaseObj[3],0] gradBBZ)/finZScale) leafWingFalloffPowerZ ) + (pow((distance [0,currVertBaseObj[3],0] gradBBZTwo)/finZScale) leafWingFalloffPowerZ)
						distanceToPivot=distance currVert currMesh.pos/255
						finalAlpha= clamp (((pow (distanceToPivot*leafLengthFalloff)leafLengthFalloffPower) +(finYVal*leafWingFalloffY)+(finXVal*leafWingFalloffX)+(finZVal*leafWingFalloffZ))*255) 0 255
						polyop.setVertColor currmesh -2 v [finalAlpha,finalAlpha,finalAlpha] 
				updateProgBar maxOperations:localMaxOperations currentOperation:i bar:2
		) catch(displayAutoErrorMessage())

A lot of that code is for the custom alpha values. You probably just need:

**store the x axis**
myAngle=((((normalize currMesh.transform[1])*[1,-1,1])+1)/2)*255 -- Branch x axis vector --make the value 0-1 in unreal -- unreal inverts vert color y. Basically, we are just constant bias scaling the values to a 0-1 range and then multiplying it by 255 for 3DS's vertex colors scale. 

polyop.setVertColor currMesh 0 #all myAngle -- feed the vector into the models vertex colors.


store the pivot point

polyop.setVertColor currMesh 3 #all pivotPos     --- this stores the X and Y values of the pivot point in R and G of uv Channel 3 (uv channel 2 in unreal. Unreals uvs are 0 based and max is 1 based)
polyop.setVertColor currMesh 4 #all [pivotPos[3],(random 0.0 255.0),0] -- just stores a random per element value in G

When testing to see if your values are being packed and read correctly in Unreal you can use the debug scalar or debug float 3 material functions. That will make the conversion process less confusing.

Btw improvements could be made to the way that pivot painter data is packed. Instead of using several channels to pack a rotation vector it could be done with just 1 uv channel. x(in the 0-1 range) + y(in the 0-.1) and z (in the .00-.01 range) added together into a single scalar value.

The current method for packing leaf rotation vectors is very poor as well. It kind of borders on random due to floating point accuracy. But this is the data format for the hierarchy painter if anyone is interested (note that the uv count that the script was initially written for was very limited):

fn convertWPtoUV Pos:[0,0,0] = (
	global temp=(Pos/shaderWSMultiplier)*[128,128,255]+[128,128,0]
	global tempx= clamp (temp[1]) 0 255
	global tempy= clamp (temp[2]) 0 255
	global tempz= clamp (temp[3]) 0 255
	global temp=[tempx,tempy,tempz]

fn paintLeaves = ( -- paints branches and leaves 
		with redraw off (
			global branchLocalBoundingBox=nodeLocalBoundingBox currBranchObj
			branchLocalBoundingBox = distance branchLocalBoundingBox[1] branchLocalBoundingBox[2]
			branchAngle=(normalize currBranchObj.transform[1])*[1.0,-1.0,1.0] -- Branch x axis vector (float 3) -- invert y for vert color 
			if branchAngle[3]<0 then (zSign=-1.0) else (zSign=1.0)
			branchAngle=((branchAngle+[1.0,1.0,1.0])/2.0)*255.0 -- Branch x axis vector (float 3) 
			branchAngle=[clamp branchAngle[1] 0 255,branchAngle[2],branchAngle[3]]
			global uv4Green=(branchAngle[2]*zSign)
			branchPos=convertWPtoUV pos:(currBranchObj.pos*[1,-1,1]) -- 0-1 range flip the y axis
			polyop.setVertColor currBranchObj -2  #all [branchAngle[1],0,0] -- set vert color for branch to branch position
			polyop.setVertColor currBranchObj 0  #all branchPos -- set vert color for branch to branch position
			polyop.setVertColor currBranchObj 3 #all [0,0,0] -- can be used for pixel shading if needed
			polyop.setVertColor currBranchObj 4 #all [0,uv4Green,0]
		for i = 1 to leafArray.count do (
				pivotPos=[ceil pivotPos[1],ceil pivotPos[2],ceil pivotPos[3]]
				xAxis=(((normalize currObj.transform[1])*.5)+.5)*255 -- leaf's x - axis
				xAxis=[clamp (xAxis[1]) 20 240, clamp (xAxis[2]) 20 240, clamp (xAxis[3]) 20 240] -- padding is added to avoid the lack of precision -- only works for near meshes
				if pivotpos[1]>0 then (pivotpos[1]+=xAxis[1]) else (pivotpos[1]-=xAxis[1])
				if pivotpos[2]>0 then (pivotpos[2]+=xAxis.y) else (pivotpos[2]-=xAxis.y)
				if pivotpos[3]>0 then (pivotpos[3]+=xAxis.z) else (pivotpos[3]-=xAxis.z)
				polyop.setVertColor currObj -2 #all [branchAngle[1],0,0]--branch - y axis
				polyop.setVertColor currObj 0 #all branchPos--branch position
				polyop.setVertColor currObj 3 #all pivotPos -- + x transform in the remainder
				polyop.setVertColor currObj 4 #all [pivotPos[3],uv4Green,0]--leaf pivot (ws), random value or constant bias scaled X with Z sign. use absolute value then scale bias to find x. Derive z from x and y to find z. Apply z sign to z channel.
				updateProgBar maxOperations:fnpaintleavesMaxOp currentOperation:i bar:3
		) catch(displayAutoErrorMessage())

so basically you constant bias scale the branch x axis vector. the find the sign of z and apply that as the sign of one of the other channels (y). so if Z was negative then we would multiply the y channel by -1. (In the shader we derive z from x and y but the returned z value is always positive so we need to store and reapply the sign after we use derive z in the shader)

you can probably read through the rest but if there are any specific questions i would be happy to help.

by the way, polyop.setVertColor currBranchObj -2 refers to vertex alpha. polyop.setVertColor currObj 0 #all branchPos is vertex color


The element mentioned in the documentation are separate models in 3ds max.

I’ll come back later to answer a few more of your questions.



I think what you’re seeing there is that you’re using a the Z falloff in the custom alpha control area. The X Y and Z sliders will brighten the meshes extremities on a given axis. Basically, it adds symmetrical gradients for a chosen axis.

You may want to use the 3d dist to pivot option instead. That will keep the root still while animating the top of the cylinder if used to modulate the rotate about axis angle input.

The color preview indicates which direction the X axis is pointing. For your cylinder you probably want to point your x axis upward. The alpha controls don’t affect that value. It should probably be labeled axis preview or something instead of color.

I wouldn’t think about stored information in terms of degrees. You should instead think of it as a vector. All of the operations we’re doing is based in vector math. So the value you store is the x axis vector. When we find a rotation axis we use cross products to find perpendicular vectors or we make use of the dot product operation to find gradients along a vector or to determine if vectors are facing each other.

Sorry, I’m not sure what this is in reference to. Values like 128 or 256 in these materials are generally just random values used to encapsulate the given models within the 0-1 range of a distance calculation or two change a textures tiling frequency.

So i think we are talking about example 1.17 “Rotate_PerObjectPivot_wind_DirectionalAnimation”.

It works in the following way (i’ll explain the individual chunks):


Create the rotation angle from the wind direction

We find the rotation vector by getting the cross product of the models x-axis vector (up for these examples) and the wind vector (positive x in this example). The result is negative y or (0,-1,0). We do it this way because it allows us to rotate models or place models at different angles while still allowing them to be affected by the wind correctly. Had we used a fixed rotate about axis vector all of the models would have to be rotated in the same direction.

Whenever you’re confused about the math I would recommend using the debugfloat3Values material function. It will display the resulting values for you.

Create world flow gradients


We take the dot product of the wind direction and the pivot position to return a gradient value that increments upward in the direction of the wind.

Each box returns 1 solid value across it’s entire surface because the dot product references the boxes pivot positions instead of world position (vertex position).

We also take the dot product of the wind with the cross product of the wind direction and the up vector. This returns a gradient the negative y vector again. We do this to get a gradient that’s 90 degrees off from the first gradient that we calculated. When combining the two gradients together using the append function we retrieve values that are similar to UV values which can be used for a texture. Which is what we’re creating in the next step.

We divide the “uv” style gradients by a large amount to reduce the texture tiling, animate them via time and pump the values into the uv slot of a texture.

**Add the DOT product result to the previous animation to offset it

Unfortunately, it looks like this comment was left over from a previous material. Regardless, in this step we’re just remapping the texture values to our liking for the rotate about axis operation.


Rotate the sub-models

Then we just rotate the models using the axis that created earlier and the rescaled texture values as the angle.

As an additional step, we calculate new normal values for the boxes using the same angle and axis from the previous rotate about axis node but this time we’re using the node called fixrotateaboutaxisnormals.

Hope that helps.

Thanks for this indepth explanation. I will check it out next time I use the pivot painter!

Maybe we could get max/maya plugin that does kind of world normal map baking, but instead of world normal map we get one that uses pivot painting formula.

@TechArt I realize this post is very old but can’t find much related information elsewhere. When fixing the vertex normals after rotating with world position offset, in this example the corrected normals are plugged into UV4 and 5. Why? Is this normally where vertex normals are stored and automatically accessed for calculation in lighting? Hoping to just plug into UV4 & 5 like this and the lighting/pixel shading pass just works.

If you look at the Normal Input, there are 2 TexCoord nodes being appended and plugged in. The 2 TexCoord nodes have to be set to Coord 4 and 5 (being an old engine version on the screenshot, the node doesnt show the coord number). The custom UVs for channel 4 and 5 are being defined there.
It also depends on the number of UV channels your mesh has, If you look at the example mesh, it already has 4 UV channel (0,1,2,3) and 4,5 are being defined in the material. So the 2 custom channels for the Normal correction will be after the existing channels on the mesh. Hope that helps