barycentric coordinates – part 2

david | maya,OpenMaya,python,tutorials | Monday, March 9th, 2015

In barycentric coordinates - part 1 I showed a way to compute the barycentric coordinates for an interior point in a triangle, and described how the result could be used in a simple constraint example.

In part 2 I am going to show a python OpenMaya code example that could be used as the basis for some kind of data interpolation between meshes with differing topology.

I have two meshes: meshSrc is the lores source mesh and meshTgt is the hires target mesh. I will iterate through the verts in meshTgt. For each vert I'll find the closest point on meshSrc. For that point I will compute the barycentric coordinates. I'll return a list of the barycentric coordinates.

I'm using the python OpenMaya api because it is the fastest way to work with dense meshes without resorting to writing a C++ plugin. OpenMaya has efficient classes and methods for working with meshes and extracting data from them. I'll briefly describe the code in sections and then show the full working example.

First I'm getting the MDagPath for meshSrc and meshTgt. This is how they are accessed by the other functions.

selectionList = OpenMaya.MSelectionList()
selectionList.add(meshSr)
meshSrcDagPath = OpenMaya.MDagPath()
selectionList.getDagPath(0, meshSrcDagPath)

Then I create a mesh iterator, MItMeshPolygon, which makes it easier to get the meshSrc data later when we need it.

comp = OpenMaya.MObject()
currentFace = OpenMaya.MItMeshPolygon( meshSrcDagPath, comp )

Dump all the meshTgt verts into a MPointArray

meshTgtMPointArray = OpenMaya.MPointArray()
meshTgtMFnMesh = OpenMaya.MFnMesh(meshTgtDagPath)
meshTgtMFnMesh.getPoints(meshTgtMPointArray, OpenMaya.MSpace.kWorld)

The MMeshIntersector is a powerful class that provides fast access to closest point on meshSrc. I create an instance of the intersector that I will call for each vert in meshTgt.

matrix = meshSrcDagPath.inclusiveMatrix() 
node = meshSrcDagPath.node()
intersector = OpenMaya.MMeshIntersector()
intersector.create( node, matrix )

This next bit is required because OpenMaya functions need the data to be the right "type" and they also need variables created in advance for storage of the results. The docs provide some good examples of this if you search for "MScriptUtil".

pointInfo = OpenMaya.MPointOnMesh()
uUtil = OpenMaya.MScriptUtil(0.0)
uPtr = uUtil.asFloatPtr()
vUtil = OpenMaya.MScriptUtil(0.0)
vPtr = vUtil.asFloatPtr()
pointArray = OpenMaya.MPointArray()
vertIdList = OpenMaya.MIntArray()

Now here's the loop where I'm iterating through the meshTgtMPointArray. For each vert MMeshIntersector returns MPointOnMesh. I use the following methods to get the data I need: getBarycentricCoords, faceIndex, triangleIndex. I need faceIndex because the barycentric coordinates only mean something if I know which verts they are derrived from. I need triangleIndex because the if the face is an n-gon, I need to know which triangle the closest point is on when that n-glon is tesselated.
faceIndex in an input to the mesh iterator. Now the iterator is just referring to that face. Then I can use triangleIndex get the 3 verts surrounding the closest point (ie the verts used to determin the barycentric coords).

for i in range(meshTgtMPointArray.length()):

    intersector.getClosestPoint( meshTgtMPointArray[i], pointInfo )
    pointInfo.getBarycentricCoords(uPtr,vPtr)
    u = uUtil.getFloat(uPtr)
    v = vUtil.getFloat(vPtr)

    faceId = pointInfo.faceIndex()
    triangleId = pointInfo.triangleIndex()

    currentFace.setIndex(faceId, dummyIntPtr)
    currentFace.getTriangle(triangleId, pointArray, vertIdList, OpenMaya.MSpace.kWorld )

    weightData.append(((vertIdList[0], vertIdList[1], vertIdList[2]), (u, v, 1-u-v)))

You may have noticed that pointInfo.getBarycentricCoords() returns the u and v value. I should mention that the 3rd value is simply 1-u-v.
In this code example I'm returning a list where each element in the list consists of two tuples, namely the 3 vert ids and the 3 barycentric coords. In the full code example below you can set the "debug" keyword argument to True and enable data to be printed for each vert. You would definitely not want to do that for a dense mesh, but it might be informative if you try it with say a simple 6 face cube as meshTgt and a low poly sphere as meshSrc.

Full Working Example

import maya.OpenMaya as OpenMaya
import traceback

