3ds Max 和 Maya 是目前最常用的建模工具之一,最近在为项目开发需求时发现,这两者在导入导出时的设置有着不小的区别。
最影响体验的就是,3ds Max在导入和导出模型时会阻塞当前max除了主窗口的所有窗口,而且速度上也不是很快,导出一个模型要卡好几秒,期间所有基于3ds Max的窗口都会被隐藏,对于插件设计来讲十分的致命。
基于此,直接从Max中读取模型数据是一个更为理想的方法,下面就介绍下一些关键接口,不过在此之前我先记录一个Max导出设置的大坑。
这里我获取数据的目的是为了从美术资产中提取出需要的部分数据并将其导出为一个新的fbx模型,因此对获取到的数据格式进行了向fbx的格式转换。如何将获取到的数据利用fbx sdk导出成新的模型,见我的另一篇博客:使用fbx sdk将获取的数据转换为模型导出。
1. 多边形拆分问题
3ds Max 默认导出设置会将多边形都拆成三角面,导致模型在检测时出现问题,这一度让我觉得是不是Max本身的问题。
最后发现是Max设置中有一个保留边缘方向(Preserve edge orientation),默认是勾选上的,它会导致将多边形都拆成三角面,将其设为False就行了。
2. Pymxs库
最早的3ds Max中提供的Python API是MaxPlus
库, 它只是对C++版本的sdk的一个缩减封装版,功能很少,也不太实用。调用它甚至不如直接使用MaxScript来的方便。
从3ds Max 2017开始,它提供了一个全新的Python API,也就是Pymxs
用于在3ds Max中执行MaxScript代码。它提供了一个Python接口,可以在Python脚本中调用MaxScript函数和命令,以及访问3ds Max中的对象和属性。使用pymxs库,可以将Python和MaxScript结合起来,实现更高效、更灵活的3D建模和动画制作。
pymxs库的主要特点包括:
-
支持Python 2和Python 3。
-
提供了完整的MaxScript函数和命令的Python接口。
-
可以在Python脚本中访问3ds Max中的对象和属性,例如场景、节点、材质、动画等。
-
支持Python和MaxScript之间的数据转换,例如将Python列表转换为MaxScript数组,将Python字典转换为MaxScript结构体等。
-
可以在Python脚本中执行MaxScript代码,并获取执行结果。
-
支持Python和MaxScript之间的交互式会话,可以在3ds Max中打开Python控制台,并在其中执行Python代码和MaxScript代码。
这边使用的就是基于Pymxs库来读取Max的数据。
3. 获取模型数据
首先,必须要知道的一个重要常识是,3ds Max中的索引大部分都是从1开始的,与Python不同,使用时必须注意到这点去转换索引。
3.1 获取选择模型
通过getCurrentSelection
接口就可以直接获取到当前选择的模型节点集合
后面通过遍历该集合中的节点node来操作来获取数据
# 引入Max的python库
from pymxs import runtime as rt
# 引入fbx sdk
import fbx
exportList = rt.getCurrentSelection() # 获取当前所有选择的模型
由于模型类型poly和mesh类型的接口不一致,为了方便起见,可以通过类型判断,用一个变量统一地使用接口
rt_convert = rt.polyop
nodecls = rt.classOf(node)
if str(nodecls) == "Editable_mesh":
rt_convert = rt.meshop
3.2 获取顶点
直接通过节点的属性就可以直接访问到模型的顶点数据,不过需要注意这里获取的verts的坐标在其pos属性中,顶点的三维坐标分别对应于pos属性中的x,y,z的值。
maxmesh = node.mesh
vertices = maxmesh.verts
pMeshVerts = [fbx.FbxVector4(vertex.pos.x, vertex.pos.y, vertex.pos.z) for vertex in vertices]
3.3 获取法线
获取法线数据时,必须要注意,直接通过3ds Max提供的 getNormal
接口去获取,得到的法线数据是不正确的。
这里直接以Maxscript获取一个立方体的例子举例:
vertNum = box01.numverts
8
for i in 1 to vertNum do
(
local vertNormal = getNormal box01.mesh i
print vertNormal
)
[0,0,-1.5708]
[0,0,-1.5708]
[0,0,-1.5708]
[0,0,-1.5708]
[0,0,1.5708]
[0,0,1.5708]
[0,0,1.5708]
[0,0,1.5708]
OK
通过getNormal
方法获得的顶点法线数据是显然错误的,首先,8个点只有2种法线方向,然后,这个法线也没有归一化,长度并不是1的,所以这个获取的结果完全是没法用的。而且实际上,我们知道,一个顶点有可能不止一个法线方向的。事实上,这里按理说应该有24个法线。
为了正确的获取法线,必须先为节点加入编辑法线的修改器,然后获取完删掉即可。
# normals
modN = node.modifiers[0]
faceNum = modN.GetNumFaces()
faceNormalIdList = [[modN.GetNormalID(i + 1, j + 1) for j in range(modN.GetFaceDegree(i + 1))] for i in
range(faceNum)]
infx = set([nidx for fn in faceNormalIdList for nidx in fn])
temp = {}
for ndx in infx:
n = modN.GetNormal(ndx)
temp[ndx] = fbx.FbxVector4(n[0], n[1], n[2])
rt.deleteModifier(node, modN)
normals = [temp[nidx] for fn in faceNormalIdList for nidx in fn]
这里,modN.GetFaceDegree(i + 1)
的含义可以理解成这个面由多少个不同角度的点组成,然后遍历组成面的这些点,通过GetNormalID
方法,获得组成这个面的顶点对应的normalId。
接着通过normalId获取normal的值,这里使用集合是为了减少对重复的normalId进行接口调用,加快速度。
3.4 获取面
稍微注意下这里mesh和poly类型的接口使用的不同,一般而言,这两者的接口名称是相同的,但是这里不同。
# faces
pMeshFaces = []
face_mat_ids = []
num_faces = rt_convert.getNumFaces(node)
if rt_convert == rt.polyop:
for i in range(num_faces):
face = rt_convert.getFaceVerts(node, i + 1)
mat_id = rt_convert.getFaceMatID(node, i + 1)
Fa = [int(face[j] - 1) for j in range(len(face))]
pMeshFaces.append(Fa)
face_mat_ids.append(mat_id)
else:
for i in range(num_faces):
face = rt.getFace(maxmesh, i + 1)
mat_id = rt.getFaceMatID(maxmesh, i + 1)
Fa = [int(face[j] - 1) for j in range(len(face))]
pMeshFaces.append(Fa)
face_mat_ids.append(mat_id)
3.5 获取Map
首先通过getNumMaps
获取该模型的通道数,然后遍历每个通道,通过getMapVert
和getMapFace
获取顶点和面数据。
UVData
是我自定义的数据类型,用于保存这些数据。
numMaps = rt_convert.getNumMaps(node)
UVDatas = []
for index in range(1, numMaps):
numFaces = rt_convert.getNumMapFaces(node, index)
numVerts = rt_convert.getNumMapVerts(node, index)
pMapVerts = []
for i in range(numVerts):
vert = rt_convert.getMapVert(node, index, i + 1)
pMapVerts.append(vert)
pMapFaces = []
for i in range(numFaces):
face = rt_convert.getMapFace(node, index, i + 1)
Fa = []
for j in range(len(face)):
Fa.append(int(face[j]))
pMapFaces.append(Fa)
uv_data = UVData(pMapVerts, pMapFaces, numVerts, numFaces, uv_name)
UVDatas.append(uv_data)
3.6 获取变换属性
通过node可以直接访问到模型的变换属性,分为局部变换和全局变换。主要包括三个属性:Translation 平移变换,Rotation 旋转变换以及Scaling 缩放变换。
值得一提的是,直接从Max中读取得到的是四元数表示的角度,这里为了将其转换为Fbx的欧拉角度角度,做了两次转换。先将其转换为Fbx的四元数类型,再利用FbxAMatrix
将其转换为欧拉角度。
# 局部变换属性
lclTranslation = fbx.FbxDouble3(node.pos.x, node.pos.y, node.pos.z)
lclRotation = node.rotation
lclRotation = fbx.FbxQuaternion(lclRotation.x, lclRotation.y, lclRotation.z, lclRotation.w)
fbxMatrix = fbx.FbxAMatrix()
fbxMatrix.SetQ(lclRotation)
# 将FbxAMatrix转换为欧拉角度
lclRotation = fbxMatrix.GetR()
lclRotation = fbx.FbxDouble3(lclRotation[0], lclRotation[1], lclRotation[2])
lclScaling = fbx.FbxDouble3(node.scale.x, node.scale.y, node.scale.z)
# 全局变换属性
geometricTranslation = node.objectOffsetPos
geometricTranslation = fbx.FbxDouble3(geometricTranslation.x, geometricTranslation.y, geometricTranslation.z)
geometricRotation = node.objectOffsetRot
geometricRotation = fbx.FbxQuaternion(geometricRotation.x, geometricRotation.y, geometricRotation.z,geometricRotation.w)
fbxMatrix.SetQ(geometricRotation)
geometricRotation = fbxMatrix.GetR()
geometricRotation = fbx.FbxDouble3(geometricRotation[0], geometricRotation[1], geometricRotation[2])
geometricScaling = node.objectOffsetScale
geometricScaling = fbx.FbxDouble3(geometricScaling.x, geometricScaling.y, geometricScaling.z)