Thanks for questioning my technique here… that really opens up this to be a discussion instead of me just trying to point you towards a direction you might not need.
Actually looking at the angleBetween command it can return eulerAngles, so yes it should be all you need to define euler orientations.
In theory one should be able to define a relative rotation so that one vector ends up on top of the other.
For example:
import pymel.core as pm
# Testing
sphere = pm.polySphere()[0]
for face in sphere.faces:
curve = pm.circle(normal=(0, 1, 0), radius=0.1)
curve = pm.ls(sl=True)[0]
# Get face center
pm.select(face, r=1)
faceBox = pm.polyEvaluate(boundingBoxComponent=True)
centerX = 0.5 * (faceBox[0][0] + faceBox[0][1])
centerY = 0.5 * (faceBox[1][0] + faceBox[1][1])
centerZ = 0.5 * (faceBox[2][0] + faceBox[2][1])
center = pm.datatypes.Vector(centerX, centerY, centerZ)
aim = face.getNormal() # Normal
origin = pm.datatypes.Vector([0.0, 1.0, 0.0]) # Y+
rot = pm.angleBetween(v1=origin , v2=aim, euler=True, ch=False)
pm.xform(curve, absolute=True, t=center)
pm.xform(curve, absolute=True, ro=rot)
But notice that it’s only orienting along a single axis and doesn’t define the secondary axis since you’re only taking into account the difference between two vectors (with angleBetween).
For example see what happens when you’re positioning cubes on the sphere:
import pymel.core as pm
def nodeToFace(node, face):
# Get face center
pm.select(face, r=1)
faceBox = pm.polyEvaluate(boundingBoxComponent=True)
centerX = 0.5 * (faceBox[0][0] + faceBox[0][1])
centerY = 0.5 * (faceBox[1][0] + faceBox[1][1])
centerZ = 0.5 * (faceBox[2][0] + faceBox[2][1])
center = pm.datatypes.Vector(centerX, centerY, centerZ)
aim = face.getNormal() # Normal
origin = pm.datatypes.Vector([0.0, 1.0, 0.0]) # Y+
rot = pm.angleBetween(v1=origin, v2=aim, euler=True, ch=False)
pm.xform(node, absolute=True, t=center)
pm.xform(node, absolute=True, ro=rot)
# Testing 1
sphere = pm.polySphere()[0]
for face in sphere.faces:
node = pm.polyCube(w=0.05, h=0.05, d=0.05)
nodeToFace(node, face)
# Testing 2
sphere = pm.polySphere()[0]
pm.move(sphere, 2, 0, 0)
for face in sphere.faces:
node = pm.circle(normal=(0, 1, 0), radius=0.1)
nodeToFace(node, face)
With an additional angleBetween you should be able to orient the secondary axis to the tangent of the face.
Or as stated before the aimConstraint should help you in a single step since you can directly define aim and up axis (plus it works with offsetted pivot points and/or different rotation orders).
Does this help you along?
Best,
Roy
EDIT:
Did some additional testing. Last one might be closest to what you’re looking for. I also tried orienting the second axis with angleBetween, but wasn’t sure about the math its doing behind the scenes so I couldn’t get that one right. I’m adding my try here anyway (first code block).
Note that these code snippets are a bit slow to run as they tend to do a lot looping and calculations with PyMel. Using new python api 2.0 you should be able to get this near instant (especially for these resolutions of meshes should be real-time!)
#1
Testing secondary axis orient with angleBetween (INCORRECT, sharing code anyway)
import pymel.core as pm
def nodeToFace(node, face):
# Get face center
pm.select(face, r=1)
faceBox = pm.polyEvaluate(boundingBoxComponent=True)
centerX = 0.5 * (faceBox[0][0] + faceBox[0][1])
centerY = 0.5 * (faceBox[1][0] + faceBox[1][1])
centerZ = 0.5 * (faceBox[2][0] + faceBox[2][1])
center = pm.datatypes.Vector(centerX, centerY, centerZ)
aim = face.getNormal() # Normal
origin = pm.datatypes.Vector([0.0, 1.0, 0.0]) # Y+
rot = pm.angleBetween(v1=origin, v2=aim, euler=True, ch=False)
# Nastiest code ever to get tangent in PyMel :O
# SO sorry, I'm normally not much of a PyMel user. (Python API 2.0 FTW!)
tangentId = face.__apimfn__().tangentIndex(0)
fnMesh = pm.PyNode(face.node()).__apimfn__()
tangents = maya.OpenMaya.MFloatVectorArray()
fnMesh.getTangents(tangents)
tangent = pm.datatypes.Vector(tangents[tangentId])
pm.xform(node, absolute=True, t=center)
pm.xform(node, absolute=True, ro=rot)
mat = pm.xform(node, q=1, ws=1, matrix=1)
localTangent = tangent * mat.inverse()
origin = pm.datatypes.Vector([0.0, 0.0, 1.0]) # Z+
rot = pm.angleBetween(v1=origin, v2=localTangent, euler=True, ch=False)
pm.xform(node, relative=True, objectSpace=True, ro=rot)
# Testing 1
sphere = pm.polySphere()[0]
for face in sphere.faces:
node = pm.polyCube(w=0.05, h=0.05, d=0.05)
nodeToFace(node, face)
# Testing 2
sphere = pm.polySphere()[0]
pm.move(sphere, 2, 0, 0)
for face in sphere.faces:
node = pm.circle(normal=(0, 1, 0), radius=0.1)
nodeToFace(node, face)
#2
Orienting by normal and tangent with the matrix orientation. Same thing should be easily doable with aimConstraint (using same normal and tangent data)
If you’re going with PyMel the aimConstraint way is likely faster since it reduces the amount of initializing of Matrix datatype you need to do.
import pymel.core as pm
import pymel.core as pm
def orientationMatrix(aim, up, center=None):
"""
Calculate a PyMel Matrix based on aim axis, up axis and potentially
a center point (for translation offset).
Using Gram schmidt process for orthonormalization
"""
if center is None:
center = pm.datatypes.Vector()
# Create vectors and calc rotation
# We don't do any prenormalization as we assume aim and up axis are normalized
# So assuming for now that: getNormal() and the upVector we provided is normalized.
tangent = (aim ^ up).normal()
up = (tangent ^ aim).normal()
# Set up the matrix
mat = pm.datatypes.Matrix([aim.x, aim.y, aim.z, 0,
up.x, up.y, up.z, 0,
tangent.x, tangent.y, tangent.z, 0,
center.x, center.y, center.z, 1])
return mat
def nodeToFace(node, face):
# Get face center
pm.select(face, r=1)
faceBox = pm.polyEvaluate(boundingBoxComponent=True)
centerX = 0.5 * (faceBox[0][0] + faceBox[0][1])
centerY = 0.5 * (faceBox[1][0] + faceBox[1][1])
centerZ = 0.5 * (faceBox[2][0] + faceBox[2][1])
center = pm.datatypes.Vector(centerX, centerY, centerZ)
aim = face.getNormal() # Normal
#origin = pm.datatypes.Vector([0.0, 1.0, 0.0]) # Y+
#rot = pm.angleBetween(v1=origin, v2=aim, euler=True, ch=False)
# Nastiest code ever to get tangent in PyMel :O
# SO sorry, I'm normally not much of a PyMel user. (Python API 2.0 FTW!)
# Get all tangents
fnMesh = pm.PyNode(face.node()).__apimfn__()
tangents = maya.OpenMaya.MFloatVectorArray()
fnMesh.getTangents(tangents)
# Get average tangent for this face (not that this does NOT provide a consistent tangent, eg. on cube?!)
itMeshPolygon = face.__apimfn__()
vtxCount = itMeshPolygon.polygonVertexCount()
tangent = pm.datatypes.Vector()
for i in range(vtxCount):
tangentId = itMeshPolygon.tangentIndex(i)
tangent += tangents[tangentId]
tangent.normalize()
mat = orientationMatrix(aim, tangent, center)
pm.xform(node, absolute=True, matrix=mat)
# Testing 1
to_object = pm.polyCube(sx=5, sy=5, sz=5)[0]
for face in to_object.faces:
node = pm.polyCube(w=0.05, h=0.05, d=0.05)
nodeToFace(node, face)
# Testing 2
to_object = pm.polySphere()[0]
pm.move(to_object, 2, 0, 0)
for face in to_object.faces:
node = pm.polyCube(w=0.05, h=0.05, d=0.05)
nodeToFace(node, face)
# Testing 3
to_object = pm.polyCube()[0]
pm.move(to_object, 4, 0, 0)
for face in to_object.faces:
node = pm.polyCube(w=0.05, h=0.05, d=0.05)
nodeToFace(node, face)