Notifications
Clear all

[Closed] Change face normals

I’m so disgruntled, I’ve spent over an entire day trying to get this func to work…just to rotate 1 vector to another vector!

I’m trying to use the dot product method because the equations are much simpler than using soh cah toa…but it seems using either method requires a million conditions…because the dot product doesn’t tell the angle to rotate by, it just tells the angle difference, so you have to figure out the sign on your own…this is not easy. Here’s what I’ve got…still not working right.


-- DESC: rotates an object assuming it is pointing in the direction of N1
--   so that it points towards N2
fn rotateByNorm obj N1 N2=
(
 a = normalize N1
 b = normalize N2
 -- 1 means increasing from N1 to N2
 -- -1 means deceasing from N1 to N2
 xchange = 1
 ychange = 1
 zchange = 1
 --determine if increasing or decreasing
 if b.x < a.x then
  xchange = -1
 if b.y < a.y then
  ychange = -1
 if b.z < a.z then
  zchange = -1
 
 --angle between points
 xrot = acos (dot [0,a.y,a.z] [0,b.y,b.z])
 yrot = acos (dot [a.x,0,a.z] [b.x,0,b.z])
 zrot = acos (dot [a.x,a.y,0] [b.x,b.y,0])
 
 --deal with negative rotations...
 if (a.y > 0 and a.z > 0 and zchange == -1 and ychange == 1) or \
	(a.y < 0 and a.z > 0 and zchange == 1 and ychange == 1) or \
	(a.y < 0 and a.z < 0 and zchange == 1 and ychange == -1) or \
	(a.y > 0 and a.z < 0 and zchange == -1 and ychange == -1)
 then
  xrot = xrot * -1
 if (a.x > 0 and a.z > 0 and zchange == -1 and xchange == 1) or \
	(a.x < 0 and a.z > 0 and zchange == 1 and xchange == 1) or \
	(a.x < 0 and a.z < 0 and zchange == 1 and xchange == -1) or \
	(a.x > 0 and a.z < 0 and zchange == -1 and xchange == -1)
 then
  yrot = yrot * -1

 if (a.x > 0 and a.y > 0 and ychange == -1 and xchange == 1) or \
	(a.x < 0 and a.y > 0 and ychange == 1 and xchange == 1) or \
	(a.x < 0 and a.y < 0 and ychange == 1 and xchange == -1) or \
	(a.x > 0 and a.y < 0 and ychange == -1 and xchange == -1)
 then
  zrot = zrot * -1
 
 rot = eulerangles xrot yrot zrot
 rotate obj rot
)

Sorry, just now I could find some time to write an example.


fn alignFacesToVector theObj theAlignVector =
(
	local theFaces = (polyOp.getFaceSelection theObj)as array  --get selected faces
	local theVerts = (polyOp.getVertsUsingFace theObj theFaces) as array --get all their vertices
	local theFaceNormal = polyOp.getFaceNormal theObj theFaces[1]  --get the first face normal
	local theCenter = [0,0,0] --define a variable to keep the center
	for v in theVerts do theCenter += (polyOp.getVert theObj v) --collect all vertex positions
	theCenter /= theVerts.count --divide by the vertex count to get the center of selection
    local theX = normalize (cross theAlignVector theFaceNormal) --cross product of normal(Z) and dir gives the X axis
	local theY = normalize (cross theX theFaceNormal) --cross product of X axis and normal (Z) gives Y axis
	local theMatrix = matrix3 theX theY theFaceNormal theCenter --build a matrix from X, Y and the normal (Z)
    local theAngle = acos (dot theFaceNormal theAlignVector) --find the angle between the matrix Z (normal) and dir.
	in coordsys theMatrix  --in coordinate system of this matrix, 
    	for v in theVerts do  --go through all vartices and rotate by that angle so the face normal points in the dir.
    		polyOp.setVert theObj v ((polyOp.getVert theObj v) * rotateXMatrix theAngle)
	redrawViews() --update the viewports
)

--Create a plane, convert to EPoly, select some faces.
--Create a Teapot and use its Z axis orientation as the reference
alignFacesToVector $Plane01 $Teapot01.dir

Bobo,

thanks for your reply. Your code works exactly the same as PrettyPixels, except you are using very different methods…I should do a speed comparison later.

Below appears to be what I was missing from my version.

 
--cross product of normal(Z) and dir gives the X axis
local theX = normalize (cross theAlignVector theFaceNormal)  
  
--cross product of X axis and normal (Z) gives Y axis
local theY = normalize (cross theX theFaceNormal) 
 