def barycentricCoords(meshSrc, meshTgt, debug=False):
    '''
    For each vert in the target mesh, find the closest point on the source mesh,
    and compute the barycentric weights at that point.

    Args
        meshSrc
        meshTgt

    Return
        weightData      (list of tuples)
    '''

    weightData = []

    # source mesh
    try:
        selectionList = OpenMaya.MSelectionList()
        selectionList.add(meshSrc)
        meshSrcDagPath = OpenMaya.MDagPath()
        selectionList.getDagPath(0, meshSrcDagPath)
    except:
        print traceback.format_exc()
        return

    # target mesh
    try:
        selectionList = OpenMaya.MSelectionList()
        selectionList.add(meshTgt)
        meshTgtDagPath = OpenMaya.MDagPath()
        selectionList.getDagPath(0, meshTgtDagPath)
    except:
        print traceback.format_exc()
        return

    # create mesh iterator
    comp = OpenMaya.MObject()
    currentFace = OpenMaya.MItMeshPolygon( meshSrcDagPath, comp )

    # get all points from target mesh
    meshTgtMPointArray = OpenMaya.MPointArray()
    meshTgtMFnMesh = OpenMaya.MFnMesh(meshTgtDagPath)
    meshTgtMFnMesh.getPoints(meshTgtMPointArray, OpenMaya.MSpace.kWorld)

    # create mesh intersector
    matrix = meshSrcDagPath.inclusiveMatrix() 
    node = meshSrcDagPath.node()
    intersector = OpenMaya.MMeshIntersector()
    intersector.create( node, matrix )

    # create variables to store the returned data
    pointInfo = OpenMaya.MPointOnMesh()
    uUtil = OpenMaya.MScriptUtil(0.0)
    uPtr = uUtil.asFloatPtr()
    vUtil = OpenMaya.MScriptUtil(0.0)
    vPtr = vUtil.asFloatPtr()
    pointArray = OpenMaya.MPointArray()
    vertIdList = OpenMaya.MIntArray()

    # dummy variable needed in .setIndex()
    dummy = OpenMaya.MScriptUtil()
    dummyIntPtr = dummy.asIntPtr()

    # For each point on the target mesh
    # Find the closest triangle on the source mesh.
    # Get the vertIds and the barycentric coords.
    # 
    for i in range(meshTgtMPointArray.length()):

        intersector.getClosestPoint( meshTgtMPointArray[i], pointInfo )
        pointInfo.getBarycentricCoords(uPtr,vPtr)
        u = uUtil.getFloat(uPtr)
        v = vUtil.getFloat(vPtr)

        faceId = pointInfo.faceIndex()
        triangleId = pointInfo.triangleIndex()

        currentFace.setIndex(faceId, dummyIntPtr)
        currentFace.getTriangle(triangleId, pointArray, vertIdList, OpenMaya.MSpace.kWorld )

        weightData.append(((vertIdList[0], vertIdList[1], vertIdList[2]), (u, v, 1-u-v)))

        if debug:
            print  100*':'
            print 'target point id:', i
            print ''
            print 'target point:', meshTgtMPointArray[i][0], meshTgtMPointArray[i][1], meshTgtMPointArray[i][2]
            closestPoint = pointInfo.getPoint()
            print 'closest pos on source:', closestPoint.x, closestPoint.y, closestPoint.z
            print 'source face id:', faceId
            print 'source triangle id:', triangleId
            print 'source vert ids:', vertIdList
            print 'source point0:', pointArray[0].x, pointArray[0].y, pointArray[0].z
            print 'source point1:', pointArray[1].x, pointArray[1].y, pointArray[1].z
            print 'source point2:', pointArray[2].x, pointArray[2].y, pointArray[2].z
            print ''
            print 'barycentric weights:', u, v, 1-u-v
            print ''

    return weightData

And to run it with a sphere and cube...

data = barycentricCoords('pSphereShape1', 'pCubeShape1', debug=True)

But for a test with dense meshes, remember to use debug=False.

As I said in the introduction, this code could form the basis for some data interpolation between meshes. I am using it in conjunction with my own deformer weights storage format to copy deformer weights between meshes with different topology and it is very fast. Pulling data from a lores mesh with approximately 65k verts and interpolating it onto to a target mesh with over 1 million verts takes around 15 seconds.

No Comments »

No comments yet.

RSS feed for comments on this post.

Leave a comment

You must be logged in to post a comment.

Powered by WordPress | Based on a theme by Roy Tanck