前言

  最近我看到TD群里有人问关于 Maya 法线获取的归纳汇总,恰好我前段时间就因为工作的需要有做过相关的开发。
  具体可以参照我之前写的文章 Maya 法线解锁 Maya 法线解锁进阶
  因此在这里总一下法线获取的归纳,希望对大家有有所帮助。

什么是法线

  首先需要理解三维软件法线的概念,具体还是可以参照我之前翻译的教程,

  看完教程可以知道,法线是用来描述光线射碰撞到模型上之后光线的反射行为。(不是搞图形渲染的,如有不当之处请多多包涵)

alt

  通过上面的截图,调整点法线的角度就可以改变模型的着色效果,因此法线贴图的原理也在这里。
  通过高模烘焙的法线信息,在低模上扭曲光线让渲染的时候模拟出高模的细节。因此法线贴图比较省资源,但是效果的精细程度比不上置换贴图。

Maya中的法线

  在Maya中,模型上有面法线和顶点法线,面法线是垂直于面的,但是实际对着色起作用的是点法线。
  我们可以在 Maya 中简单构建出两个球来说明法线对着色的影响。
  首先将其中一个球执行硬边操作。

alt

  从上图可以看到默认的球是软边的,两个一对比就很明显了。
  硬边的球看起来的面片非常明显,我们可以开启模型的点法线来进行对比。

alt
alt

  可以看到软化边的顶点法线都是合并到一起的,硬化边的法线则根据面的朝向相互独立了。
  这就造成了平滑着色之间的区别。

  通过上面的硬边例子可以知道,Maya的法线信息是根据面顶点进行存储的,这里的面顶点区别于模型的顶点。
  每个面上的顶点是单独计算的,进入 Maya 的 VertexFace 模式就非常清楚

alt
alt

  这个模式下的面都是间隔开的,可以清晰看到每个面顶点。

使用 Mel & cmds 命令获取模型法线

  常用的命令有如下这些

  其中最强大的命令是 polyNormalPerVertex 可以精确获取到法线的信息,并且精准调整法线。

  另外需要额外注意的是,调整法线如果选择顶点而不是面顶点的话,那么修改的时候会将顶点相邻的面顶点一并修改。
  就无法实现法线的精准调节了。

alt

使用 pymel & OpenMaya 获取法线

  pymel 对 OpenMaya 进行了封装,内核相通,使用起来简单很多,因此这里重点讲解 pymel 的获取。
  除了 mel 提供的命令之外,获取法线的方法还有有三种方式, 模型 顶点

模型获取 Mesh

  通过 pymel 获取一个模型, pymel 的 Mesh 类提供了大量和法线相关的命令(大部分都是调用 OpenMaya 实现的)

1
2
3
4
5
6
7
8

import pymel.core as pm
import pymel.core.nodetypes as nt
# NOTE 打印 pyeml 的 Mesh 类函数
print (dir(nt.Mesh))

