So I’ve been testing/prototyping some more in Python.
Currently I’m averaging the face-vertex-tangents so I end with a single tangent (and binormal) for a vertex, but this isn’t a bulletproof solution.
For example the top and bottom vertex on a sphere will not work correctly. Also L-shape corners will give problems.
Here’s my last snippet:
"""
Snippet testing Vertex Tangent Space in Maya (Python API)
@author: Roy Nieterau
@website: www.colorbleed.nl
@email: roy@colorbleed.nl
==================================
Averaging the Face Tangents
==================================
Currently I am getting the vertex-tangents by averaging
the vertex-face-tangents per vertex.
Note:
That this is not a good solution working with L-shapes:
- i.e. the 90 degree corners of a Cube
Note2:
Without the normalization the pinching on a sphere (top/bottom) can
result in a matrix with 0, 0, 0 scale because the normals of the
different faces summed up together will become zero (thus zero average)
"""
import maya.OpenMaya as om
import maya.cmds as mc
def getPosNormalTangents(mesh, normalize=True):
""" Prototype/Snippet to get vertex tangents from a mesh """
selList = om.MSelectionList()
selList.add(mesh)
path = om.MDagPath()
selList.getDagPath(0, path)
path.extendToShape()
fnMesh = om.MFnMesh(path)
tangents = om.MFloatVectorArray()
binormals = om.MFloatVectorArray()
fnMesh.getTangents(tangents)
fnMesh.getBinormals(binormals)
itMeshVertex = om.MItMeshVertex(path)
vertBinormals = om.MFloatVectorArray()
vertBinormals.setLength(fnMesh.numVertices())
vertTangents = om.MFloatVectorArray()
vertTangents.setLength(fnMesh.numVertices())
vertNormals = om.MFloatVectorArray()
vertNormals.setLength(fnMesh.numVertices())
vertId = 0
connectedFaceIds = om.MIntArray()
# For each vertex get the connected faces
# For each of those faces get the 'tangentId' to get the tangent and binormal stored above
# Use that to calculate the normal
while(not itMeshVertex.isDone()):
itMeshVertex.getConnectedFaces(connectedFaceIds)
tangent = om.MFloatVector()
binormal = om.MFloatVector()
for x in xrange(connectedFaceIds.length()):
faceId = connectedFaceIds[x]
tangentId = fnMesh.getTangentId(faceId, vertId)
binormal += binormals[tangentId]
tangent += tangents[tangentId]
binormal /= connectedFaceIds.length()
tangent /= connectedFaceIds.length()
if normalize:
binormal.normalize()
tangent.normalize()
normal = tangent ^ binormal
if normalize:
normal.normalize()
# Put the data in the vertArrays
vertTangents.set(tangent, vertId)
vertBinormals.set(binormal, vertId)
vertNormals.set(normal, vertId)
vertId += 1
itMeshVertex.next()
vertPoints = om.MPointArray()
fnMesh.getPoints(vertPoints)
return vertBinormals, vertTangents, vertNormals, vertPoints
def vectorsToMatrix(binormal=(1, 0, 0), tangent=(0, 1, 0), normal=(0, 0, 1), pos=(0, 0, 0), asApi=False):
""" Function to convert an orthogonal basis defined from seperate vectors + position to a matrix """
def _parseAPI(vec):
if isinstance(vec, (om.MVector, om.MFloatVector, om.MPoint, om.MFloatPoint)):
vec = [vec(x) for x in xrange(3)]
return vec
binormal = _parseAPI(binormal)
tangent = _parseAPI(tangent)
normal = _parseAPI(normal)
pos = _parseAPI(pos)
if asApi:
matrix = om.MMatrix()
for x in xrange(3):
om.MScriptUtil.setDoubleArray(matrix[0], x, binormal[x])
om.MScriptUtil.setDoubleArray(matrix[1], x, tangent[x])
om.MScriptUtil.setDoubleArray(matrix[2], x, normal[x])
om.MScriptUtil.setDoubleArray(matrix[3], x, pos[x])
return matrix
else:
return [binormal[0], binormal[1], binormal[2], 0,
tangent[0], tangent[1], tangent[2], 0,
normal[0], normal[1], normal[2], 0,
pos[0], pos[1], pos[2], 1]
#
# Test it on two objects (select two objects)
# 1. Used for calculating the tangent spaces
# 2. Duplicated to every vertex with the matrix of the tangent space
#
if __name__ == "__main__":
sel = mc.ls(sl=1)
if not len(sel) > 1:
raise RuntimeError("Select two objects. The second object will be duplicated and moved to every vertex of the first object.")
vertBinormals, vertTangents, vertNormals, vertPoints = getPosNormalTangents(sel[0])
for x in xrange(vertPoints.length()):
dup = mc.duplicate(sel[1])
mat = vectorsToMatrix(vertBinormals[x], vertTangents[x], vertNormals[x], vertPoints[x])
mc.xform(dup, m=mat)
Hope somebody can point me in the right direction!
Maybe a solution is to weight the face-vertex-tangents based on the UV triangle size, I think that’s what they mention as a solution here:
http://www.crytek.com/download/Triangle_mesh_tangent_space_calculation.pdf
EDIT:
Ok. So I implemented the functionality to average by UV area size. It does give other results, but it’s hard to check whether this is ‘perfect’.
It’s hard to change the mesh and update it directly with this script, so i’ll probably have to implement this in the C++ node to see if it works consistently.
Sharing the code here:
"""
Snippet testing Vertex Tangent Space in Maya (Python API)
@author: Roy Nieterau
@website: www.colorbleed.nl
@email: roy@colorbleed.nl
==================================
Averaging the Face Tangents
==================================
Currently I am getting the vertex-tangents by averaging
the vertex-face-tangents per vertex.
Note:
That this is not a good solution working with L-shapes:
- i.e. the 90 degree corners of a Cube
Note2:
Without the normalization the pinching on a sphere (top/bottom) can
result in a matrix with 0, 0, 0 scale because the normals of the
different faces summed up together will become zero (thus zero average)
Release Notes:
- Added weightByUVTriangleSize parameter to weight averaging by UV area size.
"""
import maya.OpenMaya as om
import maya.cmds as mc
def getPosNormalTangents(mesh, normalize=True, weightByUVTriangleSize=True):
""" Prototype/Snippet to get vertex tangents from a mesh """
selList = om.MSelectionList()
selList.add(mesh)
path = om.MDagPath()
selList.getDagPath(0, path)
path.extendToShape()
fnMesh = om.MFnMesh(path)
tangents = om.MFloatVectorArray()
binormals = om.MFloatVectorArray()
fnMesh.getTangents(tangents)
fnMesh.getBinormals(binormals)
itMeshVertex = om.MItMeshVertex(path)
vertBinormals = om.MFloatVectorArray()
vertBinormals.setLength(fnMesh.numVertices())
vertTangents = om.MFloatVectorArray()
vertTangents.setLength(fnMesh.numVertices())
vertNormals = om.MFloatVectorArray()
vertNormals.setLength(fnMesh.numVertices())
vertId = 0
connectedFaceIds = om.MIntArray()
intScriptUtil = om.MScriptUtil()
intPtr = intScriptUtil.asIntPtr()
doubleScriptUtil = om.MScriptUtil()
doublePtr = doubleScriptUtil.asDoublePtr()
if weightByUVTriangleSize:
itMeshPoly = om.MItMeshPolygon(path)
# For each vertex get the connected faces
# For each of those faces get the 'tangentId' to get the tangent and binormal stored above
# Use that to calculate the normal
while(not itMeshVertex.isDone()):
itMeshVertex.getConnectedFaces(connectedFaceIds)
tangent = om.MFloatVector()
binormal = om.MFloatVector()
if weightByUVTriangleSize:
# We will multiply the vectors with the UVTriangleSize
# Then to get the weighted average we divide by the sum of the UVTriangleSizes
divisor = 0
else:
divisor = connectedFaceIds.length()
for x in xrange(connectedFaceIds.length()):
faceId = connectedFaceIds[x]
tangentId = fnMesh.getTangentId(faceId, vertId)
if weightByUVTriangleSize:
itMeshPoly.setIndex(faceId, intPtr)
itMeshPoly.getUVArea(doublePtr)
area = om.MScriptUtil.getDouble(doublePtr)
binormal += binormals[tangentId] * area
tangent += tangents[tangentId] * area
divisor += area
else:
binormal += binormals[tangentId]
tangent += tangents[tangentId]
binormal /= divisor
tangent /= divisor
if normalize:
binormal.normalize()
tangent.normalize()
normal = tangent ^ binormal
if normalize:
normal.normalize()
# Put the data in the vertArrays
vertTangents.set(tangent, vertId)
vertBinormals.set(binormal, vertId)
vertNormals.set(normal, vertId)
vertId += 1
itMeshVertex.next()
vertPoints = om.MPointArray()
fnMesh.getPoints(vertPoints)
return vertBinormals, vertTangents, vertNormals, vertPoints
def vectorsToMatrix(binormal=(1, 0, 0), tangent=(0, 1, 0), normal=(0, 0, 1), pos=(0, 0, 0), asApi=False):
""" Function to convert an orthogonal basis defined from seperate vectors + position to a matrix """
def _parseAPI(vec):
if isinstance(vec, (om.MVector, om.MFloatVector, om.MPoint, om.MFloatPoint)):
vec = [vec(x) for x in xrange(3)]
return vec
binormal = _parseAPI(binormal)
tangent = _parseAPI(tangent)
normal = _parseAPI(normal)
pos = _parseAPI(pos)
if asApi:
matrix = om.MMatrix()
for x in xrange(3):
om.MScriptUtil.setDoubleArray(matrix[0], x, binormal[x])
om.MScriptUtil.setDoubleArray(matrix[1], x, tangent[x])
om.MScriptUtil.setDoubleArray(matrix[2], x, normal[x])
om.MScriptUtil.setDoubleArray(matrix[3], x, pos[x])
return matrix
else:
return [binormal[0], binormal[1], binormal[2], 0,
tangent[0], tangent[1], tangent[2], 0,
normal[0], normal[1], normal[2], 0,
pos[0], pos[1], pos[2], 1]
#
# Test it on two objects (select two objects)
# 1. Used for calculating the tangent spaces
# 2. Duplicated to every vertex with the matrix of the tangent space
#
if __name__ == "__main__":
sel = mc.ls(sl=1)
if not len(sel) > 1:
raise RuntimeError("Select two objects. The second object will be duplicated and moved to every vertex of the first object.")
vertBinormals, vertTangents, vertNormals, vertPoints = getPosNormalTangents(sel[0])
for x in xrange(vertPoints.length()):
dup = mc.duplicate(sel[1])
mat = vectorsToMatrix(vertBinormals[x], vertTangents[x], vertNormals[x], vertPoints[x])
mc.xform(dup, m=mat)
Still I’m very interested in hearing ideas from others! So pointers are welcome.