0%

Maya中法线转顶点色且可逆

摘要

因为项目需要, 编写了一个Maya脚本, 可以 快速 将顶点(切线/世界)法线转换为顶点色, 并可逆转换回来.

基本基于OpenMaya API 1.0编写, 速度非常快.只需要几秒就可以处理100W+三角形

实现代码

  • 人生苦短
  • 我用Python
  • 除非性能不够
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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
# coding=utf-8

# Script by MineClever, help to process high mesh normal on low mesh ~
# Bug report to : https://t.me/minemineSlaver

import maya.api.OpenMaya as api
import maya.cmds as cmds
import six

class MSingleton(type):
"""metaclass for set singleton class"""
__debug = True

def __init__(self, *args, **kwargs):
self.__instance = None
super(MSingleton,self).__init__(*args, **kwargs)

def __call__(self, *args, **kwargs):
if self.__instance is None:
if self.__debug: print(u"current class {} create singleton class".format(self.__class__))
self.__instance = super(MSingleton,self).__call__(*args, **kwargs)
return self.__instance


class MSelectionHelper(six.with_metaclass(MSingleton,object)):
__debug = False
tempSelectionList = api.MSelectionList()

@classmethod
def getCurMeshSelections(cls):
"""Return all mesh shape current selected"""
selList = api.MGlobal.getActiveSelectionList() # type: api.MSelectionList
shapeList = cls.tempSelectionList
shapeList.clear()
numOfSelected = selList.length() # type: int
if cls.__debug: print("Current selection count : {}".format(numOfSelected))

# NOTE: check if empty list
if not (numOfSelected >0) :
return shapeList

# NOTE: add all shape into selection list
for i in range(numOfSelected):
dagPath = selList.getDagPath(i) # type: api.MDagPath
if (dagPath.numberOfShapesDirectlyBelow() > 0): dagPath.extendToShape()
# NOTE: check if mesh type
if cls.__debug: print("selection[{}]:\"{}\" @ type: {}".format(i, dagPath, dagPath.apiType()))
if (dagPath.apiType() == 296) :
shapeList.add(dagPath)
if cls.__debug: print(u"shape list count: {}".format(shapeList.length()))
return shapeList


class MMProcessorBar(six.with_metaclass(MSingleton,object)):
__debug = False

__processorCount = 0
__processorTotalCount = 0
__bShowProcessBar = False

vtxUpdateThresholdNum = 40960

@classmethod
def setProcessorCountBarTotalNum(cls,inTotalCount):
# type: (int) -> None
if not cls.__bShowProcessBar:
if cls.__debug: print("Is not able to set a processor bar now")
return

cls.__processorTotalCount = inTotalCount
cmds.progressWindow(
title='Process Vertex Data',
progress=0,
status=("Done {0}/{1}\t".format(0, inTotalCount)),
isInterruptable=True,
min=0,
max=inTotalCount)

@classmethod
def updateProcessorCountBar(cls,inCurCount):
# type: (int) -> None
if not cls.__bShowProcessBar:
return

cls.__processorCount = inCurCount
if cls.__bShowProcessBar :
if (cmds.progressWindow( query=True, progress=True ) >= cls.__processorTotalCount) or \
(cmds.progressWindow( query=True, isCancelled=True ) ) :
cls.endProcessorCountBar()
else:
cmds.progressWindow( edit=True, progress=inCurCount, status=("Done {0}/{1}\t".format(cls.__processorCount, cls.__processorTotalCount)) )

@classmethod
def endProcessorCountBar(cls):
cls.__processorCount = 0
cls.__bShowProcessBar = False
cmds.progressWindow(endProgress=1)

@classmethod
def checkIfUseProcessorCountBar(cls,inTotalCount):
# type: (int) -> bool
cls.endProcessorCountBar()
cls.__bShowProcessBar = True if inTotalCount > cls.vtxUpdateThresholdNum else False
if cls.__bShowProcessBar:
cls.setProcessorCountBarTotalNum(inTotalCount)
return cls.__bShowProcessBar

@classmethod
def getCurProcessorCountBarStatus (cls):
# type: (...) -> bool
return cls.__bShowProcessBar

@classmethod
def getCurProcessorCountBarNum(cls):
# type: (...) -> int
return cls.__processorCount


class MMeshFaceVtxProcessorInterface(six.with_metaclass(MSingleton,object)):
__debug = False
halfVec = api.MVector(0.5, 0.5, 0.5)