['BoolOperation', 'MAttrClass', 'MColorRepresentation', 'MObjectColorType', 'MPublishNodeType', 'MdgTimerMetric', 'MdgTimerState', 'MdgTimerType', 'SplitPlacement', '__add__', '__apicls__', '__apihandle__', '__apimdagpath__', '__apimfn__', '__apimobject__', '__apiobject__', '__apiobjects__', '__class__', '__delattr__', '__dict__', '__doc__', '__eq__', '__format__', '__ge__', '__getattr__', '__getattribute__', '__getitem__', '__getnewargs__', '__gt__', '__hash__', '__init__', '__le__', '__lt__', '__melnode__', '__melobject__', '__metaclass__', '__module__', '__ne__', '__new__', '__nonzero__', '__or__', '__radd__', '__readonly__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__slots__', '__str__', '__subclasshook__', '__unicode__', '__weakref__', '_attr', '_componentAttributes', '_formatter_field_name_split', '_formatter_parser', '_getAssociatedColorSetInstances', '_getAssociatedUVSetInstances', '_getDagParent', '_getUVAtPoint', '_name', '_node', '_numCVsFunc_generator', '_numColorSets', '_numColors', '_numEPsFunc_generator', '_numEdges', '_numFaceVertices', '_numNormals', '_numPolygons', '_numUVSets', '_numUVs', '_numVertices', '_updateName', 'activeColor', 'addAttr', 'addChild', 'addHoles', 'addPrefix', 'area', 'assignColor', 'assignColors', 'assignUV', 'assignUVs', 'attr', 'attrDefaults', 'attrInfo', 'attributeCount', 'booleanOps', 'boundingBox', 'canBeWritten', 'cast', 'center', 'child', 'childAtIndex', 'childCount', 'classification', 'cleanupEdgeSmoothing', 'clearColors', 'clearUVs', 'comp', 'componentTypeFromName', 'componentTypeName', 'connectAttr', 'connections', 'count', 'createColorSet', 'createUVSet', 'dagPath', 'dagRoot', 'deleteAttr', 'deleteColorSet', 'deletePreset', 'deleteUVSet', 'deselect', 'destinations', 'disconnectAttr', 'dormantColor', 'drawOverrideColor', 'drawOverrideEnabled', 'drawOverrideIsReference', 'drawOverrideIsTemplate', 'duplicate', 'enableDGTiming', 'endswith', 'exists', 'extractNum', 'find', 'firstParent', 'firstParent2', 'format', 'fullPath', 'fullPathName', 'future', 'getAllParents', 'getAllPaths', 'getAssignedUVs', 'getAssociatedUVSetTextures', 'getAttr', 'getAxisAtPoint', 'getBinormals', 'getCheckSamePointTwice', 'getChildren', 'getClosestNormal', 'getClosestPoint', 'getClosestPointAndNormal', 'getColor', 'getColorRepresentation', 'getColorSetFamilyNames', 'getColorSetNames', 'getColors', 'getConnectedSetsAndMembers', 'getCurrentColorSetName', 'getCurrentUVSetName', 'getDisplayColors', 'getEdgeVertices', 'getFaceNormalIds', 'getFaceUVSetNames', 'getFaceVertexBinormal', 'getFaceVertexBinormals', 'getFaceVertexColorIndex', 'getFaceVertexColors', 'getFaceVertexNormal', 'getFaceVertexTangent', 'getFaceVertexTangents', 'getHoles', 'getIcon', 'getInstances', 'getMembers', 'getNormalIds', 'getNormals', 'getObjectColor', 'getObjectColorType', 'getOtherInstances', 'getParent', 'getParentContainer', 'getPath', 'getPoint', 'getPointAtUV', 'getPoints', 'getPolygonNormal', 'getPolygonTriangleVertices', 'getPolygonUV', 'getPolygonUVid', 'getPolygonVertices', 'getPublishedNames', 'getPublishedNodes', 'getPublishedPlugs', 'getRootTransform', 'getSiblings', 'getSubcontainers', 'getTangentId', 'getTangents', 'getTransform', 'getTriangleOffsets', 'getTriangles', 'getUV', 'getUVAtPoint', 'getUVSetFamilyNames', 'getUVSetNames', 'getUVSetsInFamily', 'getUVs', 'getUvShellsIds', 'getVertexNormal', 'getVertices', 'hasAlphaChannels', 'hasAttr', 'hasChild', 'hasColorChannels', 'hasParent', 'hide', 'hiliteColor', 'history', 'inModel', 'inUnderWorld', 'index', 'inputs', 'instanceCount', 'instanceNumber', 'intersect', 'isChildOf', 'isColorClamped', 'isColorSetPerInstance', 'isDefaultNode', 'isDisplaced', 'isEdgeSmooth', 'isFlagSet', 'isInstanceOf', 'isInstanceable', 'isInstanced', 'isInstancedAttribute', 'isIntermediate', 'isIntermediateObject', 'isLocked', 'isNormalLocked', 'isParentOf', 'isPolygonConvex', 'isReadOnly', 'isReferenced', 'isRightHandedTangent', 'isShared', 'isTrackingEdits', 'isUVSetPerInstance', 'isUniquelyNamed', 'isUsingObjectColor', 'isVisible', 'isdecimal', 'islower', 'isnumeric', 'isupper', 'join', 'listAliases', 'listAnimatable', 'listAttr', 'listComp', 'listConnections', 'listFuture', 'listHistory', 'listPresets', 'listRelatives', 'listSets', 'ljust', 'loadPreset', 'lock', 'lockFaceVertexNormals', 'lockVertexNormals', 'longName', 'lower', 'lstrip', 'makeLive', 'model', 'name', 'namespace', 'namespaceList', 'nextName', 'nextUniqueName', 'node', 'nodeName', 'nodeType', 'numChildren', 'numColorSets', 'numColors', 'numEdges', 'numFaceVertices', 'numFaces', 'numNormals', 'numPolygonVertices', 'numSelectedEdges', 'numSelectedFaces', 'numSelectedTriangles', 'numSelectedVertices', 'numTriangles', 'numUVSets', 'numUVs', 'numVertices', 'objExists', 'objectColorIndex', 'objectColorRGB', 'onBoundary', 'outputs', 'parent', 'parentAtIndex', 'parentCount', 'parentNamespace', 'partialPathName', 'partition', 'pluginName', 'polyTriangulate', 'prevName', 'referenceFile', 'registerVirtualSubClass', 'removeChild', 'removeChildAt', 'removeFaceColors', 'removeFaceVertexColors', 'removeVertexColors', 'rename', 'renameUVSet', 'replace', 'rfind', 'rindex', 'rjust', 'root', 'rpartition', 'rsplit', 'rstrip', 'savePreset', 'select', 'setAttr', 'setCheckSamePointTwice', 'setColor', 'setColorClamped', 'setColors', 'setCurrentColorSetName', 'setCurrentUVSetName', 'setDisplayColors', 'setDoNotWrite', 'setDynamicAttr', 'setEdgeSmoothing', 'setFaceColor', 'setFaceColors', 'setFaceVertexColor', 'setFaceVertexNormal', 'setIcon', 'setInstanceable', 'setIntermediate', 'setIntermediateObject', 'setLocked', 'setNormals', 'setObject', 'setObjectColor', 'setObjectColorType', 'setParent', 'setPoint', 'setPoints', 'setSomeColors', 'setSomeUVs', 'setUV', 'setUVs', 'setUseObjectColor', 'setVertexColor', 'setVertexNormal', 'shadingGroups', 'shortName', 'show', 'sources', 'split', 'startswith', 'strip', 'stripNamespace', 'stripNum', 'swapNamespace', 'syncObject', 'transformationMatrix', 'type', 'unlock', 'unlockFaceVertexNormals', 'unlockVertexNormals', 'updateSurface', 'upper', 'usingHiliteColor', 'usingObjectColor', 'worldArea']

  通过 python dir 可以打印出 pymel 封装好的函数。
  其中和 normal 相关的操作如下 | 使用方式参考官方文档