--build a matrix from X, Y and the normal (Z)
 local theMatrix = matrix3 theX theY theFaceNormal theCenter   

I’m not really understanding why it works.

Also, there are 2 different problems in this thread. You replied to my comment about the 2nd problem, and answered the first one

In my second problem, I am not trying to do any sub-object transformations. Rather, I want to simply rotate an entire object so that it is oriented towards a specified normal. I have been attempting to do this by finding an algorithm to compute the X/Y/Z rotations necessary to rotate 1 vector into the position of another vector. This way, I can take the first normal to be the axis of the object, and the second normal to be the direction I want the object to point.

After trying a bunch of complex soh cah toa equations, I then tried using 3 different dot products…to compute the angle in the X,Y,Z. The problem with this is that the dot product only gives the magnitude of the angle not the sign. Perhaps I should try using the cross product to come up with a series of exceptions for changing the sign…

Let me know if you have already worked up a function to do this, it seems like it should be pretty standard

1 Reply
(@bobo)
Joined: 1 year ago

Posts: 0

Sorry, I read just the beginning of the thread.
Once I came back home in the evening, I sat down and wrote my example without reading the rest of the thread. It was NOT optimized for speed, it simply shows the basics.

Below appears to be what I was missing from my version.

 
 --cross product of normal(Z) and dir gives the X axis
 local theX = normalize (cross theAlignVector theFaceNormal)  
   
 --cross product of X axis and normal (Z) gives Y axis
 local theY = normalize (cross theX theFaceNormal) 
  
 --build a matrix from X, Y and the normal (Z)
  local theMatrix = matrix3 theX theY theFaceNormal theCenter   
 

I’m not really understanding why it works.

Read the topic “How do I align the UVW_Modifier’s Gizmo to a selected face?” in the MAXScript Reference. I have included nice color illustrations to explain what happens…
My code here used the same concept.

What we tried to do is rotate something from one direction to another. We knew both normal vectors – the original direction and the desired direction. Obviously, to rotate from the one to the other, we would have to find the angle between these two vectors INSIDE THE PLANE defined by the two vectors and rotate about the axis that is perpendicular to that plane!
So I took the face normal and the final direction and calculated the cross product. This is a vector perpendicular to the two operands and the plane they define. This is my X vector, assuming the face normal is the Z vector. Then I found the cross product of the X and Z and got my Y which is perpendicular to the plane their define. Since X and Z are also perpendicular to each-other, we now have 3 vectors that define an orthogonal coordinate system. So I built a matrix out of the 3 and added the .row4 to be the center of rotation – the center of the selection.
Now, in the coordinate system of this matrix, the face normal points along the Z axis of the matrix, and in order to rotate it to point in the desired direction, we have to rotate about the X axis of our new coordinate system at the angle calculated using the DOT product…

Also, there are 2 different problems in this thread. You replied to my comment about the 2nd problem, and answered the first one

In my second problem, I am not trying to do any sub-object transformations. Rather, I want to simply rotate an entire object so that it is oriented towards a specified normal. I have been attempting to do this by finding an algorithm to compute the X/Y/Z rotations necessary to rotate 1 vector into the position of another vector. This way, I can take the first normal to be the axis of the object, and the second normal to be the direction I want the object to point.

After trying a bunch of complex soh cah toa equations, I then tried using 3 different dot products…to compute the angle in the X,Y,Z. The problem with this is that the dot product only gives the magnitude of the angle not the sign. Perhaps I should try using the cross product to come up with a series of exceptions for changing the sign…

Let me know if you have already worked up a function to do this, it seems like it should be pretty standard

To align an object to a normal, you don’t have to rotate it – you can just build the transformation matrix that describes the final orientation. But you can also use the same approach as in my code to define your own coordinate system find the angle of rotation and rotate the object at the angle calculated using DOT product. There is no big difference between sub-object transformations and object transformations when using matrices…

The sign is included in the vector which is used as axis of rotation.
A vector has two directions possible.
This is why you do not need sign for the angle.

If your rotation turns in the bad direction, reverse the order of the
vectors.

If this is erroneous:
rotAxis=cross v1 v2

then use this:
rotAxis=cross v2 v1

To know the direction of rotation of the axis, use the rule of the
right hand like in physics.

Hope this helps

I have just realized the speed test. The difference is not enormous. I would hope better…

With hundreds of repetitions:

vector method : Processing took 6.109 seconds
matrix method : Processing took 5.813 seconds

