前言

  这个需求是好久个月前就已经实现了的,这里补充一下文章,记录实现的过程。
  需求是这样的,描边需要用额外的法线信息进行处理,但是当前角色的法线已经有软硬的用途。
  因此需要将额外的圆滑过的法线信息写入到 切线 中,引擎的 shader 读取切线进行描边绘制。

  最开始要做这个需求,我想到的是利用 OpenMaya 的 API 将法线写入到切线就完事了。
  然而我大意了,我发现 Maya 的 MFnMesh 只有获取切线信息的没有修改的功能 链接

  在网上也搜了一遍,似乎也没有很好的解决方案。
  不过我的前导师有给我留了一手,虽然他的脚本是对接 Unity 的,但是思路是完全可以借鉴的。

  他的实现方式是将 FBX 保存为 ASCII 模式,然后用正则表达式将模型的法线信息替换到切线信息上。
  这个方案用纯 Python 即可实现,方便高效。

  只是我觉得用 FBX SDK 的话或许效率会更高,也能直接处理 Binary 的文件。

alt

FBX SDK 使用

  关于 FBX SDK 是什么,如何使用,我在之前写的文章有所介绍 链接
  FBX Python SDK 里面有支持 Py2.7 和 Py3.3 的 pyd 脚本,并且里面还有代码案例,可以参考学习。
  Py3.7 版本有需要可以在这篇文章末尾拿到 链接

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import fbx
import FbxCommon

def clear_uv(output_file):
"""删除第一张 UV"""
# NOTE 读取文件信息
manager, scene = FbxCommon.InitializeSdkObjects()
result = FbxCommon.LoadScene(manager, scene, output_file)
if not result:
return
# NOTE 获取节点信息
nodes = scene.GetRootNode()
# NOTE 获取大纲中第一个模型
mesh = nodes.GetChild(0).GetMesh()
uv = mesh.GetElementUV(0)
# NOTE 删除第一个 UV 的信息
mesh.RemoveElementUV(uv)
# NOTE 保存文件输出
FbxCommon.SaveScene(manager, scene, output_file)

  上面是我自己写的一个简单的示例代码,可以清理 FBX 里面的第一份 UV 信息。
  使用上和 C++ API保持一致,返回的数据结构可以查 C++ 文档

  我自己写的或者收集的东西放到这个仓库下了 链接

FBX SDK 将模型法线写入到切线

  思路: 输出两个 FBX 一个是正常输出,一个将模型的法线平滑再输出。
  输出完成之后将平滑法线的模型 写入到 切线 上。

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
import os
import tempfile

import pymel.core as pm
import pymel.core.nodetypes as nt
from maya import mel

import FbxCommon
from fbx import FbxDocumentInfo,FbxNodeAttribute

# NOTE 临时目录输出 FBX
SOFT_FBX_PATH = os.path.join(tempfile.gettempdir(), "soft_normal.fbx")

class TangentHandler(object):

info = {}

@classmethod
def _export_soft_fbx(cls):
"""导出 平滑 法线的 FBX"""
meshes = cls._soft_mesh()
pm.select(meshes, r=True)
mel.eval('FBXExport -f "%s" -s' % SOFT_FBX_PATH.replace("\\", "/"))
pm.delete(meshes)

@classmethod
def _soft_mesh(cls):
"""平滑法线"""
new_meshes = []
for mesh in pm.ls(sl=1, dag=1):
if hasattr(mesh, "getShape") and isinstance(mesh.getShape(), nt.Mesh):
# NOTE 如果 outline 模型用调整的法线
outline_mesh = mesh + "_outline"
if not pm.objExists(outline_mesh):
outline_mesh = pm.duplicate(mesh, n=outline_mesh)[0]
pm.polyNormalPerVertex(outline_mesh, unFreezeNormal=True)
# NOTE 硬化边
pm.polySoftEdge(outline_mesh, angle=0, constructionHistory=False)
# NOTE 平均法线
pm.polyAverageNormal(outline_mesh)
new_meshes.append(outline_mesh)
return new_meshes

@staticmethod
def _read_fbx_attribute(path, attribute):
"""读取 FBX 属性"""
manager, scene = FbxCommon.InitializeSdkObjects()
result = FbxCommon.LoadScene(manager, scene, path)
assert result, u"无法打开 FBX 文件 %s" % path

data = {}
for i in range(scene.GetNodeCount()):
node = scene.GetNode(i)
mesh = node.GetNodeAttribute()
if not mesh or mesh.GetAttributeType() != FbxNodeAttribute.eMesh:
continue

# NOTE 根据属性名动态获取属性
attr = getattr(mesh, "GetElement%s" % attribute.capitalize())()
if not attr:
continue
data[node.GetName()] = attr.GetDirectArray()

return {"manager": manager, "scene": scene, "data": data}


@classmethod
def set_tangent(cls, fbx_path, meshes):
# NOTE 选择模型
pm.select(meshes)
cls._export_soft_fbx()
origin_data = cls._read_fbx_attribute(fbx_path, "Tangent")
target_data = cls._read_fbx_attribute(SOFT_FBX_PATH, "Normal")
manager = origin_data["manager"]
scene = origin_data["scene"]
tangent_data = origin_data["data"]
normal_data = target_data["data"]

# NOTE 特殊切线导出
for name, array in tangent_data.items():
normals = normal_data[name + "_outline"]
array.Clear()
for normal in normals:
array.Add(normal)

FbxCommon.SaveScene(manager, scene, fbx_path, 0)

# NOTE 输出的 FBX 路径
fbx_path = r"E:\MayaTecent\MayaScript\FBXSDK\box.fbx"
# NOTE 处理的模型
export_meshes = [mesh for mesh in pm.ls(ni=1,assemblies=1) if isinstance(mesh.getShape(),nt.Mesh)]
TangentHandler.set_tangent(fbx_path, export_meshes)

总结

  默认不修改引擎的情况下,导入的切线无法通过重新导入来更新。
  并且切线信息的调用上,也会有些问题,unreal 会把切线当成正常的切线进行计算。