Align pivot based on face normals

Hi, I would like some tips to align the pivot of each object with the average face normals, with Y always up using MEL.
I normally do this manually selecting the object, press D, select any objects face, rotate axis(Y up), and bake pivot.
Its works, but is very time consuming, i have a lot of objects to do this.

What i have for now: But I think that return $unitWorldNormal is wrong.

{

string $selection[] = `ls -selection`;
print $selection;
for($obj in $selection)
{	
	FaceNormal($obj);
}

}

proc vector FaceNormal(string $obj)
{
string $polyInfoList[] = polyInfo -fn $obj;
string $polyInfoItem = $polyInfoList[0];
string $item[];

int $numTokens = `tokenize $polyInfoItem " " $item`;

float $x = ($item[2]);
float $y = ($item[3]);
float $z = ($item[4]);
vector $localNormal = <<$x, $y, $z>>;
print ($localNormal+ "\n");


float $transformMatrix[] = `xform -q -m -ws $obj`;
vector $worldNormal = `pointMatrixMult $localNormal $transformMatrix`;

vector $unitWorldNormal = unit($worldNormal);

return $unitWorldNormal;

}

I’m not entirely sure what it is you are trying to do.

This is how I’m interpreting it:

  1. From an object you’re looking to get the average face normal
  2. Then align the pivot (Y-axis) with this average face normal.

Let’s first get the average face normal. I’m more fluent in Python so I’ll do that. However, I’ll write it without using API functions so it translates somewhat well to MEL.

from maya import cmds


def get_average_face_normal(node):
    """Return average face normal of geometry in object-space.
    
    Note that the function might return [0, 0, 0]
    for e.g. a cube since all normals point in
    exactly opposite directions.
    
    Args:
        node (str): Node name of mesh.
        
    Returns:
        list: vector of average normal
    
    """
    
    data = cmds.polyInfo(node, faceNormals=True)
    normals = []
    for entry in data:
        # entry = 'FACE_NORMAL      0: 0.000000 0.000000 1.000000\n'
        values = entry.split(": ")[-1].strip()
        # values = 0.000000 0.000000 1.000000
        normal = [float(x) for x in values.split(" ")]
        # normal = [0.0, 0.0, 1.0]
        normals.append(normal)

    # To get the average normal we'll just sum all elements
    # and then divide by the amount of normals
    result = [0, 0, 0]
    for normal in normals:
        
        for i in range(3):
            result[i] += normal[i]
    
    for i in range(3):
        result[i] /= float(len(normals))
        
    return result   
    
    
for node in cmds.ls(sl=1):
    average = get_average_face_normal(node)

Then now let’s try and align the pivot with that average normal. This is where I’m not sure what you’re trying to do. Are you trying to rotate the geometry in such a way that the average normal points to the Y-axis. Or are you trying to set a transform tool to a Custom pivot. I don’t believe objects actually have ‘rotation’ for a pivot in maya.

Anyway, here’s an example using Maya’s Python API to use some vector/matrix math from it:

from maya import cmds
import maya.api.OpenMaya as om
import math

for node in cmds.ls(sl=1):
    average = get_average_face_normal(node)
    normal = om.MVector(*average)
    
    if normal.length() == 0:
        raise RuntimeError("Average normal is of zero length, rotation can't be defined.")
    
    # Make the average world-space
    xform = cmds.xform(node, query=True, worldSpace=True, matrix=True)
    matrix = om.MMatrix(xform)
    normal *= matrix
    
    # Rotate to have the normal aim to Y-axis
    rotation = normal.rotateTo(om.MVector.kYaxisVector)   # maya.api.OpenMaya.MQuaternion
    euler = rotation.asEulerRotation().asVector()  # maya.api.OpenMaya.MVector
    # Euler angles are in radians, we need to convert to degrees
    euler = [x * (180/math.pi) for x in euler]
    cmds.xform(node, relative=True, worldSpace=True, rotation=list(euler))

This second snippet uses the code/function from the first snippet.

This will rotate the geometry so that the average normal matches the Y-axis. Note that it won’t work for a cube due to the average normal being zero length [0, 0, 0].

Sorry, due to the formatting in your post I glimpsed over to quickly but I noticed you were already close to getting the normals (even in world-space).

Here’s a MEL version similar to yours to get the average normal (sum of all face normals and then normalized in this case). And then a hack without python api to aim your geo so that the average normal is pointing at the Y-axis by abusing an aimConstraint and then deleting it directly.

proc vector parsePolyInfoVector(string $item)
{
    string $tokens[];
    tokenize $item " " $tokens;
    float $x = $tokens[2];
    float $y = $tokens[3];
    float $z = $tokens[4];
    vector $v = <<$x, $y, $z>>;
    return $v;
}

proc vector AverageFaceNormal(string $node) {
    /*Return average face normal of geometry in object-space.
    
    Note that the function might return [0, 0, 0]
    for e.g. a cube since all normals point in
    exactly opposite directions.
    
    Args:
        node (str): Node name of mesh.
        
    Returns:
        vector: average unit normal
    
    */
    
    string $polyInfoList[] = `polyInfo -fn $node`;
    
    vector $average = <<0, 0, 0>>;
    for($item in $polyInfoList)
    {
        vector $normal = parsePolyInfoVector($item);
        $average += $normal;
    }  
    
    // Normalize vector  
    vector $average = unit($average);
    
    return $average;
 
}

// Let's aim selected nodes to Y-axis by average face normal
{
    string $selection[] = `ls -sl`;
    for ($node in $selection) {
        vector $n = `AverageFaceNormal($node)`;
        
        // Instead of doing the math ourselves let's abuse an 
        // aimConstraint to orient our node the way we want.
        // Align target so it's vertically above the source node
        // so we get the aim to go up straight note that we set
        // the object-space average face normal vector as the
        // aim vector that should be pointing upwards.
        string $target = `createNode "transform"`;
        float $pos[] = `xform -q -ws -rp $node`;
        xform -worldSpace -translation $pos[0] ($pos[1]+100) $pos[2] $target;
        string $aim[] = `aimConstraint -aim ($n.x) ($n.y) ($n.z) -upVector 0 0 1 -worldUpVector 0 0 1 -worldUpType "vector" $target $node`;
        
        delete $aim;
        delete $target;
    }
    select -noExpand $selection; // Reselect original selection
}

Man. My MEL is rusty. :blush:

Hi @BigRoyNL, sorry for my fist comment in this topic, the formatting.

I will try yours solution! Thanks for help! I got the idea!