前言
三年没在博客写文了,快要查无此人了。一晃就三年过去了,三年前还是刚刚毕业的菜鸡,三年后的现在,还是菜哈哈。
之前一直在做客户端系统(直白说就是UI拼图仔)和SDK相关的工作,兜兜转转一圈回来后发现,果然还是TA更适合我哈哈。
好了废话不多说,进入今天的正题。最近恰好在做Back Facing描边的相关工作,理所当然的遇到了模型硬边描边断裂的问题,网上一搜恰好看到了这篇很好的文章,真是瞌睡来了遇到枕头。
https://blog.csdn.net/oldside1/article/details/106456379
不过原文作者是用C++写的,网搜也搜了一圈发现好像还没有Python版本的(FBX SDK明明也有Python版本的说)。
没办法,只能自己动手用Python写一遍。
思路
代码实现思路很简单,找到Mesh中控制点的所有法线,把这些法线按特定的算法平均一下就可以。算法有好几种,直接平均,
按面积加权平均,按夹角加权平均。这里偷懒就直接加起来平均吧…
顺带一提,原文作者用了两个for循环嵌套去查找Mesh的控制点,在模型顶点数量少的时候还好,但当我用一个2.7w面数,8w顶点的模型去测试的时候,时间复杂度O( n 2 n^2 n2)的算法,光查找控制点就要执行64亿次,直接两眼一黑。没办法,只能再优化一下算法,把所有数据以控制点为key,法线列表为value,用字典存起来,这样我们只需O(n)的复杂度就能完成控制点和法线数据的查找。
代码
获取所有控制点及其法线
# 控制顶点字典 <int, List<FbxVector4>> <顶点索引,法线列表>
vertCtrlPoint = {}
# 控制顶点平均法线字典 <int, FbxVector4> <顶点索引, 平均法线>
vertAvgNormal = {}
# 先取到所有控制点的法线
for j in range(polygonVetexCount):
#print("正在读取", j, "/", polygonVetexCount)
rCurVertexIndex = polygonVertices[j]
rNormal: FbxVector4 = vertNormalArray[j]
if rCurVertexIndex not in vertCtrlPoint:
normals = [rNormal]
vertCtrlPoint[rCurVertexIndex] = normals
else:
if rNormal not in vertCtrlPoint[rCurVertexIndex]:
vertCtrlPoint[rCurVertexIndex].append(rNormal)
得到了控制点所有的法线数据,就能够计算平均法线了,这里可以换成其他的平均算法。
# 平均法线值
for key, value in vertCtrlPoint.items():
weightNormal = FbxVector4(0, 0, 0, 0)
for k in range(len(value)):
weightNormal += value[k]
weightNormal.Normalize()
vertAvgNormal[key] = weightNormal
其余就和之前的代码一样了,把法线转换到切线空间,并映射到颜色通道里去(因为取值范围不同,向量[-1, 1] ->顶点色[0, 1])
# 构建TBN,转换到切线空间下
normal = vertNormalArray[m]
tangent = vertTangentArray[m]
binormal = vertBinormalArray[m]
tempVector = FbxVector4(tangent.DotProduct(smoothNormal),
binormal.DotProduct(smoothNormal),
normal.DotProduct(smoothNormal),
0)
smoothNormal = tempVector
smoothNormal.Normalize()
# 映射到顶点色中
color = FbxColor()
color.mRed = smoothNormal[0] * 0.5 + 0.5
color.mGreen = smoothNormal[1] * 0.5 + 0.5
color.mBlue = smoothNormal[2] * 0.5 + 0.5
color.mAlpha = vertColorArray[vertColorIndex].mAlpha
补充
贴一个创建顶点色通道的代码,自己要去开Maya再加顶点色再导出来确实挺麻烦的
def AddVertColor(self, node:FbxNode):
if node.GetChildCount() > 0:
for i in range(node.GetChildCount()):
mesh = node.GetChild(i).GetMesh()
if mesh is not None :
layer = mesh.GetLayer(0)
vertexColor = FbxLayerElementVertexColor.Create(mesh, "")
vertexColor.SetMappingMode(FbxLayerElement.eByPolygonVertex)
vertexColor.SetReferenceMode(FbxLayerElement.eIndexToDirect)
vertexColorArray = vertexColor.GetDirectArray()
vertexIndexArray = vertexColor.GetIndexArray()
for j in range(mesh.GetPolygonVertexCount()):
vertexColorArray.Add(FbxColor(1, 1, 1, 1))
vertexIndexArray.Add(j)
layer.SetVertexColors(vertexColor)
self.AddVertColor(node.GetChild(i))
还有一件事,GetXXX()方法的开销很大,建议缓存起来
# 缓存数据,减少调用GetXXX()产生的开销
polygonVetexCount = mesh.GetPolygonVertexCount()
polygonVertices = mesh.GetPolygonVertices()
vertNormalArray = vertNormals.GetDirectArray()
vertTangentArray = vertTangents.GetDirectArray()
vertBinormalArray = vertBinormals.GetDirectArray()
vertColorArray = vertColor.GetDirectArray()
vertColorIndexArray = vertColor.GetIndexArray()
没缓存前处理8w顶点的模型耗时大概需要3分钟,缓存后…
快的令我有点诧异,居然耗时一秒都不到…
小结
本文只提供了Python版本及优化后的实现思路,涉及具体的原理建议去开头的文章里去看看,这里就不在过多赘述了。
该脚本可以整合加入到Maya工作流当中,实现静默处理,十分便利,不过现在手上还有动画相关的内容要处理,就先到此为止吧。
最后
还有一件事,我把源码放到GitHub上了,同时里面附带了Python3.7版本的FBX SDK库,有需要的小伙伴可以自行下载哦。
链接:AvgNormal-Python-FBX-SDK
就这样,12点了该睡觉了。
咱们下次见QwQ ,晚安好梦。