vector method : Processing took 6.156 seconds
matrix method : Processing took 6.031 seconds

vector method : Processing took 5.735 seconds
matrix method : Processing took 5.828 seconds

The code is very symmetrical.

fn getNormalOnFaceSelection obj theFaces =
	(
	local theNormal=[0,0,0]
	for face in theFaces do (theNormal+=polyop.getFaceNormal obj face)
	return (normalize theNormal)
	)

fn getCenterOnFaceSelection obj theFaces =
	(
	local theCenter=[0,0,0]
	for face in theFaces do (theCenter+=polyop.getFaceCenter obj face)
	return (theCenter/theFaces.numberSet)
	)

fn rotateByVector theObj theFaces theAxis theAngle theCenter =
	(
	local theVerts=polyOp.getVertsUsingFace theObj theFaces
	local q=quat theAngle (normalize theAxis)
	local vert
	for vert in theVerts do
		polyOp.setVert theObj vert ((((polyOp.getVert theObj vert)-theCenter)*q)+theCenter)
	)

fn rotateByMatrix theObj theFaces theMatrix theAngle =
	(
	local theVerts=polyOp.getVertsUsingFace theObj theFaces
	local theRotationMatrix=rotateXMatrix theAngle
	local vert
	in coordsys theMatrix
		for vert in theVerts do
			polyOp.setVert theObj vert ((polyOp.getVert theObj vert)*theRotationMatrix)
	)

fn alignFacesToVectorVECTOR obj theFaces theAlignVector =
	(
	local theFacesNormal=getNormalOnFaceSelection obj theFaces
	local theFacesCenter=getCenterOnFaceSelection obj theFaces
	local theAngle=acos (dot (normalize theFacesNormal) (normalize theAlignVector))
	local theAxis=cross theAlignVector theFacesNormal
	rotateByVector obj theFaces theAxis theAngle theFacesCenter
	)

fn alignFacesToVectorMATRIX obj theFaces theAlignVector =
	(
	local theFacesNormal=getNormalOnFaceSelection obj theFaces
	local theFacesCenter=getCenterOnFaceSelection obj theFaces
	local theAngle=acos (dot (normalize theFacesNormal) (normalize theAlignVector))
	local theX = normalize (cross theAlignVector theFacesNormal)
	local theY = normalize (cross theX theFacesNormal)
	local theMatrix = matrix3 theX theY theFacesNormal theFacesCenter
	rotateByMatrix obj theFaces theMatrix theAngle
	)

obj=plane length:200.0 width:200.0 lengthsegs:20 widthsegs:20
select obj
rotate obj (eulerangles 20 20 20)
convertToPoly obj
polyOp.setFaceSelection obj #{1..200}
theFaces=polyOp.getFaceSelection obj
alignVector=obj.dir
count=200


startTime=timeStamp()
undo off (
	for i=1 to count do alignFacesToVectorVECTOR obj theFaces alignVector
	)
endTime=timeStamp()
format "vector method : Processing took % seconds
" ((endTime-startTime) / 1000.0)


startTime=timeStamp()
undo off (
	for i=1 to count do alignFacesToVectorMATRIX obj theFaces alignVector
	)
endTime=timeStamp()
format "matrix method : Processing took % seconds
" ((endTime-startTime) / 1000.0)


redrawViews()

Oh That was just a test to compare the usage of the matrices and vectors…
It seems that both techniques are rather similar.
I would have thought that algebra matrix would be faster.

On the other hand I would be curious to know the techniques to increase speed.
Is it possible to apply the matrix to a group of points without using a for loop ?

2 Replies
(@bobo)
Joined: 1 year ago

Posts: 0

The main way to speed both approaches is to cache the methods in user variables to avoid the constant searching in the PolyOp structure. Even better, you could use a case statement to assign the correct PolyOp or MeshOp structure’s methods to the cache variables depending on the class of the object and thus make the function operate on both EPoly and EMesh.
For 1000 iterations (no idea what machine you were using, but 200 iterations needed only 1.5 seconds here, so I had to increase the counter) my function went from 8.5 to 7 seconds by just caching the GetVert and SetVert methods. The more vertices you manipulate, the better the results.

See “FAQ”>“How To Make It Faster”>“Cache freqeuntly used functions and objects” in MAXScript Reference 8.0

(@prettypixel)
Joined: 1 year ago

Posts: 0

You are right. It is a little faster in this way.
Thank you for the artfullness.

I changed the value to 200 to prevent that the user thinks his pc crashed.
I made the tests with 400. Here is the mystery.
But your machine goes nevertheless much more quickly than mine.