pymel Mesh 文档

面获取 MeshFace

  可以用上面的方法同理获取到内置的 normal 函数

pymel MeshFace 文档

顶点获取 MeshVertex

pymel MeshVertex 文档

注意事项

  MeshFace getNormal 获取面法线
  MeshVertex getNormal 获取顶点法线 也就是该顶点下相邻面顶点的法线平均值

  MeshFace getNormals 和 MeshVertex getNormals 可以获取到相邻面顶点的法线信息,但是返回的是数组,不知道这些信息对应的面顶点关系

  我在 Maya 2017 使用 Mesh getFaceVertexNormal 会导致 Maya 崩溃,还是用 mel 命令比较稳定

获取法线数据

OpenMaya 2.0 获取

  我之前是通过 OpenMaya 2.0 遍历模型上的面顶点来构建一个面顶点法线字典来提取法线的信息数据。
  缺点是数据不是动态的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from maya.api import OpenMaya as om
from maya import cmds
import sys
sel_list = cmds.ls(cmds.pickWalk(d="down"),type="mesh")
sel = sel_list[0] if sel_list else sys.exit(0)

sel_list = om.MSelectionList()
sel_list.add(sel)
dagPath = sel_list.getDagPath(0)

# NOTE 获取 mesh 所有的法线信息
mesh_normal = {}
itr = om.MItMeshFaceVertex(dagPath)
while not itr.isDone():
face_id = itr.faceId()
vert_id = itr.vertexId()
normal = itr.getNormal()