@classmethod
def iterMeshFaceVtx (cls, inDagPath, inColorSetName = "FaceVtxNorCol"):
# type: (api.MDagPath, str) -> bool
return False


class MMeshFaceVtxProcessorBase(MMeshFaceVtxProcessorInterface):

processorBarCls = MMProcessorBar

@classmethod
def iterMeshFaceVtx (cls, inDagPath, inColorSetName = "FaceVtxNorCol"):
# type: (api.MDagPath, str) -> bool
fnMesh = api.MFnMesh(inDagPath)

# NOTE: Init processor bar
cls.processorBarCls.checkIfUseProcessorCountBar(fnMesh.numFaceVertices)

# NOTE: force set current color set
if not cls._processCurColorSet(fnMesh, inColorSetName): return False

# NOTE: Traverse all faceVertex in current mesh
meshFaceVertexIt = api.MItMeshFaceVertex(inDagPath)

return cls._iterMeshFaceVtxDataProcess(fnMesh, meshFaceVertexIt)

@classmethod
def _processCurColorSet(cls, fnMesh, inColorSetName):
# type: (api.MFnMesh, str) -> bool
""" check current color set """
return False

@classmethod
def _iterMeshFaceVtxDataProcess (cls, fnMesh, meshFaceVertexIt):
#type: (api.MFnMesh, api.MItMeshFaceVertex,) -> bool
"""Traverse all faceVertex in current mesh"""
return False


class MMeshFaceVtxProcessorCol2Nor(MMeshFaceVtxProcessorBase):

@classmethod
def _processCurColorSet(cls, fnMesh, inColorSetName):
# type: (api.MFnMesh, str) -> bool
""" check current color set """

# NOTE: check if valid color set to set color
colorSets = fnMesh.getColorSetNames() # type: list[str]
if not inColorSetName in colorSets:
# NOTE: Create new color set
api.MGlobal.displayWarning(u"\"{}\" dont have colorSet \"{}\" to recovery normal".format(fnMesh.dagPath(), inColorSetName))
return False
return True

@classmethod
def _iterMeshFaceVtxDataProcess (cls, fnMesh, meshFaceVertexIt):
#type: (api.MFnMesh, api.MItMeshFaceVertex,) -> bool
"""Traverse all faceVertex in current mesh"""

# NOTE: Get Current Color Set name
curColorSetName = fnMesh.currentColorSetName()

# NOTE: Save to temp list
normals = api.MVectorArray()
faceIds = api.MIntArray()
vertexIds=api.MIntArray()

# NOTE: Processor bar update data
curIndex = 0
nextUpdateNum = cls.processorBarCls.vtxUpdateThresholdNum

while not meshFaceVertexIt.isDone():
faceVtxCol = meshFaceVertexIt.getColor(curColorSetName) # type: api.MColor
faceVtxCor2Vec = api.MVector(faceVtxCol[0],faceVtxCol[1],faceVtxCol[2])
normals.append((faceVtxCor2Vec - cls.halfVec) * 2)
vertexIds.append(meshFaceVertexIt.vertexId())
faceIds.append(meshFaceVertexIt.faceId())
meshFaceVertexIt.next()

# NOTE: update processor bar
curIndex+=1
if curIndex >= nextUpdateNum:
nextUpdateNum += cls.processorBarCls.vtxUpdateThresholdNum
cls.processorBarCls.updateProcessorCountBar(curIndex)
else:
fnMesh.setFaceVertexNormals(normals, faceIds, vertexIds)
normals = faceIds = vertexIds = None
cls.processorBarCls.endProcessorCountBar() # NOTE: end processor bar
return True
return False


class MMeshFaceVtxProcessorWsNor2Col(MMeshFaceVtxProcessorBase):

@classmethod
def _processCurColorSet(cls,fnMesh, inColorSetName):
# type: (api.MFnMesh, str) -> bool
""" force set current color set """

# NOTE: check if valid color set to set color
colorSets = fnMesh.getColorSetNames() # type: list[str]
if not inColorSetName in colorSets:
# NOTE: Create new color set
fnMesh.createColorSet(inColorSetName, True)
fnMesh.setCurrentColorSetName(inColorSetName)
return True

@classmethod
def _iterMeshFaceVtxDataProcess (cls, fnMesh, meshFaceVertexIt):
#type: (api.MFnMesh, api.MItMeshFaceVertex,) -> bool
"""Traverse all faceVertex in current mesh"""