Read the topic “How do I align the UVW_Modifier’s Gizmo to a selected face?” in the MAXScript Reference. I have included nice color illustrations to explain what happens…
My code here used the same concept.

A good explanation, I like the toon shaded arrows.
So, basically we can use a matrix to represent a coordinate system by giving it vectors for each axis. Got it. How did you get involved in writing documentation for MAX SCript?

Next, we use a matrix to change the object. This is still confusing to me.

The code:

local theAngle = acos (dot theFaceNormal theAlignVector)

If the facenormal = [1,0,0] and the algin vector = [0,1,0] then the two vectors are at 90 degree angles in the XY plane,
and everything makes sense because the dot product is 10+01+0*0 = 0, and acos(0)=90.

But, what about when the vectors are in 3 axis like this:

dot (normalize [1,2,3]) (normalize [3,2,1]) = 0.71

The angle between them is 71 degrees…in what plane?? They should have a different angle in any
given plane…

Next,

polyOp.setVert theObj v ((polyOp.getVert theObj v) * rotateXMatrix theAngle)

Ok, we create a new matrix based on the mysterious angle. This matrix must be a columnar matrix with 3
rows and 1 columns for it to be multiplied by the point. It will be designed so that it causes a
rotation of X (in the YZ plane) of the amount specified by the angle.

Why are we rotating in the X plane though? Don’t we need to rotate in 2 different planes in order
to achieve any rotation in 3D space?

1 Reply
(@bobo)
Joined: 1 year ago

Posts: 0

That’s what matrices are for actually. Each node has a .transform matrix and IS a coordinate system. This is why you can pick a node in the Coordinate systems drop-down list, or use In Coordsys Box01 do ()

As for the help, they asked me around 2002, I was available, I got the job as external consultant… Have been doing it since Max 5.

Next, we use a matrix to change the object. This is still confusing to me.

The code:

local theAngle = acos (dot theFaceNormal theAlignVector)

If the facenormal = [1,0,0] and the algin vector = [0,1,0] then the two vectors are at 90 degree angles in the XY plane,
and everything makes sense because the dot product is 10+01+0*0 = 0, and acos(0)=90.

But, what about when the vectors are in 3 axis like this:

dot (normalize [1,2,3]) (normalize [3,2,1]) = 0.71

The angle between them is 71 degrees…in what plane?? They should have a different angle in any given plane…

According to basic stereometry, 3 points define EXACYLY one plane. Since all vectors have technically their start at [0,0,0] and their end at the Point3 defining them, having two vectors means you have 3 Point3 values, where [0,0,0] is the 3rd point. (You can translate them in space as you want, but this is what a Point3 value means internally!)
Having 3 points means we have EXACTLY one plane passing through both vectors. So the angle between these vectors is always in the plane these two vectors lie in.

Next,

polyOp.setVert theObj v ((polyOp.getVert theObj v) * rotateXMatrix theAngle)

Ok, we create a new matrix based on the mysterious angle. This matrix must be a columnar matrix with 3 rows and 1 columns for it to be multiplied by the point. It will be designed so that it causes a rotation of X (in the YZ plane) of the amount specified by the angle.

Actually, this matrix would rotate any point around the origin [0,0,0] about the X axis [1,0,0]. But since we transformed the complete code into our own coordinate system, the X axis is not the world axis anymore, but our custom X axis.

Why are we rotating in the X plane though? Don’t we need to rotate in 2 different planes in order to achieve any rotation in 3D space?

No, using our custom matrix, we simplied the 3D operation by transforming it into a TWO DIMENSIONAL operation! Instead of working in world space, we enforced our own special case coordinate system where all axes play well with what we are trying to achieve!

We found the plane passing through the two vectors, created a 3rd vector perpendicular to that plane, built a matrix out of that vector, the original normal which we assumed is axis Z, and a 3rd vector defined by the cross product of these two, and then translated the matrix to be coincident with the center of rotation we are interested in. Suddenly, in order to rotate our vector about that center means to rotate the Z axis of the coordinate system about the X axis of the coordinate system at the angle which was calculated in the YZ plane already.

I selected the X axis as the rotation axis deliberately. You could build the matrix taking the face normal as the X, the plane normal as Z and find the Y out of them, then define a rotateZmatrix to rotate the X axis about the Z axis. Does not matter much…

Hope this helps!