mesh_normal.setdefault(vert_id,{})
mesh_normal[vert_id][face_id] = normal
itr.next()

print(mesh_normal)

PyMel 获取

  OpenMaya 用起来比较麻烦,用 pymel 获取就优雅很多。(效率会低些)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import sys
import pymel.core as pm
from collections import defaultdict

sel_list = pm.ls(pm.pickWalk(d="down"),type="mesh")
sel = sel_list[0] if sel_list else sys.exit(0)

mesh_normal = defaultdict(dict)
for vtxFace in sel.vtxFace:
# NOTE currentItemIndex 获取到 ComponentIndex | 查 pymel 源码可知是 tuple 类型
vert_id,face_id = vtxFace.currentItemIndex()
mesh_normal[vert_id][face_id] = pm.polyNormalPerVertex(vtxFace,q=1,normalXYZ=1)

print(mesh_normal)

动态获取法线

  如果要动态获取模型的法线信息,可以通过构建类的方式来获取。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import pymel.core as pm
import pymel.core.nodetypes as nt

class NormalData(object):
def __init__(self,mesh):
mesh = mesh[0] if hasattr(mesh,"__iter__") else mesh
mesh = mesh.node() if hasattr(mesh,"node") else mesh
self.mesh = mesh.getShape() if hasattr(mesh,"getShape") else mesh
if type(self.mesh) is not nt.Mesh:
raise RuntimeError("Please Pass a Mesh object to NormalData Class")

def __getitem__(self,item):
vtxFace = self.mesh.vtxFace[item]
return {face_idx:pm.polyNormalPerVertex(vtxFace[face_idx],q=1,normalXYZ=1) for _,face_idx in vtxFace.indicesIter()}

def __repr__(self):
return "%s('%s')" % (self.__class__.__name__,self.mesh)

mesh_normal = NormalData(pm.selected())

print(mesh_normal)
print(mesh_normal[0])
print(mesh_normal[0][0])
# NormalData('pSphereShape1')
# {19: [0.19055484235286713, -0.9797220230102539, -0.06191524863243103], 0: [0.19055484235286713, -0.9797220230102539, -0.06191524863243103], 379: [0.19055484235286713, -0.9797220230102539, -0.06191524863243103], 360: [0.19055484235286713, -0.9797220230102539, -0.06191524863243103]}
# [0.19055484235286713, -0.9797220230102539, -0.06191524863243103]

总结

  以上就是 Maya 的法线获取的总结。
  另外我补充一下关于法线解锁的问题, OpenMaya 中的 MFnMesh 有命令可以获取到模型的边是否为硬边和软边 isEdgeSmooth
  但是如果导出 FBX 之类的格式没有保留平滑组信息,或者是 Max 导入到 Maya 之类的情况,上面提到的命令就不管用了,即便看起来是有软硬边,但是获取的数据都是 False
  这也导致法线解锁所有的法线变硬边了,这种情况下是可以通过遍历获取面顶点的法线来重新恢软硬边的信息的,虽然我针对这个情况开发了 Maya 法线解锁工具,但是工具实现的效果并不理想。 github