# -- NOTE: Save to temp list
colors = api.MColorArray()
faceIds = api.MIntArray()
vertexIds=api.MIntArray()

# NOTE: Processor bar update data
curIndex = 0
nextUpdateNum = cls.processorBarCls.vtxUpdateThresholdNum
modifier = api.MDGModifier()

while not meshFaceVertexIt.isDone():
norVec = meshFaceVertexIt.getNormal() # type: api.MVector
colors.append(norVec * 0.5 + cls.halfVec)
vertexIds.append(meshFaceVertexIt.vertexId())
faceIds.append(meshFaceVertexIt.faceId())
meshFaceVertexIt.next()

# NOTE: update processor bar
curIndex+=1
if curIndex >= nextUpdateNum:
nextUpdateNum += cls.processorBarCls.vtxUpdateThresholdNum
cls.processorBarCls.updateProcessorCountBar(curIndex)
else:
fnMesh.setFaceVertexColors(colors, faceIds, vertexIds, modifier=modifier)
modifier.doIt()
colors = faceIds = vertexIds = None
cls.processorBarCls.endProcessorCountBar() # NOTE: end processor bar
return True
return False


class MMeshFaceVtxProcessorTanNor2Col(MMeshFaceVtxProcessorInterface):

@classmethod
def iterMeshFaceVtx (cls, inDagPath, inColorSetName = "FaceVtxNorCol"):
# type: (api.MDagPath, str) -> bool
modifier = api.MDGModifier()
fnMesh = api.MFnMesh(inDagPath)

# NOTE: check if valid color set to set color
colorSets = fnMesh.getColorSetNames() # type: list[str]
if not inColorSetName in colorSets:
# NOTE: Create new color set
fnMesh.createColorSet(inColorSetName, True)
fnMesh.setCurrentColorSetName(inColorSetName)

# NOTE: Get current uvSet name
curUvSet = fnMesh.currentUVSetName() # type: str

# NOTE: Traverse all faceVertex in current mesh
meshFaceVertexIt = api.MItMeshFaceVertex(inDagPath)

# -- NOTE: Save to temp list
colors = api.MColorArray()
faceIds = api.MIntArray()
vertexIds=api.MIntArray()
zeroArray = (0,0,0)

while not meshFaceVertexIt.isDone():
norVec = meshFaceVertexIt.getNormal() # type: api.MVector
binorVec = meshFaceVertexIt.getBinormal(uvSet=curUvSet) # type: api.MVector
tanVec = meshFaceVertexIt.getTangent(uvSet=curUvSet) # type: api.MVector

# NOTE: build TBN Matrix
tbnMatrix = api.MMatrix([vec[i] if i < 3 else 0 for vec in (tanVec, binorVec, norVec, zeroArray) for i in range(4)])

# NOTE: pre-multiply TBN matrix. same as post-multiply transposed TBN matrix
tanNorVec = tbnMatrix * norVec # type: api.MVector
colors.append(tanNorVec * 0.5 + cls.halfVec)
vertexIds.append(meshFaceVertexIt.vertexId())
faceIds.append(meshFaceVertexIt.faceId())
meshFaceVertexIt.next()
else:
fnMesh.setFaceVertexColors(colors, faceIds, vertexIds, modifier=modifier)
modifier.doIt()
colors = faceIds = vertexIds = None
return True
return False


class MMeshFaceVtxProcessorTanOffsetNor2Col(MMeshFaceVtxProcessorInterface):

@classmethod
def iterMeshFaceVtx (cls, inDagPath, inColorSetName = "FaceVtxNorCol"):
# type: (api.MDagPath, str) -> bool
modifier = api.MDGModifier()
fnMesh = api.MFnMesh(inDagPath)

# NOTE: check if valid color set to set color
colorSets = fnMesh.getColorSetNames() # type: list[str]
if not inColorSetName in colorSets:
# NOTE: Create new color set
fnMesh.createColorSet(inColorSetName, True)
fnMesh.setCurrentColorSetName(inColorSetName)

# NOTE: Get current uvSet name
curUvSet = fnMesh.currentUVSetName() # type: str

# NOTE: Traverse all faceVertex in current mesh
meshFaceVertexIt = api.MItMeshFaceVertex(inDagPath)

# -- NOTE: Save to temp list
colors = api.MColorArray()
faceIds = api.MIntArray()
vertexIds=api.MIntArray()
zeroArray = (0,0,0)
refColorSetName= inColorSetName