According to basic stereometry, 3 points define EXACYLY one plane. Since all vectors have technically their start at [0,0,0] and their end at the Point3 defining them, having two vectors means you have 3 Point3 values, where [0,0,0] is the 3rd point. (You can translate them in space as you want, but this is what a Point3 value means internally!)

Ah, ok

No, using our custom matrix, we simplied the 3D operation by transforming it into a TWO DIMENSIONAL operation! Instead of working in world space, we enforced our own special case coordinate system where all axes play well with what we are trying to achieve!

Ahh…that’s pretty cool.

…at the angle which was calculated in the YZ plane already.

But don’t we need to know what direction to rotate in, because the acos just gives us a magnitude?

Finally, your function isn’t quite working for me.

I replaced the poly ops with equivalent mesh ops because I can’t use edit polys, because I cannot choose the extrusion direction.

The replaced version:


fn meshAlignFacesToVector theObj theFaces theAlignVector =
(
local theVerts = (meshop.getVertsUsingFace theObj theFaces) as array --get all their vertices
local theFaceNormal = (getNormal theObj theFaces[1]) --get the first face normal
local theCenter = [0,0,0] --define a variable to keep the center
 
for v in theVerts do
(
theCenter += (meshop.getVert theObj v) --collect all vertex positions
)
 
theCenter = theCenter / theVerts.count --divide by the vertex count to get the center of selection
 
	local theX = normalize (cross theAlignVector theFaceNormal) --cross product of normal(Z) and dir gives the X axis
local theY = normalize (cross theX theFaceNormal) --cross product of X axis and normal (Z) gives Y axis
local theMatrix = matrix3 theX theY theFaceNormal theCenter --build a matrix from X, Y and the normal (Z)
	local theAngle = acos (dot theFaceNormal theAlignVector) --find the angle between the matrix Z (normal) and dir.
 
in coordsys theMatrix --in coordinate system of this matrix, 
	 for v in theVerts do --go through all vartices and rotate by that angle so the face normal points in the dir.
	 meshop.setVert theObj v ((meshop.getVert theObj v) * rotateXMatrix theAngle)
redrawViews() --update the viewports
)

Firstly, the vector to orient by needs to be muliplied by -1. But more importantly, if I write a script to alignFaceByVector, extrude that face, and then alignFaceByVector again, it will not work. The second time that I call alignFace, it aligns to the wrong vector.

local theFaceNormal = (getNormal theObj theFaces[1])

should be

local theFaceNormal = (getFaceNormal theObj theFaces[1])

getNormal returns the VERTEX normal, not the face normal. Once you do a first operation, the vertex normal might not be identical to the face normal, and you will get a wrong angle.

As for the direction, you don’t have to know the sign of the angle. Here is why:

Imaging that the original vector is horizontal and the target vector is pointing up along world Z. The angle would be 90 degrees. Calculating the cross product of these two vectors will give you the axis of rotation.
Now if the target vector is pointing straight down along [0,0,-1], you will again get an angle of 90 degrees, but the cross product will return a rotation axis which points in exactly opposite direction of the previous example, causing the angle to rotate in the opposite direction and correctly aligning your normal to -Z and not +Z.
The direction of the cross product result depends on the points order.
If you have the points A, B and C, if you calculate the cross product of the vectors AB and BC, the direction of the cross product will be to the side that sees the points ABC in CCW order. The cross product of the vectors CB and BA will point in exactly the opposite direction. This is why you have to build faces in CCW order when closing gaps in a mesh – the face normal is calculated from the edges in CCW order!

Ah, ok.

Well, I’ve tried to use this knowledge to make a function to rotate an entire object to be oriented with a specific normal. I guess it’s mostly the same as your code but I understand what I’ve written.

Unfotrunately it doesn’t quite work. I’m trying to do a shortcut method rather than going through each vert, since this is going to be in a loop that is executed perhaps 30,000 times!


fn alignObjToNorm obj norm=
(
 --find center of obj to rotate around
 center = 0
 for v = 1 to obj.numverts do
  center += (meshop.getVert obj v)
 center /= obj.numverts
 
 --create new set of axis
 xaxis = normalize( cross norm obj.dir )
 yaxis = normalize( cross xaxis obj.dir )
 zaxis = obj.dir
 
 --gen our coord system
 rotateCoords = matrix3 xaxis yaxis zaxis center
 
 --gen the angle to rotate by, and gen a matrix for it
 angle = acos ( dot obj.dir norm )
 rotateMatrix = rotateXMatrix angle
 
 -- apply rotation
 in coordsys rotateCoords
  obj.rotation = rotateMatrix
)

Page 2 / 3