while not meshFaceVertexIt.isDone():
faceVtxCol = meshFaceVertexIt.getColor(refColorSetName) # type: api.MColor
faceVtxCor2Vec = api.MVector(faceVtxCol[0],faceVtxCol[1],faceVtxCol[2])
decodedNorVec = (faceVtxCor2Vec - cls.halfVec) * 2 # type: api.MVector

norVec = meshFaceVertexIt.getNormal() # type: api.MVector
binorVec = meshFaceVertexIt.getBinormal(uvSet=curUvSet) # type: api.MVector
tanVec = meshFaceVertexIt.getTangent(uvSet=curUvSet) # type: api.MVector

# NOTE: build TBN Matrix
tbnMatrix = api.MMatrix([vec[i] if i < 3 else 0 for vec in (tanVec, binorVec, norVec, zeroArray) for i in range(4)])

# NOTE: pre-multiply TBN matrix. same as post-multiply transposed TBN matrix
tanNorVec = tbnMatrix * decodedNorVec # type: api.MVector
colors.append(tanNorVec * 0.5 + cls.halfVec)
vertexIds.append(meshFaceVertexIt.vertexId())
faceIds.append(meshFaceVertexIt.faceId())
meshFaceVertexIt.next()
else:
fnMesh.setFaceVertexColors(colors, faceIds, vertexIds, modifier=modifier)
modifier.doIt()
colors = faceIds = vertexIds = None
return True
return False


class MVtxNorColConverter (object):
__debug = True
__convertClass = MMeshFaceVtxProcessorInterface
__validConvertClass = False
__colorSetName = "FaceVtxNorCol"

@classmethod
def setConvertClass (cls, inConvertClass):
# type: (MMeshFaceVtxProcessorInterface) -> bool

if MMeshFaceVtxProcessorInterface.__subclasscheck__(inConvertClass):
cls.__convertClass = inConvertClass
cls.__validConvertClass = True
return True
else:
raise Exception("Not valid convert class")

@classmethod
def setColorSetName (cls, colSetName):
# type: (str) -> bool
if isinstance(colSetName,str):
cls.__colorSetName = colSetName
return True
return False

@classmethod
def getColorSetName (cls):
# type: (...) -> str
return cls.__colorSetName

@classmethod
def getCurConvertClassName (cls):
# type: (...) -> str
return cls.__convertClass.__name__

@classmethod
def _convertSelMesh(cls):
# type: (...) -> bool
curMeshList = MSelectionHelper.getCurMeshSelections()
if curMeshList.isEmpty():
api.MGlobal.displayWarning(u"No mesh has been selected?")
return False

curMeshListIt = api.MItSelectionList(curMeshList)
while not (curMeshListIt.isDone()):
curMeshDagPath= curMeshListIt.getDagPath() # type: api.MDagPath
if not cls.iterMeshFaceVtx(curMeshDagPath): print(u"process mesh \"{}\" fail".format(curMeshDagPath))
if cmds.progressWindow( query=True, isCancelled=True ):
api.MGlobal.displayWarning("Processor end by user!")
break
curMeshListIt.next()
else:
return True

@classmethod
def convertSelMesh (cls):
try:
cls._convertSelMesh()
except Exception as E:
api.MGlobal.displayError(str(E))
finally:
# NOTE: Force end process window
cmds.progressWindow(endProgress=1)

@classmethod
def iterMeshFaceVtx (cls, inDagPath):
# type: (api.MDagPath) -> bool
if not cls.__validConvertClass :
raise Exception("Not set a valid convert class")
return cls.__convertClass.iterMeshFaceVtx(inDagPath, cls.__colorSetName)

def runScript_nor2col(*args, **kw):
MVtxNorColConverter.setConvertClass(MMeshFaceVtxProcessorWsNor2Col)
MVtxNorColConverter.convertSelMesh()

def runScript_tanNor2tanCol(*args, **kw):
"""Generate Tangent space Normal, test only!"""
MVtxNorColConverter.setConvertClass(MMeshFaceVtxProcessorTanOffsetNor2Col)
MVtxNorColConverter.convertSelMesh()

def runScript_col2nor(*args, **kw):
MVtxNorColConverter.setConvertClass(MMeshFaceVtxProcessorCol2Nor)
MVtxNorColConverter.convertSelMesh()

if __name__ == "__main__":
runScript_nor2col()

注释

  • 理论上还是够快的
  • 如果用C++ 重写, 应该可以考虑上多线程, 分割处理所有顶点面, 应当可以处理上亿三角形

歡迎關注我的其它發布渠道